'use client'; import { useForm, Resolver } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; import { FileUploadZone } from '@/components/custom/file-upload-zone'; import { useRouter } from 'next/navigation'; import { Loader2 } from 'lucide-react'; import { useCreateCorrespondence, useUpdateCorrespondence } from '@/hooks/use-correspondence'; import { Organization } from '@/types/organization'; import { useOrganizations, useProjects, useCorrespondenceTypes, useDisciplines, useContracts } from '@/hooks/use-master-data'; import { CreateCorrespondenceDto } from '@/types/dto/correspondence/create-correspondence.dto'; import { useState, useEffect } from 'react'; import { correspondenceService as _correspondenceService } from '@/lib/services/correspondence.service'; import { numberingApi } from '@/lib/api/numbering'; import { filesApi } from '@/lib/api/files'; // Updated Zod Schema with all required fields const correspondenceSchema = z.object({ projectId: z.string().min(1, 'Please select a Project'), contractId: z.string().min(1, 'Please select a Contract'), documentTypeId: z.number().min(1, 'Please select a Document Type'), disciplineId: z.number().optional(), subject: z.string().min(5, 'Subject must be at least 5 characters'), description: z.string().optional(), body: z.string().optional(), remarks: z.string().optional(), dueDate: z.string().optional(), // ISO Date string documentDate: z.string().optional(), issuedDate: z.string().optional(), receivedDate: z.string().optional(), fromOrganizationId: z.string().min(1, 'Please select From Organization'), toOrganizationId: z.string().min(1, 'Please select To Organization'), ccOrganizationIds: z.array(z.string()).optional(), // CC organizations support importance: z.enum(['NORMAL', 'HIGH', 'URGENT']), attachments: z.array(z.instanceof(File)).optional(), }); type FormData = z.infer; type ProjectOption = { publicId?: string; uuid?: string; id?: number; projectName: string; projectCode: string; }; type ContractOption = { publicId?: string; contractName?: string; contractCode?: string; }; type CorrespondenceTypeOption = { id: number; typeName: string; typeCode: string; }; interface DisciplineOption { id: number; disciplineCode: string; codeNameEn?: string; } interface InitialCorrespondenceData { projectId?: number | string; project?: { uuid?: string }; correspondenceTypeId?: number; disciplineId?: number; revisions?: Array<{ isCurrent?: boolean; subject?: string; title?: string; description?: string; body?: string; remarks?: string; dueDate?: string; documentDate?: string; issuedDate?: string; receivedDate?: string; details?: { importance: 'NORMAL' | 'HIGH' | 'URGENT' }; }>; originatorId?: number; recipients?: Array<{ recipientType: string; recipientOrganizationId: number; }>; correspondenceNumber?: string; } const extractArrayData = (value: unknown): T[] => { let current: unknown = value; for (let i = 0; i < 5; i += 1) { if (Array.isArray(current)) { return current as T[]; } if (!current || typeof current !== 'object' || !('data' in current)) { return []; } current = (current as { data?: unknown }).data; } return Array.isArray(current) ? (current as T[]) : []; }; export function CorrespondenceForm({ initialData, uuid }: { initialData?: InitialCorrespondenceData; uuid?: string }) { const router = useRouter(); const createMutation = useCreateCorrespondence(); const updateMutation = useUpdateCorrespondence(); // Fetch master data for dropdowns const { data: projectsData, isLoading: isLoadingProjects } = useProjects(); const { data: organizations, isLoading: isLoadingOrgs } = useOrganizations(); const { data: correspondenceTypesData, isLoading: isLoadingTypes } = useCorrespondenceTypes(); const projects = (projectsData as ProjectOption[]) ?? []; const organizationOptions = extractArrayData(organizations); const correspondenceTypes = extractArrayData(correspondenceTypesData); // Extract initial values if editing const currentRev = initialData?.revisions?.find((r) => r.isCurrent) || initialData?.revisions?.[0]; const defaultValues: Partial = { projectId: initialData?.project?.uuid || (initialData?.projectId ? String(initialData.projectId) : undefined), documentTypeId: initialData?.correspondenceTypeId || undefined, disciplineId: initialData?.disciplineId || undefined, subject: currentRev?.subject || currentRev?.title || '', description: currentRev?.description || '', body: currentRev?.body || '', remarks: currentRev?.remarks || '', dueDate: currentRev?.dueDate ? new Date(currentRev.dueDate).toISOString().split('T')[0] : undefined, documentDate: currentRev?.documentDate ? new Date(currentRev.documentDate).toISOString().split('T')[0] : undefined, issuedDate: currentRev?.issuedDate ? new Date(currentRev.issuedDate).toISOString().split('T')[0] : undefined, receivedDate: currentRev?.receivedDate ? new Date(currentRev.receivedDate).toISOString().split('T')[0] : undefined, fromOrganizationId: initialData?.originatorId ? String(initialData.originatorId) : undefined, // Map initial recipient (TO) - Simplified for now toOrganizationId: initialData?.recipients?.find((r) => r.recipientType === 'TO')?.recipientOrganizationId ? String(initialData.recipients.find((r) => r.recipientType === 'TO')?.recipientOrganizationId) : undefined, importance: currentRev?.details?.importance || 'NORMAL', } as Partial; const { register, handleSubmit, setValue, watch, formState: { errors }, } = useForm({ // @ts-ignore: Zod version mismatch resolver: zodResolver(correspondenceSchema) as unknown as Resolver, defaultValues: defaultValues as FormData, }); // Watch for controlled inputs const projectId = watch('projectId'); const contractId = watch('contractId'); const documentTypeId = watch('documentTypeId'); const disciplineId = watch('disciplineId'); const fromOrgId = watch('fromOrganizationId'); const toOrgId = watch('toOrganizationId'); // Fetch contracts based on selected project const { data: contractsData, isLoading: isLoadingContracts } = useContracts(projectId); const contracts = extractArrayData(contractsData); // Fetch disciplines based on selected contract const { data: disciplinesData, isLoading: isLoadingDisciplines } = useDisciplines(contractId); const disciplines = extractArrayData(disciplinesData); // Reset dependent fields when project changes useEffect(() => { if (projectId) { setValue('contractId', ''); setValue('disciplineId', undefined); } }, [projectId, setValue]); // Reset discipline when contract changes useEffect(() => { if (contractId) { setValue('disciplineId', undefined); } }, [contractId, setValue]); const [isUploading, setIsUploading] = useState(false); const onSubmit = async (data: FormData) => { // Build recipients array with TO and CC const recipients = [ { organizationId: data.toOrganizationId, type: 'TO' as const }, ...(data.ccOrganizationIds?.map(orgId => ({ organizationId: orgId, type: 'CC' as const })) || []) ]; // Phase 1: Upload attachments to temp storage let attachmentTempIds: string[] | undefined; const validFiles = (data.attachments || []).filter((f): f is File => f instanceof File && !('validationError' in f && (f as { validationError?: string }).validationError)); if (validFiles.length > 0) { setIsUploading(true); try { const uploaded = await filesApi.uploadMany(validFiles); attachmentTempIds = uploaded.map((u) => u.tempId); } catch (_err) { setIsUploading(false); return; } setIsUploading(false); } const payload: CreateCorrespondenceDto = { projectId: data.projectId, typeId: data.documentTypeId, disciplineId: data.disciplineId, subject: data.subject, description: data.description, body: data.body, remarks: data.remarks, dueDate: data.dueDate ? new Date(data.dueDate).toISOString() : undefined, documentDate: data.documentDate ? new Date(data.documentDate).toISOString() : undefined, issuedDate: data.issuedDate ? new Date(data.issuedDate).toISOString() : undefined, receivedDate: data.receivedDate ? new Date(data.receivedDate).toISOString() : undefined, originatorId: data.fromOrganizationId, attachmentTempIds, recipients, details: { importance: data.importance, }, }; if (uuid && initialData) { updateMutation.mutate( { uuid, data: payload }, { onSuccess: () => router.push(`/correspondences/${uuid}`) } ); } else { createMutation.mutate(payload, { onSuccess: () => router.push('/correspondences'), }); } }; const isPending = createMutation.isPending || updateMutation.isPending || isUploading; // -- Preview Logic -- const [preview, setPreview] = useState<{ number: string; isDefaultTemplate: boolean } | null>(null); useEffect(() => { if (!projectId || !documentTypeId || !fromOrgId || !toOrgId) { setPreview(null); return; } const fetchPreview = async () => { try { const res = await numberingApi.previewNumber({ projectId, correspondenceTypeId: documentTypeId, disciplineId, originatorOrganizationId: fromOrgId, recipientOrganizationId: toOrgId, }); setPreview({ number: res.previewNumber, isDefaultTemplate: res.isDefault }); } catch (_err) { setPreview(null); } }; const timer = setTimeout(fetchPreview, 500); return () => clearTimeout(timer); }, [projectId, documentTypeId, disciplineId, fromOrgId, toOrgId]); return (
{/* Existing Document Number (Read Only) */} {initialData?.correspondenceNumber && (
{preview && preview.number !== initialData.correspondenceNumber && ( Start Change Detected )}
)} {/* Preview Section */} {preview && (

{initialData?.correspondenceNumber ? 'New Document Number (Preview)' : 'Document Number Preview'} {preview.number !== initialData?.correspondenceNumber && initialData?.correspondenceNumber && ( Will Update )}

{preview.number} {preview.isDefaultTemplate && ( Default Template )}
{preview.number !== initialData?.correspondenceNumber && initialData?.correspondenceNumber && (

* The document number will be regenerated because critical fields were changed.

)}
)} {/* Document Metadata Section */}
{/* Project Dropdown */}
{errors.projectId &&

{errors.projectId.message}

}
{/* Contract Dropdown */}
{errors.contractId &&

{errors.contractId.message}

}
{/* Document Type Dropdown */}
{errors.documentTypeId &&

{errors.documentTypeId.message}

}
{/* Discipline Dropdown (Optional) */}
{/* Subject */}
{errors.subject &&

{errors.subject.message}

}
{/* Body */}