feat: add module filter to documents list and refactor version creation form UI

This commit is contained in:
Yashwin 2026-04-02 21:30:20 +05:30
parent dfe6d74993
commit 435375fc9f
6 changed files with 403 additions and 157 deletions

View File

@ -37,6 +37,8 @@ const stepSchema = z
requires_comment: z.boolean().default(false), requires_comment: z.boolean().default(false),
requires_attachment: z.boolean().default(false), requires_attachment: z.boolean().default(false),
sla_hours: z.number().optional().nullable(), sla_hours: z.number().optional().nullable(),
sla_warning_hours: z.number().optional().nullable(),
sla_escalation_hours: z.number().optional().nullable(),
}) })
.superRefine((data, ctx) => { .superRefine((data, ctx) => {
// Skip all assignee/action validation for terminal steps // Skip all assignee/action validation for terminal steps
@ -339,6 +341,8 @@ export const WorkflowDefinitionModal = ({
requires_comment: s.requires_comment || false, requires_comment: s.requires_comment || false,
requires_attachment: s.requires_attachment || false, requires_attachment: s.requires_attachment || false,
sla_hours: s.sla?.hours, sla_hours: s.sla?.hours,
sla_warning_hours: s.sla?.warning_hours,
sla_escalation_hours: s.sla?.escalation_hours,
})), })),
transitions: (definition.transitions || []).map((t) => ({ transitions: (definition.transitions || []).map((t) => ({
from_step_code: t.from_step_code, from_step_code: t.from_step_code,
@ -393,13 +397,43 @@ export const WorkflowDefinitionModal = ({
...data, ...data,
source_module: selectedModuleNames, source_module: selectedModuleNames,
tenantId: tenantId || undefined, 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) => { steps: data.steps?.map((step: any) => {
if (step.step_type === "terminal") { 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 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 = ({
</span> </span>
</label> </label>
</div> </div>
{watchedSteps[index]?.step_type !== "terminal" && (
<div className="md:col-span-3 grid grid-cols-1 md:grid-cols-3 gap-x-4 gap-y-3 mt-2 pt-4 border-t border-[rgba(0,0,0,0.05)]">
<FormField
label="SLA Hours"
type="number"
placeholder="e.g. 24"
{...register(`steps.${index}.sla_hours` as const, {
valueAsNumber: true,
})}
error={(errors.steps as any)?.[index]?.sla_hours?.message}
disabled={isEdit}
/>
<FormField
label="Warning Hours"
type="number"
placeholder="e.g. 4"
{...register(`steps.${index}.sla_warning_hours` as const, {
valueAsNumber: true,
})}
error={(errors.steps as any)?.[index]?.sla_warning_hours?.message}
disabled={isEdit}
/>
<FormField
label="Escalation Hours"
type="number"
placeholder="e.g. 48"
{...register(`steps.${index}.sla_escalation_hours` as const, {
valueAsNumber: true,
})}
error={(errors.steps as any)?.[index]?.sla_escalation_hours?.message}
disabled={isEdit}
/>
</div>
)}
</div> </div>
</div> </div>
))} ))}

View File

@ -233,12 +233,30 @@ export const WorkflowDefinitionViewModal = ({
)} )}
{/* SLA */} {/* SLA */}
{(step.sla?.hours != null ||
step.sla?.warning_hours != null ||
step.sla?.escalation_hours != null) && (
<div className="flex flex-wrap gap-2">
{step.sla?.hours != null && ( {step.sla?.hours != null && (
<div className="flex items-center gap-1 text-[11px] opacity-80"> <div className="flex items-center gap-1 text-[11px] opacity-80">
<Clock className="w-3 h-3" /> <Clock className="w-3 h-3" />
<span>SLA: {step.sla.hours}h</span> <span>SLA: {step.sla.hours}h</span>
</div> </div>
)} )}
{step.sla?.warning_hours != null && (
<div className="flex items-center gap-1 text-[11px] text-amber-600 font-medium">
<AlertCircle className="w-3 h-3" />
<span>Warn: {step.sla.warning_hours}h</span>
</div>
)}
{step.sla?.escalation_hours != null && (
<div className="flex items-center gap-1 text-[11px] text-red-600 font-medium">
<ArrowRight className="w-3 h-3" />
<span>Esc: {step.sla.escalation_hours}h</span>
</div>
)}
</div>
)}
{/* Requirements */} {/* Requirements */}
{step.requirements?.signature && ( {step.requirements?.signature && (

View File

@ -366,7 +366,7 @@ const CreateDocument = (): ReactElement => {
<div className="flex items-center gap-2 mb-3"> <div className="flex items-center gap-2 mb-3">
<Paperclip className="w-4 h-4 text-[#112868]" /> <Paperclip className="w-4 h-4 text-[#112868]" />
<h3 className="text-sm font-semibold text-[#0f1724]"> <h3 className="text-sm font-semibold text-[#0f1724]">
Attach File (Optional) Load Content From File (Optional)
</h3> </h3>
</div> </div>
<p className="text-xs text-[#6b7280] mb-3"> <p className="text-xs text-[#6b7280] mb-3">

View File

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

View File

@ -17,6 +17,7 @@ import {
import { workflowService } from "@/services/workflow-service"; import { workflowService } from "@/services/workflow-service";
import type { DocumentDetail, DocumentVersion } from "@/types/document"; import type { DocumentDetail, DocumentVersion } from "@/types/document";
import type { WorkflowInstance } from "@/types/workflow"; import type { WorkflowInstance } from "@/types/workflow";
import { cn } from "@/lib/utils";
import { showToast } from "@/utils/toast"; import { showToast } from "@/utils/toast";
import { Paperclip, Plus, User } from "lucide-react"; import { Paperclip, Plus, User } from "lucide-react";
@ -712,30 +713,43 @@ const ViewDocument = (): ReactElement => {
</button> </button>
</div> </div>
{showNewVersionForm && ( {showNewVersionForm && (
<div className="border border-[rgba(0,0,0,0.08)] rounded-lg p-4 bg-[#f8fafc]"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8 animate-in fade-in slide-in-from-top-4 duration-500">
<h4 className="text-base font-semibold text-[#0f1724] mb-3"> {/* Main Form Area */}
Create New Version <div className="lg:col-span-2 space-y-6">
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden">
<div className="p-6 border-b border-[rgba(0,0,0,0.08)] bg-gray-50/50">
<h4 className="text-base font-semibold text-[#0f1724]">
Update Document Content
</h4> </h4>
<p className="text-xs text-[#6b7280] mt-1">
{/* File Attachment Selection */} Modify the document content or load it from an existing file attachment.
<div className="mb-4 border border-[rgba(0,0,0,0.08)] rounded-lg p-3 bg-white">
<div className="flex items-center gap-2 mb-2">
<Paperclip className="w-4 h-4 text-[#112868]" />
<span className="text-sm font-medium text-[#0f1724]">
Load Content From File (Optional)
</span>
</div>
<p className="text-xs text-[#6b7280] mb-2">
Select a file to extract and auto-fill content below.
</p> </p>
</div>
<div className="p-6 space-y-6">
{/* File Attachment Selection */}
<div className="border border-[rgba(0,0,0,0.08)] rounded-lg p-4 bg-[#f8fafc]/50">
<div className="flex items-center gap-2 mb-3">
<div className="p-1.5 bg-[#112868]/10 rounded-md">
<Paperclip className="w-4 h-4 text-[#112868]" />
</div>
<div>
<span className="text-sm font-semibold text-[#0f1724]">
Load Content From File
</span>
<p className="text-xs text-[#6b7280]">
Extract text directly from an uploaded PDF or Word document.
</p>
</div>
</div>
<FormSelect <FormSelect
label="Select File" label=""
value={versionSelectedFileId} value={versionSelectedFileId}
onValueChange={(val) => onValueChange={(val) =>
void handleVersionFileSelect(val) void handleVersionFileSelect(val)
} }
options={[ options={[
{ value: "", label: "— None —" }, { value: "", label: "— No file selected —" },
...versionFiles.map((f) => ({ ...versionFiles.map((f) => ({
value: f.id, value: f.id,
label: `${f.original_name} (${f.file_size_formatted})`, label: `${f.original_name} (${f.file_size_formatted})`,
@ -744,31 +758,29 @@ const ViewDocument = (): ReactElement => {
placeholder={ placeholder={
isLoadingVersionFiles isLoadingVersionFiles
? "Loading files..." ? "Loading files..."
: "Select a file to attach" : "Search and select a file..."
} }
/> />
{versionSelectedFileId && ( {versionSelectedFileId && (
<div className="mt-2 grid grid-cols-1 md:grid-cols-2 gap-x-3 gap-y-0 text-xs text-[#6b7280]"> <div className="mt-3 grid grid-cols-2 gap-x-4 gap-y-2 p-3 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-[11px]">
<p> <div>
<span className="font-medium">File:</span>{" "} <span className="text-gray-400">File Name:</span>{" "}
{versionFileName} <span className="text-[#0f1724] font-medium">{versionFileName}</span>
</p> </div>
<p> <div>
<span className="font-medium">Type:</span>{" "} <span className="text-gray-400">File Size:</span>{" "}
{versionMimeType} <span className="text-[#0f1724] font-medium">
</p> {versionFileSize ? `${(versionFileSize / 1024 / 1024).toFixed(2)} MB` : "-"}
<p> </span>
<span className="font-medium">Size:</span>{" "} </div>
{versionFileSize <div>
? `${(versionFileSize / 1024 / 1024).toFixed(2)} MB` <span className="text-gray-400">MIME Type:</span>{" "}
: "-"} <span className="text-[#0f1724] font-medium">{versionMimeType}</span>
</p> </div>
<p> <div>
<span className="font-medium">Hash:</span>{" "} <span className="text-gray-400">Checksum:</span>{" "}
{versionFileHash <span className="text-[#0f1724] font-mono">{versionFileHash?.substring(0, 12)}...</span>
? versionFileHash.substring(0, 16) + "..." </div>
: "-"}
</p>
</div> </div>
)} )}
</div> </div>
@ -777,7 +789,7 @@ const ViewDocument = (): ReactElement => {
label="Document Content" label="Document Content"
value={newVersionContentHtml} value={newVersionContentHtml}
required required
minHeightClassName="h-[400px] overflow-y-auto" minHeightClassName="h-[500px] overflow-y-auto"
onChange={(html, text) => { onChange={(html, text) => {
setNewVersionContentHtml(html); setNewVersionContentHtml(html);
setNewVersionContent(text); setNewVersionContent(text);
@ -789,17 +801,17 @@ const ViewDocument = (): ReactElement => {
}} }}
error={versionErrors.content} error={versionErrors.content}
/> />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Version Reason and Increment Row */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 p-4 rounded-lg border border-[rgba(0,0,0,0.06)] bg-[#f8fafc]/30">
<div className="space-y-2">
<FormSelect <FormSelect
label="Change Reason" label="Change Reason"
required required
options={[ options={[
{ value: "minor_edit", label: "minor_edit" }, { value: "minor_edit", label: "minor_edit" },
{ value: "correction", label: "correction" }, { value: "correction", label: "correction" },
{ { value: "regulatory_update", label: "regulatory_update" },
value: "regulatory_update",
label: "regulatory_update",
},
{ value: "major_rewrite", label: "major_rewrite" }, { value: "major_rewrite", label: "major_rewrite" },
]} ]}
value={newVersionChangeReason} value={newVersionChangeReason}
@ -814,26 +826,59 @@ const ViewDocument = (): ReactElement => {
placeholder="Select change reason" placeholder="Select change reason"
error={versionErrors.change_reason} error={versionErrors.change_reason}
/> />
<div className="flex items-center gap-2 pt-8"> <p className="text-[10px] text-[#6b7280]">
<input Select a reason category such as minor_edit, correction, etc.
id="major-version" </p>
type="checkbox" </div>
checked={isMajorVersion}
onChange={(event) => <div className="space-y-2">
setIsMajorVersion(event.target.checked) <div className="flex items-center justify-between">
} <label className="text-[13px] font-semibold text-[#0f1724]">
className="w-4 h-4"
/>
<label
htmlFor="major-version"
className="text-sm text-[#0f1724]"
>
Major Version? Major Version?
</label> </label>
<button
type="button"
onClick={() => setIsMajorVersion(!isMajorVersion)}
className={cn(
"relative inline-flex h-5 w-9 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none",
isMajorVersion ? "bg-[#112868]" : "bg-gray-200"
)}
>
<span
className={cn(
"pointer-events-none inline-block h-4 w-4 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out",
isMajorVersion ? "translate-x-4" : "translate-x-0"
)}
/>
</button>
</div>
<div className="h-10 px-3 flex items-center justify-between bg-white border border-[rgba(0,0,0,0.08)] rounded-md shadow-sm">
<span className="text-[11px] font-medium text-[#475569]">
{isMajorVersion ? "Major" : "Minor"} Increment
</span>
<span className="text-[11px] font-bold text-[#0f1724]">
{`${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}`;
})()}`}
</span>
</div>
<p className="text-[10px] text-[#6b7280] leading-tight">
{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.`
}
</p>
</div> </div>
</div> </div>
<div className="mt-1">
<label className="text-[13px] font-medium text-[#0e1b2a] mb-2 block"> <div className="space-y-1.5">
<label className="text-[13px] font-semibold text-[#0f1724]">
Change Summary Change Summary
</label> </label>
<textarea <textarea
@ -842,24 +887,92 @@ const ViewDocument = (): ReactElement => {
onChange={(event) => onChange={(event) =>
setNewVersionChangeSummary(event.target.value) setNewVersionChangeSummary(event.target.value)
} }
placeholder="Provide a brief description of the changes..." placeholder="Provide a brief, professional summary of what has changed in this version..."
className="w-full px-3 py-2 border border-[rgba(0,0,0,0.08)] rounded-md text-sm" className="w-full px-3 py-2 border border-[rgba(0,0,0,0.08)] rounded-lg text-sm focus:ring-1 focus:ring-[#112868]/20 focus:outline-none transition-all"
/> />
</div> </div>
<div className="mt-4 flex justify-end gap-2"> </div>
<div className="p-6 py-4 border-t border-[rgba(0,0,0,0.08)] bg-gray-50/50 flex justify-end gap-3">
<SecondaryButton <SecondaryButton
onClick={() => setShowNewVersionForm(false)} onClick={() => setShowNewVersionForm(false)}
> >
Cancel Discard Draft
</SecondaryButton> </SecondaryButton>
<PrimaryButton <PrimaryButton
onClick={() => void handleCreateVersion()} onClick={() => void handleCreateVersion()}
disabled={isVersionSaving} disabled={isVersionSaving}
className="px-8"
> >
{isVersionSaving ? "Creating..." : "Create Version"} {isVersionSaving ? "Creating..." : "Save New Version"}
</PrimaryButton> </PrimaryButton>
</div> </div>
</div> </div>
</div>
{/* Right Sidebar */}
<div className="space-y-5">
{/* Version Context Card */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden">
<div className="p-5 border-b border-[rgba(0,0,0,0.08)]">
<h4 className="text-sm font-bold text-[#0f1724]">Version Context</h4>
<p className="text-[11px] text-[#6b7280] mt-0.5">Reference information for current revision.</p>
</div>
<div className="p-5 pt-4 space-y-4">
<div className="inline-flex items-center px-2.5 py-1 rounded-md bg-[#084cc8]/5 text-[#084cc8] text-[11px] font-bold border border-[#084cc8]/10 w-full justify-center">
Checked Out for Editing
</div>
<div className="space-y-3">
{[
{ label: "Document Number", value: document?.document_number },
{ label: "Current Version", value: `v${document?.current_version || "-"}` },
{ label: "Next Version", value: (() => {
if (!document?.current_version) return isMajorVersion ? "v1.0" : "v0.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 `v${major}.${minor}`;
})()
},
{ label: "Document Type", value: document?.document_type },
{ label: "Owner", value: document?.owner?.name || "-" },
{ label: "Department", value: document?.department || "-" },
].map((item, i) => (
<div key={i} className="flex items-center justify-between py-2 border-b border-gray-50 last:border-0">
<span className="text-[11px] text-[#6b7280] font-medium">{item.label}</span>
<span className="text-[11px] text-[#0f1724] font-bold">{item.value}</span>
</div>
))}
</div>
</div>
</div>
{/* Review Notes Card */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm p-5 space-y-4">
<div>
<h4 className="text-sm font-bold text-[#0f1724]">Review Notes</h4>
<p className="text-[11px] text-[#6b7280] mt-0.5 leading-relaxed">
Creating a new draft revision will automatically trigger the following system actions:
</p>
</div>
<div className="space-y-2">
{[
"Status resets to Draft",
"Workflow restarts",
"Version history retained"
].map((note, i) => (
<div key={i} className="flex items-center gap-2 px-3 py-2 bg-gray-50 rounded-lg border border-gray-100/50">
<div className="w-1.5 h-1.5 rounded-full bg-[#112868]/40" />
<span className="text-[11px] font-semibold text-[#475569]">{note}</span>
</div>
))}
</div>
</div>
</div>
</div>
)} )}
<div className="border border-[rgba(0,0,0,0.08)] rounded-lg overflow-hidden"> <div className="border border-[rgba(0,0,0,0.08)] rounded-lg overflow-hidden">
<DataTable <DataTable
@ -1202,7 +1315,7 @@ const ViewDocument = (): ReactElement => {
onClick={() => setSelectedWorkflowAction(action)} onClick={() => setSelectedWorkflowAction(action)}
className="px-4 py-2 bg-white border border-blue-200 rounded-md text-xs font-bold text-blue-700 hover:bg-blue-600 hover:text-white transition-all shadow-sm" className="px-4 py-2 bg-white border border-blue-200 rounded-md text-xs font-bold text-blue-700 hover:bg-blue-600 hover:text-white transition-all shadow-sm"
> >
{action.name} {action.action || action.name}
</button> </button>
))} ))}
</div> </div>
@ -1214,7 +1327,7 @@ const ViewDocument = (): ReactElement => {
<div className="p-4 border border-amber-200 bg-amber-50 rounded-lg animate-in fade-in slide-in-from-top-2"> <div className="p-4 border border-amber-200 bg-amber-50 rounded-lg animate-in fade-in slide-in-from-top-2">
<div className="flex justify-between items-start mb-3"> <div className="flex justify-between items-start mb-3">
<h4 className="text-xs font-bold text-amber-900 uppercase tracking-wider"> <h4 className="text-xs font-bold text-amber-900 uppercase tracking-wider">
Executing Action: {selectedWorkflowAction.name} Executing Action: {selectedWorkflowAction.action||selectedWorkflowAction.name}
</h4> </h4>
<button <button
onClick={() => { onClick={() => {
@ -1290,6 +1403,12 @@ const ViewDocument = (): ReactElement => {
> >
Action Action
</th> </th>
<th
scope="col"
className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider"
>
Due Date
</th>
<th <th
scope="col" scope="col"
className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider"
@ -1343,10 +1462,25 @@ const ViewDocument = (): ReactElement => {
</span> </span>
)} )}
</td> </td>
<td className="px-4 py-3 whitespace-nowrap text-[11px] text-gray-500 font-medium"> <td className="px-4 py-3 whitespace-nowrap">
<span
className={`text-[11px] font-semibold ${
task.status === "pending" &&
task.due_at &&
new Date(task.due_at) < new Date()
? "text-red-600"
: "text-gray-600"
}`}
>
{task.due_at ? formatDateTime(task.due_at) : "-"}
</span>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<span className="text-[11px] text-gray-400 italic">
{task.completed_at {task.completed_at
? formatDateTime(task.completed_at) ? formatDateTime(task.completed_at)
: "-"} : "-"}
</span>
</td> </td>
</tr> </tr>
))} ))}

View File

@ -13,6 +13,7 @@ export interface ListDocumentsParams {
category_id?: string; category_id?: string;
document_type?: string; document_type?: string;
owner_id?: string; owner_id?: string;
source_module_id?: string;
search?: string; search?: string;
limit?: number; limit?: number;
offset?: number; offset?: number;
@ -97,6 +98,8 @@ export const documentService = {
if (params.document_type) if (params.document_type)
queryParams.append("document_type", params.document_type); queryParams.append("document_type", params.document_type);
if (params.owner_id) queryParams.append("owner_id", params.owner_id); if (params.owner_id) queryParams.append("owner_id", params.owner_id);
if (params.source_module_id)
queryParams.append("source_module_id", params.source_module_id);
if (params.search) queryParams.append("search", params.search); if (params.search) queryParams.append("search", params.search);
if (params.limit !== undefined) if (params.limit !== undefined)
queryParams.append("limit", params.limit.toString()); queryParams.append("limit", params.limit.toString());