diff --git a/public/images/icons/favicon_bw.ico b/public/images/icons/favicon_bw.ico new file mode 100644 index 0000000..af34a53 Binary files /dev/null and b/public/images/icons/favicon_bw.ico differ diff --git a/public/images/icons/favicon_bw_round.ico b/public/images/icons/favicon_bw_round.ico new file mode 100644 index 0000000..21cfaf9 Binary files /dev/null and b/public/images/icons/favicon_bw_round.ico differ diff --git a/public/images/icons/favicon_gold.ico b/public/images/icons/favicon_gold.ico new file mode 100644 index 0000000..f6fca30 Binary files /dev/null and b/public/images/icons/favicon_gold.ico differ diff --git a/public/images/logos/PP_Logo_BW.png b/public/images/logos/PP_Logo_BW.png new file mode 100644 index 0000000..b476314 Binary files /dev/null and b/public/images/logos/PP_Logo_BW.png differ diff --git a/public/images/logos/PP_Logo_BW_round.png b/public/images/logos/PP_Logo_BW_round.png new file mode 100644 index 0000000..b3539b0 Binary files /dev/null and b/public/images/logos/PP_Logo_BW_round.png differ diff --git a/src/app/admin/dev-management/hooks/executeSql.ts b/src/app/admin/dev-management/hooks/executeSql.ts new file mode 100644 index 0000000..bfa8b7f --- /dev/null +++ b/src/app/admin/dev-management/hooks/executeSql.ts @@ -0,0 +1,49 @@ +import { authFetch } from '../../../utils/authFetch'; +import { log } from '../../../utils/logger'; + +export type SqlExecutionData = { + result?: any; + isMulti?: boolean; +}; + +export type SqlExecutionMeta = { + durationMs?: number; +}; + +export async function importSqlDump(file: File): Promise<{ ok: boolean; data?: SqlExecutionData; meta?: SqlExecutionMeta; message?: string }>{ + const BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3001'; + const url = `${BASE_URL}/api/admin/dev/sql`; + + log('🧪 Dev SQL Import: POST', url); + try { + const form = new FormData(); + form.append('file', file); + const res = await authFetch(url, { + method: 'POST', + headers: { Accept: 'application/json' }, + body: form + }); + + let body: any = null; + try { + body = await res.clone().json(); + } catch { + body = null; + } + + if (res.status === 401) { + return { ok: false, message: 'Unauthorized. Please log in.' }; + } + if (res.status === 403) { + return { ok: false, message: body?.error || 'Forbidden. Admin access required.' }; + } + if (!res.ok) { + return { ok: false, message: body?.error || 'SQL execution failed.' }; + } + + return { ok: true, data: body?.data, meta: body?.meta }; + } catch (e: any) { + log('❌ Dev SQL: network error', e?.message || e); + return { ok: false, message: 'Network error while executing SQL.' }; + } +} diff --git a/src/app/admin/dev-management/page.tsx b/src/app/admin/dev-management/page.tsx new file mode 100644 index 0000000..aadeb04 --- /dev/null +++ b/src/app/admin/dev-management/page.tsx @@ -0,0 +1,197 @@ +'use client' + +import React from 'react' +import Header from '../../components/nav/Header' +import Footer from '../../components/Footer' +import PageTransitionEffect from '../../components/animation/pageTransitionEffect' +import { CommandLineIcon, PlayIcon, TrashIcon, ExclamationTriangleIcon, ArrowUpTrayIcon } from '@heroicons/react/24/outline' +import useAuthStore from '../../store/authStore' +import { useRouter } from 'next/navigation' +import { importSqlDump, SqlExecutionData, SqlExecutionMeta } from './hooks/executeSql' + +export default function DevManagementPage() { + const router = useRouter() + const user = useAuthStore(s => s.user) + + const isAdmin = + !!user && + ( + (user as any)?.role === 'admin' || + (user as any)?.userType === 'admin' || + (user as any)?.isAdmin === true || + ((user as any)?.roles?.includes?.('admin')) || + (user as any)?.role === 'super_admin' || + (user as any)?.userType === 'super_admin' || + (user as any)?.isSuperAdmin === true || + ((user as any)?.roles?.includes?.('super_admin')) + ) + + const [authChecked, setAuthChecked] = React.useState(false) + React.useEffect(() => { + if (user === null) { + router.replace('/login') + return + } + if (user && !isAdmin) { + router.replace('/admin') + return + } + setAuthChecked(true) + }, [user, isAdmin, router]) + + const [selectedFile, setSelectedFile] = React.useState(null) + const [loading, setLoading] = React.useState(false) + const [error, setError] = React.useState('') + const [result, setResult] = React.useState(null) + const [meta, setMeta] = React.useState(null) + const fileInputRef = React.useRef(null) + + const runImport = async () => { + setError('') + if (!selectedFile) { + setError('Please select a SQL dump file.') + return + } + setLoading(true) + try { + const res = await importSqlDump(selectedFile) + if (!res.ok) { + setError(res.message || 'Failed to import SQL dump.') + return + } + setResult(res.data || null) + setMeta(res.meta || null) + } finally { + setLoading(false) + } + } + + const clearResults = () => { + setResult(null) + setMeta(null) + setError('') + } + + const onImportFile = (e: React.ChangeEvent) => { + const file = e.target.files?.[0] + if (!file) return + setSelectedFile(file) + e.target.value = '' + } + + if (!authChecked) return null + + return ( + +
+
+
+
+
+
+
+ +
+
+

Dev Management

+

Import SQL dump files to run database migrations.

+
+
+
+ +
+
Use with caution
+
SQL dumps run immediately and can modify production data.
+
+
+
+ +
+
+
+

SQL Dump Import

+
+ + + +
+
+ +
+
Select a .sql dump file using Import SQL.
+
Only SQL dump files are supported.
+ {selectedFile && ( +
Selected: {selectedFile.name}
+ )} +
+ + + {error && ( +
+ {error} +
+ )} +
+ +
+

Result Summary

+
+
+ Result Sets + + {Array.isArray(result?.result) ? (result?.result as any[]).length : result?.result ? 1 : 0} + +
+
+ Multi-statement + {result?.isMulti ? 'Yes' : 'No'} +
+
+ Duration + {meta?.durationMs ? `${meta.durationMs} ms` : '-'} +
+
+
+ Multi-statement SQL and dump files are supported. Use with caution. +
+
+
+ +
+
+

Import Results

+
+ + {!result && ( +
No results yet. Import a SQL dump to see output.
+ )} + + {result?.result && ( +
+                  {JSON.stringify(result.result, null, 2)}
+                
+ )} +
+
+
+
+
+
+ ) +} diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 31d33d7..b085e30 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -11,20 +11,36 @@ import { ArrowRightIcon, Squares2X2Icon, BanknotesIcon, - ClipboardDocumentListIcon + ClipboardDocumentListIcon, + CommandLineIcon } from '@heroicons/react/24/outline' import { useMemo, useState, useEffect } from 'react' import { useRouter } from 'next/navigation' import { useAdminUsers } from '../hooks/useAdminUsers' +import useAuthStore from '../store/authStore' // env-based feature flags const DISPLAY_MATRIX = process.env.NEXT_PUBLIC_DISPLAY_MATRIX !== 'false' const DISPLAY_ABONEMENTS = process.env.NEXT_PUBLIC_DISPLAY_ABONEMMENTS !== 'false' const DISPLAY_NEWS = process.env.NEXT_PUBLIC_DISPLAY_NEWS !== 'false' +const DISPLAY_DEV_MANAGEMENT = process.env.NEXT_PUBLIC_DISPLAY_DEV_MANAGEMENT !== 'false' export default function AdminDashboardPage() { const router = useRouter() const { userStats, isAdmin } = useAdminUsers() + const user = useAuthStore(s => s.user) + const isAdminOrSuper = + !!user && + ( + (user as any)?.role === 'admin' || + (user as any)?.userType === 'admin' || + (user as any)?.isAdmin === true || + ((user as any)?.roles?.includes?.('admin')) || + (user as any)?.role === 'super_admin' || + (user as any)?.userType === 'super_admin' || + (user as any)?.isSuperAdmin === true || + ((user as any)?.roles?.includes?.('super_admin')) + ) const [isClient, setIsClient] = useState(false) const [isMobile, setIsMobile] = useState(false) @@ -318,6 +334,49 @@ export default function AdminDashboardPage() { }`} /> + + {/* Dev Management */} + diff --git a/src/app/admin/user-verify/page.tsx b/src/app/admin/user-verify/page.tsx index 87d5b72..431e02f 100644 --- a/src/app/admin/user-verify/page.tsx +++ b/src/app/admin/user-verify/page.tsx @@ -5,7 +5,6 @@ import PageLayout from '../../components/PageLayout' import UserDetailModal from '../../components/UserDetailModal' import { MagnifyingGlassIcon, - CheckIcon, ExclamationTriangleIcon, EyeIcon } from '@heroicons/react/24/outline' @@ -14,14 +13,14 @@ import { PendingUser } from '../../utils/api' type UserType = 'personal' | 'company' type UserRole = 'user' | 'admin' +type VerificationReadyFilter = 'all' | 'ready' | 'not_ready' +type StatusFilter = 'all' | 'pending' | 'verifying' | 'active' export default function AdminUserVerifyPage() { const { pendingUsers, loading, - error, - verifying, - verifyUser: handleVerifyUser, + error, isAdmin, fetchPendingUsers } = useAdminUsers() @@ -34,6 +33,8 @@ export default function AdminUserVerifyPage() { const [search, setSearch] = useState('') const [fType, setFType] = useState<'all' | UserType>('all') const [fRole, setFRole] = useState<'all' | UserRole>('all') + const [fReady, setFReady] = useState('all') + const [fStatus, setFStatus] = useState('all') const [perPage, setPerPage] = useState(10) const [page, setPage] = useState(1) @@ -44,10 +45,18 @@ export default function AdminUserVerifyPage() { const lastName = u.last_name || '' const companyName = u.company_name || '' const fullName = u.user_type === 'company' ? companyName : `${firstName} ${lastName}` + const isReadyToVerify = u.email_verified === 1 && u.profile_completed === 1 && + u.documents_uploaded === 1 && u.contract_signed === 1 return ( (fType === 'all' || u.user_type === fType) && (fRole === 'all' || u.role === fRole) && + (fStatus === 'all' || u.status === fStatus) && + ( + fReady === 'all' || + (fReady === 'ready' && isReadyToVerify) || + (fReady === 'not_ready' && !isReadyToVerify) + ) && ( !search.trim() || u.email.toLowerCase().includes(search.toLowerCase()) || @@ -55,7 +64,7 @@ export default function AdminUserVerifyPage() { ) ) }) - }, [pendingUsers, search, fType, fRole]) + }, [pendingUsers, search, fType, fRole, fReady, fStatus]) const totalPages = Math.max(1, Math.ceil(filtered.length / perPage)) const current = filtered.slice((page - 1) * perPage, page * perPage) @@ -178,9 +187,9 @@ export default function AdminUserVerifyPage() {

Search & Filter Pending Users

-
-
- +
+
+
+
+
+ + +
+
+ + +
+
+
-
- -
@@ -275,13 +304,8 @@ export default function AdminUserVerifyPage() { ? (u.company_name?.[0] || 'C').toUpperCase() : `${u.first_name?.[0] || 'U'}${u.last_name?.[0] || 'U'}`.toUpperCase() - const isVerifying = verifying.has(u.id.toString()) const createdDate = new Date(u.created_at).toLocaleDateString() - // Check if user has completed all verification steps - const isReadyToVerify = u.email_verified === 1 && u.profile_completed === 1 && - u.documents_uploaded === 1 && u.contract_signed === 1 - return ( @@ -314,30 +338,6 @@ export default function AdminUserVerifyPage() { View - {isReadyToVerify ? ( - - ) : ( - Incomplete steps - )}
@@ -387,6 +387,9 @@ export default function AdminUserVerifyPage() { setSelectedUserId(null) }} userId={selectedUserId} + onUserUpdated={() => { + fetchPendingUsers() + }} /> ) diff --git a/src/app/components/UserDetailModal.tsx b/src/app/components/UserDetailModal.tsx index 974a472..4bfe706 100644 --- a/src/app/components/UserDetailModal.tsx +++ b/src/app/components/UserDetailModal.tsx @@ -47,7 +47,6 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated const token = useAuthStore(state => state.accessToken) // Contract preview state (lazy-loaded, per contract type) - const [previewLoading, setPreviewLoading] = useState(false) const [activePreviewTab, setActivePreviewTab] = useState<'contract' | 'gdpr'>('contract') const [previewState, setPreviewState] = useState({ contract: { loading: false, html: null as string | null, error: null as string | null }, @@ -88,17 +87,6 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated } } - // Load both contract and GDPR previews when modal opens after user is known - useEffect(() => { - if (!isOpen || !userId || !token || !userDetails) return - setPreviewLoading(true) - Promise.all([ - loadContractPreview('contract'), - loadContractPreview('gdpr') - ]).finally(() => setPreviewLoading(false)) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isOpen, userId, token, userDetails]) - const handleStatusChange = async (newStatus: UserStatus) => { if (!userId || !token || newStatus === selectedStatus) return @@ -439,11 +427,11 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated