From 435375fc9f67f8af32f8b5d77c1cd18296f27f99 Mon Sep 17 00:00:00 2001 From: Yashwin Date: Thu, 2 Apr 2026 21:30:20 +0530 Subject: [PATCH] feat: add module filter to documents list and refactor version creation form UI --- .../shared/WorkflowDefinitionModal.tsx | 75 ++- .../shared/WorkflowDefinitionViewModal.tsx | 26 +- src/pages/tenant/CreateDocument.tsx | 2 +- src/pages/tenant/Documents.tsx | 26 +- src/pages/tenant/ViewDocument.tsx | 428 ++++++++++++------ src/services/document-service.ts | 3 + 6 files changed, 403 insertions(+), 157 deletions(-) diff --git a/src/components/shared/WorkflowDefinitionModal.tsx b/src/components/shared/WorkflowDefinitionModal.tsx index c8597d2..d928665 100644 --- a/src/components/shared/WorkflowDefinitionModal.tsx +++ b/src/components/shared/WorkflowDefinitionModal.tsx @@ -37,6 +37,8 @@ const stepSchema = z requires_comment: z.boolean().default(false), requires_attachment: z.boolean().default(false), sla_hours: z.number().optional().nullable(), + sla_warning_hours: z.number().optional().nullable(), + sla_escalation_hours: z.number().optional().nullable(), }) .superRefine((data, ctx) => { // Skip all assignee/action validation for terminal steps @@ -339,6 +341,8 @@ export const WorkflowDefinitionModal = ({ requires_comment: s.requires_comment || false, requires_attachment: s.requires_attachment || false, sla_hours: s.sla?.hours, + sla_warning_hours: s.sla?.warning_hours, + sla_escalation_hours: s.sla?.escalation_hours, })), transitions: (definition.transitions || []).map((t) => ({ from_step_code: t.from_step_code, @@ -393,13 +397,43 @@ export const WorkflowDefinitionModal = ({ ...data, source_module: selectedModuleNames, tenantId: tenantId || undefined, - // Strip terminal-step-only-hidden fields from the payload + // Strip fields based on step type and assignee type to prevent backend validation errors steps: data.steps?.map((step: any) => { if (step.step_type === "terminal") { - const { assignee_type, assignee_role, assignee_id, available_actions, ...rest } = step; + const { + assignee_type, + assignee_role, + assignee_id, + available_actions, + ...rest + } = step; return rest; } - return step; + + const cleanedStep = { ...step }; + + // Clean up assignee fields based on assignee_type + if (step.assignee_type === "originator") { + delete cleanedStep.assignee_role; + delete cleanedStep.assignee_id; + } else if (step.assignee_type === "role") { + delete cleanedStep.assignee_id; + } else if (step.assignee_type === "user") { + delete cleanedStep.assignee_role; + // Ensure ID is not an empty string which fails UUID validation + if (cleanedStep.assignee_id === "") { + delete cleanedStep.assignee_id; + } + } + + // Clean up SLA fields + ['sla_hours', 'sla_warning_hours', 'sla_escalation_hours'].forEach(field => { + if (cleanedStep[field] === 0 || cleanedStep[field] === null || cleanedStep[field] === "") { + delete cleanedStep[field]; + } + }); + + return cleanedStep; }), }; @@ -933,6 +967,41 @@ export const WorkflowDefinitionModal = ({ + + {watchedSteps[index]?.step_type !== "terminal" && ( +
+ + + +
+ )} ))} diff --git a/src/components/shared/WorkflowDefinitionViewModal.tsx b/src/components/shared/WorkflowDefinitionViewModal.tsx index ead2bc2..c10d43b 100644 --- a/src/components/shared/WorkflowDefinitionViewModal.tsx +++ b/src/components/shared/WorkflowDefinitionViewModal.tsx @@ -233,10 +233,28 @@ export const WorkflowDefinitionViewModal = ({ )} {/* SLA */} - {step.sla?.hours != null && ( -
- - SLA: {step.sla.hours}h + {(step.sla?.hours != null || + step.sla?.warning_hours != null || + step.sla?.escalation_hours != null) && ( +
+ {step.sla?.hours != null && ( +
+ + SLA: {step.sla.hours}h +
+ )} + {step.sla?.warning_hours != null && ( +
+ + Warn: {step.sla.warning_hours}h +
+ )} + {step.sla?.escalation_hours != null && ( +
+ + Esc: {step.sla.escalation_hours}h +
+ )}
)} diff --git a/src/pages/tenant/CreateDocument.tsx b/src/pages/tenant/CreateDocument.tsx index 62a72cd..f41bd4e 100644 --- a/src/pages/tenant/CreateDocument.tsx +++ b/src/pages/tenant/CreateDocument.tsx @@ -366,7 +366,7 @@ const CreateDocument = (): ReactElement => {

- Attach File (Optional) + Load Content From File (Optional)

diff --git a/src/pages/tenant/Documents.tsx b/src/pages/tenant/Documents.tsx index 1b8f183..4e2b932 100644 --- a/src/pages/tenant/Documents.tsx +++ b/src/pages/tenant/Documents.tsx @@ -9,7 +9,9 @@ import { type Column, } from "@/components/shared"; import { documentService } from "@/services/document-service"; +import { moduleService } from "@/services/module-service"; import type { DocumentCategory, DocumentSummary } from "@/types/document"; +import type { MyModule } from "@/types/module"; import { Plus, Search } from "lucide-react"; const formatDate = (value?: string | null): string => { @@ -39,6 +41,8 @@ const Documents = (): ReactElement => { const [statusFilter, setStatusFilter] = useState(null); const [categoryFilter, setCategoryFilter] = useState(null); const [typeFilter, setTypeFilter] = useState(null); + const [moduleFilter, setModuleFilter] = useState(null); + const [modules, setModules] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [limit, setLimit] = useState(10); const [total, setTotal] = useState(0); @@ -51,14 +55,16 @@ const Documents = (): ReactElement => { useEffect(() => { const loadDropdownData = async (): Promise => { try { - const [categoriesRes, statusesRes, typesRes] = await Promise.all([ + const [categoriesRes, statusesRes, typesRes, modulesRes] = await Promise.all([ documentService.getCategories(), documentService.getStatuses(), documentService.getTypes(), + moduleService.getMyModules(), ]); setCategories(categoriesRes.data || []); setStatuses(statusesRes.data || []); setTypes(typesRes.data || []); + setModules(modulesRes.data || []); } catch { // Keep page usable even if some filter metadata endpoints fail. } @@ -76,6 +82,7 @@ const Documents = (): ReactElement => { status: statusFilter || undefined, category_id: categoryFilter || undefined, document_type: typeFilter || undefined, + source_module_id: moduleFilter || undefined, search: search.trim() || undefined, limit, offset, @@ -92,7 +99,7 @@ const Documents = (): ReactElement => { }; void loadDocuments(); - }, [statusFilter, categoryFilter, typeFilter, search, limit, offset]); + }, [statusFilter, categoryFilter, typeFilter, moduleFilter, search, limit, offset]); const columns: Column[] = useMemo( () => [ @@ -266,6 +273,20 @@ const Documents = (): ReactElement => { placeholder="All" /> + ({ + value: module.id, + label: module.name, + }))} + value={moduleFilter} + onChange={(value) => { + setModuleFilter(value as string | null); + setCurrentPage(1); + }} + placeholder="All Modules" + /> + {/* { setStatusFilter(null); setCategoryFilter(null); setTypeFilter(null); + setModuleFilter(null); setCurrentPage(1); }} className="text-sm font-medium text-[#6b7280] hover:text-[#0f1724] transition-colors" diff --git a/src/pages/tenant/ViewDocument.tsx b/src/pages/tenant/ViewDocument.tsx index 0561683..1494470 100644 --- a/src/pages/tenant/ViewDocument.tsx +++ b/src/pages/tenant/ViewDocument.tsx @@ -17,6 +17,7 @@ import { 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"; @@ -712,152 +713,264 @@ const ViewDocument = (): ReactElement => {

{showNewVersionForm && ( -
-

- Create New Version -

- - {/* File Attachment Selection */} -
-
- - - Load Content From File (Optional) - -
-

- Select a file to extract and auto-fill content below. -

- - void handleVersionFileSelect(val) - } - options={[ - { value: "", label: "— None —" }, - ...versionFiles.map((f) => ({ - value: f.id, - label: `${f.original_name} (${f.file_size_formatted})`, - })), - ]} - placeholder={ - isLoadingVersionFiles - ? "Loading files..." - : "Select a file to attach" - } - /> - {versionSelectedFileId && ( -
-

- File:{" "} - {versionFileName} -

-

- Type:{" "} - {versionMimeType} -

-

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

-

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

+ {/* Main Form Area */} +
+
+
+

+ Update Document Content +

+

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

- )} -
- { - setNewVersionContentHtml(html); - setNewVersionContent(text); - if (text.trim()) - setVersionErrors((prev) => ({ - ...prev, - content: "", - })); - }} - error={versionErrors.content} - /> -
- { - setNewVersionChangeReason(val); - if (val) - setVersionErrors((prev) => ({ - ...prev, - change_reason: "", - })); - }} - placeholder="Select change reason" - error={versionErrors.change_reason} - /> -
- - setIsMajorVersion(event.target.checked) - } - className="w-4 h-4" - /> - +
+ {/* 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.` + } +

+
+
+ +
+ +