import { useEffect, useState, type ReactElement } from "react"; import { useNavigate } from "react-router-dom"; import { useForm, Controller, useFieldArray } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import * as z from "zod"; import { Layout } from "@/components/layout/Layout"; import { FormField, FormSelect, FormTextArea, PrimaryButton, RichTextEditor, FormTagInput, SecondaryButton, } from "@/components/shared"; import { documentService, type FileAttachmentItem, } from "@/services/document-service"; import type { DocumentCategory } from "@/types/document"; import { showToast } from "@/utils/toast"; import { ArrowLeft, FileText, Info, Paperclip, Plus, MessageSquare, } from "lucide-react"; import { moduleService } from "@/services/module-service"; import type { MyModule } from "@/types/module"; import { workflowService } from "@/services/workflow-service"; const documentSchema = z.object({ title: z.string().min(1, "Document title is required"), description: z.string().optional(), document_type: z.string().min(1, "Document type is required"), category_id: z.string().optional(), department: z.string().optional(), tags: z.array(z.string()), selectedModuleId: z.string().min(1, "Source module is required"), source_record_id: z.string().optional(), workflow_definition_id: z.string().optional(), content: z.string().optional(), contentHtml: z.string().optional(), sections: z .array( z.object({ title: z.string().min(1, "Section title is required"), content: z.string().optional(), contentHtml: z.string().min(1, "Section content is required"), is_mandatory: z.boolean(), }), ) .min(1, "At least one section is required for structured review"), }); type DocumentFormData = z.infer; const CreateDocument = (): ReactElement => { const navigate = useNavigate(); const [isSaving, setIsSaving] = useState(false); const [types, setTypes] = useState>([]); const [categories, setCategories] = useState([]); const [modules, setModules] = useState([]); const [workflowOptions, setWorkflowOptions] = useState< Array<{ value: string; label: string }> >([]); const { control, register, handleSubmit, setValue, watch, formState: { errors }, } = useForm({ resolver: zodResolver(documentSchema), defaultValues: { title: "", description: "", document_type: "", category_id: "", department: "", tags: [], selectedModuleId: "", source_record_id: "", workflow_definition_id: "", content: "", contentHtml: "", sections: [], }, }); const { fields, append, remove } = useFieldArray({ control, name: "sections", }); // File attachment fields const [files, setFiles] = useState([]); const [selectedFileId, setSelectedFileId] = useState(""); const [fileName, setFileName] = useState(""); const [filePath, setFilePath] = useState(""); const [fileSize, setFileSize] = useState(undefined); const [mimeType, setMimeType] = useState(""); const [fileHash, setFileHash] = useState(""); const [isLoadingFiles, setIsLoadingFiles] = useState(false); // Split-Pane view state const [showSourcePane, setShowSourcePane] = useState(false); const [sourceHtml, setSourceHtml] = useState(""); const [selectionMenu, setSelectionMenu] = useState<{ x: number; y: number; text: string; } | null>(null); const handleTextSelection = () => { const selection = window.getSelection(); if (!selection || selection.isCollapsed) { setSelectionMenu(null); return; } const text = selection.toString().trim(); if (text.length < 2) { setSelectionMenu(null); return; } const range = selection.getRangeAt(0); const rect = range.getBoundingClientRect(); setSelectionMenu({ x: rect.left + rect.width / 2, y: rect.top + window.scrollY, text: text, }); }; const addSelectedToNewSection = () => { if (!selectionMenu) return; append({ title: selectionMenu.text.substring(0, 30) + (selectionMenu.text.length > 30 ? "..." : ""), contentHtml: `

${selectionMenu.text}

`, content: selectionMenu.text, is_mandatory: true, }); setSelectionMenu(null); window.getSelection()?.removeAllRanges(); showToast.success("Added to new section"); }; const appendToLastSection = () => { if (!selectionMenu || fields.length === 0) return; const lastIndex = fields.length - 1; const currentHtml = watch(`sections.${lastIndex}.contentHtml`); const currentText = watch(`sections.${lastIndex}.content`); setValue( `sections.${lastIndex}.contentHtml`, `${currentHtml}

${selectionMenu.text}

`, ); setValue( `sections.${lastIndex}.content`, `${currentText}\n\n${selectionMenu.text}`, ); setSelectionMenu(null); window.getSelection()?.removeAllRanges(); showToast.success("Appended to section " + (lastIndex + 1)); }; useEffect(() => { const loadLookups = async (): Promise => { try { const [typesRes, categoriesRes, modulesRes] = await Promise.all([ documentService.getTypes(), documentService.getCategories(), moduleService.getMyModules(), ]); setTypes(typesRes.data || []); setCategories(categoriesRes.data || []); setModules(modulesRes.data || []); } catch (err: any) { showToast.error( err?.response?.data?.error?.message || "Failed to load document metadata", ); } }; void loadLookups(); void loadFiles(); }, []); const selectedModuleId = watch("selectedModuleId"); useEffect(() => { const loadWorkflows = async (): Promise => { if (!selectedModuleId) { setWorkflowOptions([]); setValue("workflow_definition_id", ""); return; } try { const workflowRes = await workflowService.listDefinitions({ entity_type: "document", status: "active", source_module_id: selectedModuleId, limit: 100, }); setWorkflowOptions( (workflowRes.data || []).map((d: any) => ({ value: d.id, label: `${d.name} (${d.code})`, })), ); // Clear selection if the current one isn't in the new list const currentWfId = watch("workflow_definition_id"); if ( currentWfId && !workflowRes.data.some((d: any) => d.id === currentWfId) ) { setValue("workflow_definition_id", ""); } } catch (err: any) { console.error("Failed to load workflows:", err); setWorkflowOptions([]); } }; void loadWorkflows(); }, [selectedModuleId, setValue]); const loadFiles = async (): Promise => { setIsLoadingFiles(true); try { const res = await documentService.listForDropdown(); setFiles(res.data || []); } catch (err: any) { showToast.error( err?.response?.data?.error?.message || "Failed to load files", ); } finally { setIsLoadingFiles(false); } }; const handleFileSelect = async (fileId: string): Promise => { setSelectedFileId(fileId); if (!fileId) { setFileName(""); setFilePath(""); setFileSize(undefined); setMimeType(""); setFileHash(""); setValue("contentHtml", ""); setValue("content", ""); return; } const selected = files.find((f) => f.id === fileId); if (!selected) return; setFileName(selected.original_name); setFilePath(selected.file_path); setFileSize(selected.file_size); setMimeType(selected.mime_type); setFileHash(selected.checksum); try { showToast.success( `Extracting content from "${selected.original_name}"...`, ); const res = await documentService.getFileContent(fileId); if (res.success && res.data) { setSourceHtml(res.data.html || ""); setShowSourcePane(true); // Automatically show the split pane // Reset sections and start with a single "Main Content" block while (fields.length > 0) remove(0); append({ title: "Main Content", contentHtml: res.data.html || "", content: res.data.text || "", is_mandatory: true, }); showToast.success( `Content loaded from "${selected.original_name}". Use the reference pane to build your structure.`, ); // Clear main content fields as we are now strictly using sections setValue("contentHtml", ""); setValue("content", ""); } else { showToast.error("Failed to extract file content"); } } catch (err: any) { const msg = err?.response?.data?.error?.message || "Failed to extract file content"; showToast.error(msg); // Fallback: create a section with error information while (fields.length > 0) remove(0); append({ title: "Sourced Content", contentHtml: `

Document sourced from file: ${selected.original_name}

`, content: `Document sourced from file: ${selected.original_name}`, is_mandatory: true, }); } }; const onFormSubmit = async (data: DocumentFormData): Promise => { try { setIsSaving(true); const response = await documentService.create({ title: data.title.trim(), description: data.description?.trim() || undefined, document_type: data.document_type, category_id: data.category_id || undefined, department: data.department?.trim() || undefined, tags: data.tags || [], content: data.content?.trim() || undefined, content_html: data.contentHtml?.trim() || undefined, file_name: fileName || undefined, file_path: filePath || undefined, file_size: fileSize, mime_type: mimeType || undefined, file_hash: fileHash || undefined, source_module: modules.find((m) => m.id === data.selectedModuleId)! .module_id, source_module_id: data.selectedModuleId, source_record_id: data.source_record_id?.trim() || undefined, workflow_definition_id: data.workflow_definition_id, sections: data.sections?.map((s, i) => ({ title: s.title, content: s.content, content_html: s.contentHtml, order_index: i, is_mandatory: s.is_mandatory, })), }); showToast.success("Document created successfully"); navigate(`/tenant/documents/${response.data.id}`); } catch (err: any) { showToast.error( err?.response?.data?.error?.message || "Failed to create document", ); } finally { setIsSaving(false); } }; return (
{/* LEFT PANE: Source Reference (Only visible if showSourcePane is true) */} {showSourcePane && (

Document Reference

{fileName}

{/* Floating Selection Menu */} {selectionMenu && (
)}

Interactive Mode: {" "} Select text from this pane and copy it directly into your sections on the right. This ensures accuracy and full traceability from the source document.

)} {/* RIGHT PANE: The Form */}

New Controlled Document

{watch("workflow_definition_id") ? ( Document will go directly to{" "} In Review {" "} status upon creation. ) : ( Document will be created in{" "} Draft status. )}

Classification

( ({ value: type.code, label: type.name, }))} placeholder="Select type" error={errors.document_type?.message} /> )} /> ( ({ value: category.id, label: `${category.name} (${category.code})`, }))} placeholder="Select category" error={errors.category_id?.message} /> )} /> ( )} /> ( { field.onChange(val); setValue("workflow_definition_id", ""); }} options={modules.map((m) => ({ value: m.id, label: m.name, }))} placeholder="Select originating module" error={errors.selectedModuleId?.message} /> )} /> ( )} />
{/* File Attachment Selection */}

Load Content From File (Optional)

{sourceHtml && !showSourcePane && ( )}

Select a previously uploaded file to automatically populate content and file metadata.

({ value: f.id, label: `${f.original_name} (${f.file_size_formatted})`, })), ]} placeholder={ isLoadingFiles ? "Loading files..." : "Select a file to attach" } /> {selectedFileId && (

File: {fileName}

Type: {mimeType}

Size:{" "} {fileSize ? `${(fileSize / 1024 / 1024).toFixed(2)} MB` : "-"}

Hash:{" "} {fileHash ? fileHash.substring(0, 16) + "..." : "-"}

)}
{/* Structured Sections Selection */}

Structured Sections

append({ title: "", content: "", contentHtml: "", is_mandatory: true, }) } className="h-8 text-[11px]" > + Add Section
{fields.length > 0 ? (
{fields.map((field, index) => (
( { editorField.onChange(html); setValue(`sections.${index}.content`, text); }} error={ errors.sections?.[index]?.contentHtml?.message } /> )} /> {/* Section Comments (Feedback for rework) */} {(field as any).comments && (field as any).comments.length > 0 && (
Unresolved Feedback ( {(field as any).comments.length})
{(field as any).comments.map((comment: any) => (
{comment.author_email} {comment.workflow_step}

{comment.comment_text}

))}
)}
))}
) : (

No structured sections added yet.

Use sections for granular review and collaboration.

)}

Structured Review Enabled

All documents must use sections to enable granular review, QA validation, and section-level rework tracking.

{isSaving ? "Creating..." : "Create Document"}
); }; export default CreateDocument;