feat: add module filter to documents list and refactor version creation form UI
This commit is contained in:
parent
dfe6d74993
commit
435375fc9f
@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -233,10 +233,28 @@ export const WorkflowDefinitionViewModal = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* SLA */}
|
{/* SLA */}
|
||||||
{step.sla?.hours != null && (
|
{(step.sla?.hours != null ||
|
||||||
<div className="flex items-center gap-1 text-[11px] opacity-80">
|
step.sla?.warning_hours != null ||
|
||||||
<Clock className="w-3 h-3" />
|
step.sla?.escalation_hours != null) && (
|
||||||
<span>SLA: {step.sla.hours}h</span>
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{step.sla?.hours != null && (
|
||||||
|
<div className="flex items-center gap-1 text-[11px] opacity-80">
|
||||||
|
<Clock className="w-3 h-3" />
|
||||||
|
<span>SLA: {step.sla.hours}h</span>
|
||||||
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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,152 +713,264 @@ 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">
|
||||||
</h4>
|
<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">
|
||||||
{/* File Attachment Selection */}
|
<h4 className="text-base font-semibold text-[#0f1724]">
|
||||||
<div className="mb-4 border border-[rgba(0,0,0,0.08)] rounded-lg p-3 bg-white">
|
Update Document Content
|
||||||
<div className="flex items-center gap-2 mb-2">
|
</h4>
|
||||||
<Paperclip className="w-4 h-4 text-[#112868]" />
|
<p className="text-xs text-[#6b7280] mt-1">
|
||||||
<span className="text-sm font-medium text-[#0f1724]">
|
Modify the document content or load it from an existing file attachment.
|
||||||
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>
|
|
||||||
<FormSelect
|
|
||||||
label="Select File"
|
|
||||||
value={versionSelectedFileId}
|
|
||||||
onValueChange={(val) =>
|
|
||||||
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 && (
|
|
||||||
<div className="mt-2 grid grid-cols-1 md:grid-cols-2 gap-x-3 gap-y-0 text-xs text-[#6b7280]">
|
|
||||||
<p>
|
|
||||||
<span className="font-medium">File:</span>{" "}
|
|
||||||
{versionFileName}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span className="font-medium">Type:</span>{" "}
|
|
||||||
{versionMimeType}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span className="font-medium">Size:</span>{" "}
|
|
||||||
{versionFileSize
|
|
||||||
? `${(versionFileSize / 1024 / 1024).toFixed(2)} MB`
|
|
||||||
: "-"}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
<span className="font-medium">Hash:</span>{" "}
|
|
||||||
{versionFileHash
|
|
||||||
? versionFileHash.substring(0, 16) + "..."
|
|
||||||
: "-"}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<RichTextEditor
|
<div className="p-6 space-y-6">
|
||||||
label="Document Content"
|
{/* File Attachment Selection */}
|
||||||
value={newVersionContentHtml}
|
<div className="border border-[rgba(0,0,0,0.08)] rounded-lg p-4 bg-[#f8fafc]/50">
|
||||||
required
|
<div className="flex items-center gap-2 mb-3">
|
||||||
minHeightClassName="h-[400px] overflow-y-auto"
|
<div className="p-1.5 bg-[#112868]/10 rounded-md">
|
||||||
onChange={(html, text) => {
|
<Paperclip className="w-4 h-4 text-[#112868]" />
|
||||||
setNewVersionContentHtml(html);
|
</div>
|
||||||
setNewVersionContent(text);
|
<div>
|
||||||
if (text.trim())
|
<span className="text-sm font-semibold text-[#0f1724]">
|
||||||
setVersionErrors((prev) => ({
|
Load Content From File
|
||||||
...prev,
|
</span>
|
||||||
content: "",
|
<p className="text-xs text-[#6b7280]">
|
||||||
}));
|
Extract text directly from an uploaded PDF or Word document.
|
||||||
}}
|
</p>
|
||||||
error={versionErrors.content}
|
</div>
|
||||||
/>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<FormSelect
|
||||||
<FormSelect
|
label=""
|
||||||
label="Change Reason"
|
value={versionSelectedFileId}
|
||||||
required
|
onValueChange={(val) =>
|
||||||
options={[
|
void handleVersionFileSelect(val)
|
||||||
{ value: "minor_edit", label: "minor_edit" },
|
}
|
||||||
{ value: "correction", label: "correction" },
|
options={[
|
||||||
{
|
{ value: "", label: "— No file selected —" },
|
||||||
value: "regulatory_update",
|
...versionFiles.map((f) => ({
|
||||||
label: "regulatory_update",
|
value: f.id,
|
||||||
},
|
label: `${f.original_name} (${f.file_size_formatted})`,
|
||||||
{ value: "major_rewrite", label: "major_rewrite" },
|
})),
|
||||||
]}
|
]}
|
||||||
value={newVersionChangeReason}
|
placeholder={
|
||||||
onValueChange={(val) => {
|
isLoadingVersionFiles
|
||||||
setNewVersionChangeReason(val);
|
? "Loading files..."
|
||||||
if (val)
|
: "Search and select a file..."
|
||||||
setVersionErrors((prev) => ({
|
}
|
||||||
...prev,
|
/>
|
||||||
change_reason: "",
|
{versionSelectedFileId && (
|
||||||
}));
|
<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]">
|
||||||
}}
|
<div>
|
||||||
placeholder="Select change reason"
|
<span className="text-gray-400">File Name:</span>{" "}
|
||||||
error={versionErrors.change_reason}
|
<span className="text-[#0f1724] font-medium">{versionFileName}</span>
|
||||||
/>
|
</div>
|
||||||
<div className="flex items-center gap-2 pt-8">
|
<div>
|
||||||
<input
|
<span className="text-gray-400">File Size:</span>{" "}
|
||||||
id="major-version"
|
<span className="text-[#0f1724] font-medium">
|
||||||
type="checkbox"
|
{versionFileSize ? `${(versionFileSize / 1024 / 1024).toFixed(2)} MB` : "-"}
|
||||||
checked={isMajorVersion}
|
</span>
|
||||||
onChange={(event) =>
|
</div>
|
||||||
setIsMajorVersion(event.target.checked)
|
<div>
|
||||||
}
|
<span className="text-gray-400">MIME Type:</span>{" "}
|
||||||
className="w-4 h-4"
|
<span className="text-[#0f1724] font-medium">{versionMimeType}</span>
|
||||||
/>
|
</div>
|
||||||
<label
|
<div>
|
||||||
htmlFor="major-version"
|
<span className="text-gray-400">Checksum:</span>{" "}
|
||||||
className="text-sm text-[#0f1724]"
|
<span className="text-[#0f1724] font-mono">{versionFileHash?.substring(0, 12)}...</span>
|
||||||
>
|
</div>
|
||||||
Major Version?
|
</div>
|
||||||
</label>
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RichTextEditor
|
||||||
|
label="Document Content"
|
||||||
|
value={newVersionContentHtml}
|
||||||
|
required
|
||||||
|
minHeightClassName="h-[500px] overflow-y-auto"
|
||||||
|
onChange={(html, text) => {
|
||||||
|
setNewVersionContentHtml(html);
|
||||||
|
setNewVersionContent(text);
|
||||||
|
if (text.trim())
|
||||||
|
setVersionErrors((prev) => ({
|
||||||
|
...prev,
|
||||||
|
content: "",
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
error={versionErrors.content}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 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
|
||||||
|
label="Change Reason"
|
||||||
|
required
|
||||||
|
options={[
|
||||||
|
{ value: "minor_edit", label: "minor_edit" },
|
||||||
|
{ value: "correction", label: "correction" },
|
||||||
|
{ value: "regulatory_update", label: "regulatory_update" },
|
||||||
|
{ value: "major_rewrite", label: "major_rewrite" },
|
||||||
|
]}
|
||||||
|
value={newVersionChangeReason}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setNewVersionChangeReason(val);
|
||||||
|
if (val)
|
||||||
|
setVersionErrors((prev) => ({
|
||||||
|
...prev,
|
||||||
|
change_reason: "",
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
placeholder="Select change reason"
|
||||||
|
error={versionErrors.change_reason}
|
||||||
|
/>
|
||||||
|
<p className="text-[10px] text-[#6b7280]">
|
||||||
|
Select a reason category such as minor_edit, correction, etc.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<label className="text-[13px] font-semibold text-[#0f1724]">
|
||||||
|
Major Version?
|
||||||
|
</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 className="space-y-1.5">
|
||||||
|
<label className="text-[13px] font-semibold text-[#0f1724]">
|
||||||
|
Change Summary
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={3}
|
||||||
|
value={newVersionChangeSummary}
|
||||||
|
onChange={(event) =>
|
||||||
|
setNewVersionChangeSummary(event.target.value)
|
||||||
|
}
|
||||||
|
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-lg text-sm focus:ring-1 focus:ring-[#112868]/20 focus:outline-none transition-all"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</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
|
||||||
|
onClick={() => setShowNewVersionForm(false)}
|
||||||
|
>
|
||||||
|
Discard Draft
|
||||||
|
</SecondaryButton>
|
||||||
|
<PrimaryButton
|
||||||
|
onClick={() => void handleCreateVersion()}
|
||||||
|
disabled={isVersionSaving}
|
||||||
|
className="px-8"
|
||||||
|
>
|
||||||
|
{isVersionSaving ? "Creating..." : "Save New Version"}
|
||||||
|
</PrimaryButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-1">
|
|
||||||
<label className="text-[13px] font-medium text-[#0e1b2a] mb-2 block">
|
{/* Right Sidebar */}
|
||||||
Change Summary
|
<div className="space-y-5">
|
||||||
</label>
|
{/* Version Context Card */}
|
||||||
<textarea
|
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden">
|
||||||
rows={3}
|
<div className="p-5 border-b border-[rgba(0,0,0,0.08)]">
|
||||||
value={newVersionChangeSummary}
|
<h4 className="text-sm font-bold text-[#0f1724]">Version Context</h4>
|
||||||
onChange={(event) =>
|
<p className="text-[11px] text-[#6b7280] mt-0.5">Reference information for current revision.</p>
|
||||||
setNewVersionChangeSummary(event.target.value)
|
</div>
|
||||||
}
|
<div className="p-5 pt-4 space-y-4">
|
||||||
placeholder="Provide a brief description of the changes..."
|
<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">
|
||||||
className="w-full px-3 py-2 border border-[rgba(0,0,0,0.08)] rounded-md text-sm"
|
Checked Out for Editing
|
||||||
/>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex justify-end gap-2">
|
<div className="space-y-3">
|
||||||
<SecondaryButton
|
{[
|
||||||
onClick={() => setShowNewVersionForm(false)}
|
{ label: "Document Number", value: document?.document_number },
|
||||||
>
|
{ label: "Current Version", value: `v${document?.current_version || "-"}` },
|
||||||
Cancel
|
{ label: "Next Version", value: (() => {
|
||||||
</SecondaryButton>
|
if (!document?.current_version) return isMajorVersion ? "v1.0" : "v0.1";
|
||||||
<PrimaryButton
|
const parts = document.current_version.replace("v", "").split(".");
|
||||||
onClick={() => void handleCreateVersion()}
|
let major = parseInt(parts[0]) || 0;
|
||||||
disabled={isVersionSaving}
|
let minor = parseInt(parts[1]) || 0;
|
||||||
>
|
if (isMajorVersion) { major++; minor = 0; } else { minor++; }
|
||||||
{isVersionSaving ? "Creating..." : "Create Version"}
|
return `v${major}.${minor}`;
|
||||||
</PrimaryButton>
|
})()
|
||||||
|
},
|
||||||
|
{ 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>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -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">
|
||||||
{task.completed_at
|
<span
|
||||||
? formatDateTime(task.completed_at)
|
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
|
||||||
|
? formatDateTime(task.completed_at)
|
||||||
|
: "-"}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user