diff --git a/src/app/components/UserDetailModal.tsx b/src/app/components/UserDetailModal.tsx index f206439..46439e3 100644 --- a/src/app/components/UserDetailModal.tsx +++ b/src/app/components/UserDetailModal.tsx @@ -30,6 +30,13 @@ interface UserDetailModalProps { type UserStatus = 'inactive' | 'pending' | 'active' | 'suspended' | 'archived' +type ContractFileItem = { + key: string + filename: string + documentId?: number | null + contract_type?: 'contract' | 'gdpr' | string | null +} + const STATUS_OPTIONS: { value: UserStatus; label: string; color: string }[] = [ { value: 'pending', label: 'Pending', color: 'amber' }, { value: 'active', label: 'Active', color: 'green' }, @@ -53,6 +60,14 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated gdpr: { loading: false, html: null as string | null, error: null as string | null, warning: null as string | null }, }) + const [contractFiles, setContractFiles] = useState<{ contract: ContractFileItem[]; gdpr: ContractFileItem[] }>({ + contract: [], + gdpr: [] + }) + const [docsLoading, setDocsLoading] = useState(false) + const [moveLoading, setMoveLoading] = useState>({}) + const [selectedFile, setSelectedFile] = useState<{ contract?: string; gdpr?: string }>({}) + const missingIdOrContract = !!userDetails?.userStatus && ( userDetails.userStatus.documents_uploaded !== 1 || userDetails.userStatus.contract_signed !== 1 @@ -71,8 +86,14 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated useEffect(() => { if (isOpen && userId && token) { fetchUserDetails() + loadContractFiles() } }, [isOpen, userId, token]) + useEffect(() => { + if (!isOpen || !userId || !token || !userDetails) return + loadContractFiles() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userDetails]) useEffect(() => { if (!isOpen) return @@ -81,6 +102,8 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated contract: { loading: false, html: null, error: null, warning: null }, gdpr: { loading: false, html: null, error: null, warning: null } }) + setContractFiles({ contract: [], gdpr: [] }) + setSelectedFile({}) }, [isOpen, userId]) useEffect(() => { @@ -164,14 +187,14 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated } } - const loadContractPreview = async (contractType: 'contract' | 'gdpr') => { + const loadContractPreview = async (contractType: 'contract' | 'gdpr', documentId?: number, objectKey?: string) => { if (!userId || !token || !userDetails) return setPreviewState((prev) => ({ ...prev, [contractType]: { ...prev[contractType], loading: true, error: null, warning: null } })) try { - const result = await AdminAPI.getContractPreviewHtml(token, String(userId), userDetails.user.user_type, contractType) + const result = await AdminAPI.getContractPreviewHtml(token, String(userId), userDetails.user.user_type, contractType, documentId, objectKey) setPreviewState((prev) => ({ ...prev, [contractType]: { loading: false, html: result.html, error: null, warning: result.warning || null } @@ -185,6 +208,44 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated } } + const loadContractFiles = async () => { + if (!userId || !token) return + setDocsLoading(true) + try { + const result = await AdminAPI.listContractFiles(token, String(userId), userDetails?.user?.user_type) + const contract = Array.isArray(result?.contract) ? result.contract : [] + const gdpr = Array.isArray(result?.gdpr) ? result.gdpr : [] + setContractFiles({ contract, gdpr }) + setSelectedFile((prev) => ({ + contract: prev.contract || contract[0]?.key || undefined, + gdpr: prev.gdpr || gdpr[0]?.key || undefined + })) + } catch (e) { + console.error('UserDetailModal.loadContractFiles error:', e) + setContractFiles({ contract: [], gdpr: [] }) + } finally { + setDocsLoading(false) + } + } + + const moveContractDoc = async (documentId: number | undefined, targetType: 'contract' | 'gdpr', filename?: string | null, objectKey?: string) => { + if (!userId || !token) return + const label = targetType === 'gdpr' ? 'GDPR' : 'Contract' + const name = filename ? `\n\nFile: ${filename}` : '' + const ok = window.confirm(`Move this document to ${label}?${name}`) + if (!ok) return + const loadingKey = objectKey || String(documentId || '') + setMoveLoading((prev) => ({ ...prev, [loadingKey]: true })) + try { + await AdminAPI.moveContractDocument(token, String(userId), documentId, targetType, objectKey) + await loadContractFiles() + } catch (e) { + console.error('UserDetailModal.moveContractDoc error:', e) + } finally { + setMoveLoading((prev) => ({ ...prev, [loadingKey]: false })) + } + } + const formatDate = (dateString: string | undefined | null) => { if (!dateString) return 'N/A' return new Date(dateString).toLocaleDateString('en-US', { @@ -194,14 +255,6 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated }) } - const formatFileSize = (bytes: number | undefined) => { - if (!bytes) return '0 B' - const k = 1024 - const sizes = ['B', 'KB', 'MB', 'GB'] - const i = Math.floor(Math.log(bytes) / Math.log(k)) - return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i] - } - const getStatusColor = (status: UserStatus) => { const option = STATUS_OPTIONS.find(opt => opt.value === status) return option?.color || 'gray' @@ -469,7 +522,12 @@ export default function UserDetailModal({ isOpen, onClose, userId, onUserUpdated
+ {(() => { + const files = contractFiles[activePreviewTab] || [] + const selectedKey = selectedFile[activePreviewTab] || files[0]?.key + const selectedItem = files.find((f) => f.key === selectedKey) || files[0] + const moveTarget = activePreviewTab === 'contract' ? 'gdpr' : 'contract' + const isMoving = selectedItem?.key ? !!moveLoading[selectedItem.key] : false + return ( +
+
+
Files in {activePreviewTab.toUpperCase()}
+ +
+ + {docsLoading && ( +
Loading files…
+ )} + + {!docsLoading && files.length === 0 && ( +
No files found in this folder.
+ )} + + {!docsLoading && files.length > 0 && ( + <> + {files.length > 1 && ( +
+ {files.map((f) => ( + + ))} +
+ )} + + {selectedItem && ( +
+
Selected: {selectedItem.filename}
+ {files.length >= 1 && ( + + )} +
+ )} + + )} +
+ ) + })()} {previewState[activePreviewTab].warning && (
{previewState[activePreviewTab].warning} diff --git a/src/app/utils/api.ts b/src/app/utils/api.ts index 604f250..b4da925 100644 --- a/src/app/utils/api.ts +++ b/src/app/utils/api.ts @@ -56,6 +56,9 @@ export const API_ENDPOINTS = { ADMIN_COFFEE_DELETE: '/api/admin/coffee/:id', // Contract preview (admin) – matches backend route ADMIN_CONTRACT_PREVIEW: '/api/admin/contracts/:id/preview', + ADMIN_CONTRACT_DOCS: '/api/admin/contracts/:id/documents', + ADMIN_CONTRACT_MOVE: '/api/admin/contracts/:id/move', + ADMIN_CONTRACT_FILES: '/api/admin/contracts/:id/files', } // API Helper Functions @@ -480,11 +483,13 @@ export class AdminAPI { return true } - static async getContractPreviewHtml(token: string, userId: string, userType?: 'personal' | 'company', contractType?: 'contract' | 'gdpr') { + static async getContractPreviewHtml(token: string, userId: string, userType?: 'personal' | 'company', contractType?: 'contract' | 'gdpr', documentId?: number, objectKey?: string) { let endpoint = API_ENDPOINTS.ADMIN_CONTRACT_PREVIEW.replace(':id', userId) const qs = new URLSearchParams() if (userType) qs.set('userType', userType) if (contractType) qs.set('contract_type', contractType) + if (documentId) qs.set('documentId', String(documentId)) + if (objectKey) qs.set('objectKey', objectKey) const qsStr = qs.toString() if (qsStr) endpoint += `?${qsStr}` const response = await ApiClient.get(endpoint, token) @@ -500,6 +505,44 @@ export class AdminAPI { const html = await response.text() return { html, warning: warningHeader } } + + static async listContractDocuments(token: string, userId: string, userType?: 'personal' | 'company') { + let endpoint = API_ENDPOINTS.ADMIN_CONTRACT_DOCS.replace(':id', userId) + const qs = new URLSearchParams() + if (userType) qs.set('userType', userType) + const qsStr = qs.toString() + if (qsStr) endpoint += `?${qsStr}` + const response = await ApiClient.get(endpoint, token) + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Failed to load contract documents' })) + throw new Error(error.message || 'Failed to load contract documents') + } + return response.json() + } + + static async listContractFiles(token: string, userId: string, userType?: 'personal' | 'company') { + let endpoint = API_ENDPOINTS.ADMIN_CONTRACT_FILES.replace(':id', userId) + const qs = new URLSearchParams() + if (userType) qs.set('userType', userType) + const qsStr = qs.toString() + if (qsStr) endpoint += `?${qsStr}` + const response = await ApiClient.get(endpoint, token) + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Failed to load contract files' })) + throw new Error(error.message || 'Failed to load contract files') + } + return response.json() + } + + static async moveContractDocument(token: string, userId: string, documentId: number | undefined, targetType: 'contract' | 'gdpr', objectKey?: string) { + const endpoint = API_ENDPOINTS.ADMIN_CONTRACT_MOVE.replace(':id', userId) + const response = await ApiClient.post(endpoint, { documentId, targetType, objectKey }, token) + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Failed to move contract document' })) + throw new Error(error.message || 'Failed to move contract document') + } + return response.json() + } } // Response Types