import { useEffect, useState, type ReactElement } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Layout } from "@/components/layout/Layout"; import { DataTable, FormSelect, Modal, PrimaryButton, RichTextEditor, SecondaryButton, type Column, } from "@/components/shared"; import { documentService, type FileAttachmentItem, } from "@/services/document-service"; import { workflowService } from "@/services/workflow-service"; import type { DocumentDetail, DocumentVersion } from "@/types/document"; import type { WorkflowInstance } from "@/types/workflow"; import { cn } from "@/lib/utils"; import { showToast } from "@/utils/toast"; import { Paperclip, Plus, User } from "lucide-react"; const formatDateTime = (value?: string | null): string => { if (!value) return "-"; return new Date(value).toLocaleString("en-US", { month: "short", day: "numeric", year: "numeric", hour: "2-digit", minute: "2-digit", }); }; type DocumentAction = | "submit" | "approve" | "reject" | "effective" | "obsolete" | "checkout" | "checkin"; const ACTION_LABELS: Record = { submit: "Submit For Review", approve: "Approve", reject: "Reject", effective: "Make Effective", obsolete: "Make Obsolete", checkout: "Checkout", checkin: "Checkin", }; const ViewDocument = (): ReactElement => { const navigate = useNavigate(); const { id } = useParams(); const [document, setDocument] = useState(null); const [versions, setVersions] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState< "overview" | "version-history" | "workflow-history" >("overview"); const [workflowHistory, setWorkflowHistory] = useState([]); const [isHistoryLoading, setIsHistoryLoading] = useState(false); const [activeAction, setActiveAction] = useState(null); const [isActionLoading, setIsActionLoading] = useState(false); const [workflowDefinitionId, setWorkflowDefinitionId] = useState(""); const [workflowOptions, setWorkflowOptions] = useState< Array<{ value: string; label: string }> >([]); const [actionComment, setActionComment] = useState(""); const [effectiveDate, setEffectiveDate] = useState(""); const [signatureId, setSignatureId] = useState(""); const [showNewVersionForm, setShowNewVersionForm] = useState(false); const [newVersionContent, setNewVersionContent] = useState(""); const [newVersionContentHtml, setNewVersionContentHtml] = useState(""); const [newVersionChangeReason, setNewVersionChangeReason] = useState("minor_edit"); const [newVersionChangeSummary, setNewVersionChangeSummary] = useState(""); const [isMajorVersion, setIsMajorVersion] = useState(false); const [versionErrors, setVersionErrors] = useState>( {}, ); const [actionErrors, setActionErrors] = useState>({}); const [isVersionSaving, setIsVersionSaving] = useState(false); const [showWorkflowTracker, setShowWorkflowTracker] = useState(false); const [workflowInstance, setWorkflowInstance] = useState(null); const [isWorkflowLoading, setIsWorkflowLoading] = useState(false); // File attachment fields for new version const [versionFiles, setVersionFiles] = useState([]); const [versionSelectedFileId, setVersionSelectedFileId] = useState(""); const [versionFileName, setVersionFileName] = useState(""); const [transitionComment, setTransitionComment] = useState(""); const [selectedWorkflowAction, setSelectedWorkflowAction] = useState(null); const [isTransitioning, setIsTransitioning] = useState(false); const [versionFilePath, setVersionFilePath] = useState(""); const [versionFileSize, setVersionFileSize] = useState( undefined, ); const [versionMimeType, setVersionMimeType] = useState(""); const [versionFileHash, setVersionFileHash] = useState(""); const [isLoadingVersionFiles, setIsLoadingVersionFiles] = useState(false); useEffect(() => { if (!id) return; const loadDocument = async (): Promise => { try { setIsLoading(true); setError(null); const [documentRes, versionsRes] = await Promise.all([ documentService.getById(id), documentService.getVersions(id), ]); setDocument(documentRes.data); setVersions(versionsRes.data || []); } catch (err: any) { const message = err?.response?.data?.error?.message || "Failed to load document details"; setError(message); showToast.error(message); } finally { setIsLoading(false); } }; void loadDocument(); void loadVersionFiles(); }, [id]); useEffect(() => { if (activeTab === "workflow-history" && id) { void loadWorkflowHistory(); } }, [activeTab, id]); const loadWorkflowHistory = async (): Promise => { if (!document?.workflow_instance_id) { setWorkflowHistory([]); return; } try { setIsHistoryLoading(true); const res = await workflowService.getInstanceHistory( document.workflow_instance_id, ); const history = res.data || []; // Sort history descending by default const sortedHistory = [...history].sort( (a: any, b: any) => new Date(b.performed_at).getTime() - new Date(a.performed_at).getTime(), ); setWorkflowHistory(sortedHistory); } catch { setWorkflowHistory([]); showToast.error("Failed to load workflow history"); } finally { setIsHistoryLoading(false); } }; const loadVersionFiles = async (): Promise => { setIsLoadingVersionFiles(true); try { const res = await documentService.listFiles(); setVersionFiles(res.data || []); } catch { // silent } finally { setIsLoadingVersionFiles(false); } }; const handleVersionFileSelect = async (fileId: string): Promise => { setVersionSelectedFileId(fileId); if (!fileId) { setVersionFileName(""); setVersionFilePath(""); setVersionFileSize(undefined); setVersionMimeType(""); setVersionFileHash(""); return; } const selected = versionFiles.find((f) => f.id === fileId); if (!selected) return; setVersionFileName(selected.original_name); setVersionFilePath(selected.file_path); setVersionFileSize(selected.file_size); setVersionMimeType(selected.mime_type); setVersionFileHash(selected.checksum); try { showToast.success( `Extracting content from "${selected.original_name}"...`, ); const res = await documentService.getFileContent(fileId); if (res.success && res.data) { setNewVersionContentHtml(res.data.html || ""); setNewVersionContent(res.data.text || ""); showToast.success(`Content loaded from "${selected.original_name}"`); } 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); const html = `

Document sourced from file: ${selected.original_name}

`; setNewVersionContentHtml(html); setNewVersionContent( `Document sourced from file: ${selected.original_name}`, ); } }; const refreshData = async (): Promise => { if (!id) return; const [documentRes, versionsRes] = await Promise.all([ documentService.getById(id), documentService.getVersions(id), ]); setDocument(documentRes.data); setVersions(versionsRes.data || []); }; const openWorkflowTracker = async (instanceId?: string): Promise => { const targetId = instanceId || document?.workflow_instance_id; if (!targetId) { showToast.error("No workflow instance associated with this document"); return; } try { setIsWorkflowLoading(true); setShowWorkflowTracker(true); const res = await workflowService.getInstance(targetId); setWorkflowInstance(res.data); } catch (err: any) { const msg = err?.response?.data?.error?.message || "Failed to load workflow tracker"; showToast.error(msg); setShowWorkflowTracker(false); } finally { setIsWorkflowLoading(false); } }; const resetActionModal = (): void => { setActiveAction(null); setWorkflowDefinitionId(""); setActionComment(""); setEffectiveDate(""); setSignatureId(""); setWorkflowOptions([]); setActionErrors({}); }; const openActionModal = async (action: DocumentAction): Promise => { if (action === "checkin") { await handleAction(action); return; } setActiveAction(action); if (action === "submit") { try { const response = await workflowService.listDefinitions({ status: "active", entity_type: "document", source_module_id: document?.source_module_id || undefined, limit: 100, offset: 0, }); setWorkflowOptions( (response.data || []).map((definition) => ({ value: definition.id, label: `${definition.name} (${definition.code})`, })), ); } catch { setWorkflowOptions([]); showToast.error("Failed to load active workflow definitions"); } } }; const handleAction = async (action: DocumentAction): Promise => { if (!id) return; setActionErrors({}); const localErrors: Record = {}; if (action === "submit" && !workflowDefinitionId) { showToast.error("workflow_definition_id is required"); return; } if (action === "reject" && !actionComment.trim()) { localErrors.comment = "Reason is required for reject"; } if (action === "effective" && !effectiveDate) { localErrors.effective_date = "Effective date is required"; } if (action === "obsolete" && !actionComment.trim()) { localErrors.comment = "Reason is required to obsolete"; } if (action === "checkout" && !actionComment.trim()) { localErrors.comment = "Reason is required for checkout"; } if (Object.keys(localErrors).length > 0) { setActionErrors(localErrors); return; } try { setIsActionLoading(true); if (action === "submit") await documentService.submitForReview(id, workflowDefinitionId); if (action === "approve") await documentService.approve(id, actionComment); if (action === "reject") { await documentService.reject(id, actionComment.trim()); } if (action === "effective") { await documentService.makeEffective( id, effectiveDate, signatureId.trim(), ); } if (action === "obsolete") { await documentService.makeObsolete(id, actionComment.trim()); } if (action === "checkout") await documentService.checkout(id, actionComment.trim()); if (action === "checkin") await documentService.checkin(id); showToast.success("Document action completed"); await refreshData(); resetActionModal(); } catch (err: any) { showToast.error( err?.response?.data?.error?.message || "Failed to execute action", ); } finally { setIsActionLoading(false); } }; const handleCreateVersion = async (): Promise => { if (!id) return; // Clear previous errors setVersionErrors({}); const localErrors: Record = {}; if (!newVersionChangeReason) { localErrors.change_reason = "Change reason is required"; } if (!newVersionContent.trim()) { localErrors.content = "Document content is required"; } if (Object.keys(localErrors).length > 0) { setVersionErrors(localErrors); return; } try { setIsVersionSaving(true); await documentService.createVersion(id, { content: newVersionContent.trim(), content_html: newVersionContentHtml.trim() || undefined, change_reason: newVersionChangeReason, change_summary: newVersionChangeSummary.trim() || undefined, is_major_version: isMajorVersion, file_name: versionFileName || undefined, file_path: versionFilePath || undefined, file_size: versionFileSize, mime_type: versionMimeType || undefined, file_hash: versionFileHash || undefined, }); showToast.success("New version created successfully"); setShowNewVersionForm(false); setNewVersionChangeSummary(""); setIsMajorVersion(false); setVersionSelectedFileId(""); setVersionFileName(""); setVersionFilePath(""); setVersionFileSize(undefined); setVersionMimeType(""); setVersionFileHash(""); await refreshData(); } catch (err: any) { showToast.error( err?.response?.data?.error?.message || "Failed to create version", ); } finally { setIsVersionSaving(false); } }; const handleWorkflowTransition = async (): Promise => { if (!workflowInstance || !selectedWorkflowAction) return; const pendingTask = workflowInstance.tasks.find( (t) => t.status === "pending", ); if (!pendingTask) { showToast.error("No pending task found for this workflow instance"); return; } if (selectedWorkflowAction.requires_comment && !transitionComment.trim()) { showToast.error("Comments are required for this action"); return; } try { setIsTransitioning(true); await workflowService.transition(workflowInstance.id, { task_id: pendingTask.id, action: selectedWorkflowAction.action, comments: transitionComment.trim() || undefined, }); showToast.success(`Action "${selectedWorkflowAction.name}" completed`); // Refresh workflow instance data const res = await workflowService.getInstance(workflowInstance.id); setWorkflowInstance(res.data); // Reset transition state setSelectedWorkflowAction(null); setTransitionComment(""); // Also refresh document data as it might have changed status await refreshData(); } catch (err: any) { showToast.error( err?.response?.data?.error?.message || "Failed to complete workflow action", ); } finally { setIsTransitioning(false); } }; const versionColumns: Column[] = [ { key: "version_number", label: "Version" }, { key: "status", label: "Status" }, { key: "change_summary", label: "Change Summary", render: (version) => version.change_summary || "-", }, { key: "module_name", label: "Module", render: (version) => version.module_name || "Platform", }, { key: "created_by", label: "Author", render: (version) => version.created_by || "-", }, { key: "created_at", label: "Created At", render: (version) => formatDateTime(version.created_at), }, ]; return ( {document?.status === "draft" && (
)} {document?.status === "in_review" && ( <> )} {document?.status === "approved" && (
)} {document?.status === "effective" && ( )} ), }} >

Document Properties

Details and classification for{" "} {document?.document_number || "-"}

{document?.status || "draft"} {document?.document_type || "-"} v{document?.current_version || "-"} {document?.module_name && ( {document.module_name} )}
{isLoading ? (
Loading document...
) : error ? (
{error}
) : document ? ( <> {activeTab === "overview" && (
Document Number:

{document.document_number}

Title:

{document.title}

Category:

{document.category?.name || "-"}

Department:

{document.department || "-"}

Effective Date:

{formatDateTime(document.effective_date)}

Next Review Date:

{formatDateTime(document.next_review_date)}

Tags:

{document.tags && document.tags.length > 0 ? document.tags.join(", ") : "-"}

Document Content

{document.content_html ? (
) : (
{document.content || "-"}
)}
)} {activeTab === "version-history" && (
{showNewVersionForm && (
{/* Main Form Area */}

Update Document Content

Modify the document content or load it from an existing file attachment.

{/* File Attachment Selection */}
Load Content From File

Extract text directly from an uploaded PDF or Word document.

void handleVersionFileSelect(val) } options={[ { value: "", label: "— No file selected —" }, ...versionFiles.map((f) => ({ value: f.id, label: `${f.original_name} (${f.file_size_formatted})`, })), ]} placeholder={ isLoadingVersionFiles ? "Loading files..." : "Search and select a file..." } /> {versionSelectedFileId && (
File Name:{" "} {versionFileName}
File Size:{" "} {versionFileSize ? `${(versionFileSize / 1024 / 1024).toFixed(2)} MB` : "-"}
MIME Type:{" "} {versionMimeType}
Checksum:{" "} {versionFileHash?.substring(0, 12)}...
)}
{ setNewVersionContentHtml(html); setNewVersionContent(text); if (text.trim()) setVersionErrors((prev) => ({ ...prev, content: "", })); }} error={versionErrors.content} /> {/* Version Reason and Increment Row */}
{ setNewVersionChangeReason(val); if (val) setVersionErrors((prev) => ({ ...prev, change_reason: "", })); }} placeholder="Select change reason" error={versionErrors.change_reason} />

Select a reason category such as minor_edit, correction, etc.

{isMajorVersion ? "Major" : "Minor"} Increment {`${document?.current_version || "0.0"} to ${(() => { if (!document?.current_version) return isMajorVersion ? "1.0" : "0.1"; const parts = document.current_version.replace("v", "").split("."); let major = parseInt(parts[0]) || 0; let minor = parseInt(parts[1]) || 0; if (isMajorVersion) { major++; minor = 0; } else { minor++; } return `${major}.${minor}`; })()}`}

{isMajorVersion ? "Incrementing to a major version indicates significant changes or a full document overhaul." : `Turn on major versioning to increment to v${(parseInt((document?.current_version || "0.0").split(".")[0]) || 0) + 1}.0 instead of a minor revision.` }