feat: Add module association to documents and integrate workflow history and tracking.
This commit is contained in:
parent
254bc9f40e
commit
e4aede83d9
@ -6,6 +6,8 @@ import { documentService, type FileAttachmentItem } from "@/services/document-se
|
|||||||
import type { DocumentCategory } from "@/types/document";
|
import type { DocumentCategory } from "@/types/document";
|
||||||
import { showToast } from "@/utils/toast";
|
import { showToast } from "@/utils/toast";
|
||||||
import { ArrowLeft, FileText, Info, Paperclip } from "lucide-react";
|
import { ArrowLeft, FileText, Info, Paperclip } from "lucide-react";
|
||||||
|
import { moduleService } from "@/services/module-service";
|
||||||
|
import type { MyModule } from "@/types/module";
|
||||||
|
|
||||||
const CreateDocument = (): ReactElement => {
|
const CreateDocument = (): ReactElement => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -21,6 +23,8 @@ const CreateDocument = (): ReactElement => {
|
|||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [types, setTypes] = useState<Array<{ code: string; name: string }>>([]);
|
const [types, setTypes] = useState<Array<{ code: string; name: string }>>([]);
|
||||||
const [categories, setCategories] = useState<DocumentCategory[]>([]);
|
const [categories, setCategories] = useState<DocumentCategory[]>([]);
|
||||||
|
const [modules, setModules] = useState<MyModule[]>([]);
|
||||||
|
const [selectedModuleId, setSelectedModuleId] = useState("");
|
||||||
|
|
||||||
// File attachment fields
|
// File attachment fields
|
||||||
const [files, setFiles] = useState<FileAttachmentItem[]>([]);
|
const [files, setFiles] = useState<FileAttachmentItem[]>([]);
|
||||||
@ -35,12 +39,14 @@ const CreateDocument = (): ReactElement => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadLookups = async (): Promise<void> => {
|
const loadLookups = async (): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const [typesRes, categoriesRes] = await Promise.all([
|
const [typesRes, categoriesRes, modulesRes] = await Promise.all([
|
||||||
documentService.getTypes(),
|
documentService.getTypes(),
|
||||||
documentService.getCategories(),
|
documentService.getCategories(),
|
||||||
|
moduleService.getMyModules(),
|
||||||
]);
|
]);
|
||||||
setTypes(typesRes.data || []);
|
setTypes(typesRes.data || []);
|
||||||
setCategories(categoriesRes.data || []);
|
setCategories(categoriesRes.data || []);
|
||||||
|
setModules(modulesRes.data || []);
|
||||||
} catch {
|
} catch {
|
||||||
showToast.error("Failed to load document metadata");
|
showToast.error("Failed to load document metadata");
|
||||||
}
|
}
|
||||||
@ -133,6 +139,8 @@ const CreateDocument = (): ReactElement => {
|
|||||||
file_size: fileSize,
|
file_size: fileSize,
|
||||||
mime_type: mimeType || undefined,
|
mime_type: mimeType || undefined,
|
||||||
file_hash: fileHash || undefined,
|
file_hash: fileHash || undefined,
|
||||||
|
source_module: selectedModuleId ? modules.find(m => m.id === selectedModuleId)?.module_id : undefined,
|
||||||
|
source_module_id: selectedModuleId || undefined,
|
||||||
});
|
});
|
||||||
showToast.success("Document created successfully");
|
showToast.success("Document created successfully");
|
||||||
navigate(`/tenant/documents/${response.data.id}`);
|
navigate(`/tenant/documents/${response.data.id}`);
|
||||||
@ -248,6 +256,19 @@ const CreateDocument = (): ReactElement => {
|
|||||||
onChange={(e) => setTags(e.target.value)}
|
onChange={(e) => setTags(e.target.value)}
|
||||||
placeholder="Comma separated tags (e.g. quality, sop)"
|
placeholder="Comma separated tags (e.g. quality, sop)"
|
||||||
/>
|
/>
|
||||||
|
<FormSelect
|
||||||
|
label="Source Module"
|
||||||
|
value={selectedModuleId}
|
||||||
|
onValueChange={setSelectedModuleId}
|
||||||
|
options={[
|
||||||
|
{ value: "", label: "Platform (Default)" },
|
||||||
|
...modules.map((m) => ({
|
||||||
|
value: m.id,
|
||||||
|
label: m.name,
|
||||||
|
})),
|
||||||
|
]}
|
||||||
|
placeholder="Specify originating module (Optional)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -135,6 +135,13 @@ const Documents = (): ReactElement => {
|
|||||||
</span>
|
</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "module_name",
|
||||||
|
label: "Module",
|
||||||
|
render: (doc) => (
|
||||||
|
<span className="text-[#0f1724]">{doc.module_name || "Platform"}</span>
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "current_version",
|
key: "current_version",
|
||||||
label: "Version",
|
label: "Version",
|
||||||
|
|||||||
@ -13,8 +13,9 @@ import {
|
|||||||
import { documentService, type FileAttachmentItem } from "@/services/document-service";
|
import { documentService, type FileAttachmentItem } from "@/services/document-service";
|
||||||
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 { showToast } from "@/utils/toast";
|
import { showToast } from "@/utils/toast";
|
||||||
import { Paperclip, Plus } from "lucide-react";
|
import { Paperclip, Plus, User } from "lucide-react";
|
||||||
|
|
||||||
const formatDateTime = (value?: string | null): string => {
|
const formatDateTime = (value?: string | null): string => {
|
||||||
if (!value) return "-";
|
if (!value) return "-";
|
||||||
@ -53,9 +54,11 @@ const ViewDocument = (): ReactElement => {
|
|||||||
const [versions, setVersions] = useState<DocumentVersion[]>([]);
|
const [versions, setVersions] = useState<DocumentVersion[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [activeTab, setActiveTab] = useState<"overview" | "version-history">(
|
const [activeTab, setActiveTab] = useState<"overview" | "version-history" | "workflow-history">(
|
||||||
"overview",
|
"overview",
|
||||||
);
|
);
|
||||||
|
const [workflowInstances, setWorkflowInstances] = useState<any[]>([]);
|
||||||
|
const [isHistoryLoading, setIsHistoryLoading] = useState(false);
|
||||||
|
|
||||||
const [activeAction, setActiveAction] = useState<DocumentAction | null>(null);
|
const [activeAction, setActiveAction] = useState<DocumentAction | null>(null);
|
||||||
const [isActionLoading, setIsActionLoading] = useState(false);
|
const [isActionLoading, setIsActionLoading] = useState(false);
|
||||||
@ -73,6 +76,9 @@ const ViewDocument = (): ReactElement => {
|
|||||||
const [newVersionChangeSummary, setNewVersionChangeSummary] = useState("");
|
const [newVersionChangeSummary, setNewVersionChangeSummary] = useState("");
|
||||||
const [isMajorVersion, setIsMajorVersion] = useState(false);
|
const [isMajorVersion, setIsMajorVersion] = useState(false);
|
||||||
const [isVersionSaving, setIsVersionSaving] = useState(false);
|
const [isVersionSaving, setIsVersionSaving] = useState(false);
|
||||||
|
const [showWorkflowTracker, setShowWorkflowTracker] = useState(false);
|
||||||
|
const [workflowInstance, setWorkflowInstance] = useState<WorkflowInstance | null>(null);
|
||||||
|
const [isWorkflowLoading, setIsWorkflowLoading] = useState(false);
|
||||||
|
|
||||||
// File attachment fields for new version
|
// File attachment fields for new version
|
||||||
const [versionFiles, setVersionFiles] = useState<FileAttachmentItem[]>([]);
|
const [versionFiles, setVersionFiles] = useState<FileAttachmentItem[]>([]);
|
||||||
@ -111,6 +117,29 @@ const ViewDocument = (): ReactElement => {
|
|||||||
void loadVersionFiles();
|
void loadVersionFiles();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (activeTab === "workflow-history" && id) {
|
||||||
|
void loadWorkflowHistory();
|
||||||
|
}
|
||||||
|
}, [activeTab, id]);
|
||||||
|
|
||||||
|
const loadWorkflowHistory = async (): Promise<void> => {
|
||||||
|
try {
|
||||||
|
setIsHistoryLoading(true);
|
||||||
|
const res = await workflowService.listInstances({
|
||||||
|
entity_type: "document",
|
||||||
|
entity_id: id,
|
||||||
|
limit: 100,
|
||||||
|
offset: 0,
|
||||||
|
});
|
||||||
|
setWorkflowInstances(res.data || []);
|
||||||
|
} catch {
|
||||||
|
showToast.error("Failed to load workflow history");
|
||||||
|
} finally {
|
||||||
|
setIsHistoryLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const loadVersionFiles = async (): Promise<void> => {
|
const loadVersionFiles = async (): Promise<void> => {
|
||||||
setIsLoadingVersionFiles(true);
|
setIsLoadingVersionFiles(true);
|
||||||
try {
|
try {
|
||||||
@ -172,6 +201,26 @@ const ViewDocument = (): ReactElement => {
|
|||||||
setVersions(versionsRes.data || []);
|
setVersions(versionsRes.data || []);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openWorkflowTracker = async (instanceId?: string): Promise<void> => {
|
||||||
|
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 => {
|
const resetActionModal = (): void => {
|
||||||
setActiveAction(null);
|
setActiveAction(null);
|
||||||
setWorkflowDefinitionId("");
|
setWorkflowDefinitionId("");
|
||||||
@ -319,6 +368,11 @@ const ViewDocument = (): ReactElement => {
|
|||||||
label: "Change Summary",
|
label: "Change Summary",
|
||||||
render: (version) => version.change_summary || "-",
|
render: (version) => version.change_summary || "-",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: "module_name",
|
||||||
|
label: "Module",
|
||||||
|
render: (version) => version.module_name || "Platform",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: "created_by",
|
key: "created_by",
|
||||||
label: "Author",
|
label: "Author",
|
||||||
@ -373,6 +427,13 @@ const ViewDocument = (): ReactElement => {
|
|||||||
)}
|
)}
|
||||||
{document?.status === "in_review" && (
|
{document?.status === "in_review" && (
|
||||||
<>
|
<>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="inline-flex items-center gap-2 h-9 px-4 rounded-md bg-[#112868] text-white text-xs font-medium hover:bg-[#0c1d4a]"
|
||||||
|
onClick={() => void openWorkflowTracker()}
|
||||||
|
>
|
||||||
|
Workflow Status
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md bg-emerald-600 text-white text-xs font-medium hover:bg-emerald-700"
|
className="inline-flex items-center gap-2 h-9 px-4 rounded-md bg-emerald-600 text-white text-xs font-medium hover:bg-emerald-700"
|
||||||
@ -419,6 +480,11 @@ const ViewDocument = (): ReactElement => {
|
|||||||
<span className="px-2 py-1 rounded-full bg-blue-100 text-blue-700 text-[11px] font-medium">
|
<span className="px-2 py-1 rounded-full bg-blue-100 text-blue-700 text-[11px] font-medium">
|
||||||
v{document?.current_version || "-"}
|
v{document?.current_version || "-"}
|
||||||
</span>
|
</span>
|
||||||
|
{document?.module_name && (
|
||||||
|
<span className="px-2 py-1 rounded-full bg-purple-100 text-purple-700 text-[11px] font-medium">
|
||||||
|
{document.module_name}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -438,6 +504,13 @@ const ViewDocument = (): ReactElement => {
|
|||||||
>
|
>
|
||||||
Version History
|
Version History
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`text-sm pb-2 ${activeTab === "workflow-history" ? "text-[#084cc8] border-b-2 border-[#084cc8]" : "text-[#6b7280]"}`}
|
||||||
|
onClick={() => setActiveTab("workflow-history")}
|
||||||
|
>
|
||||||
|
Workflow History
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
@ -622,6 +695,53 @@ const ViewDocument = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{activeTab === "workflow-history" && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="border border-[rgba(0,0,0,0.08)] rounded-lg overflow-hidden">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Workflow</th>
|
||||||
|
<th className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Current Step</th>
|
||||||
|
<th className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Status</th>
|
||||||
|
<th className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Started At</th>
|
||||||
|
<th className="px-4 py-3 text-right text-[10px] font-bold text-gray-400 uppercase tracking-wider font-bold">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-100">
|
||||||
|
{isHistoryLoading ? (
|
||||||
|
<tr><td colSpan={5} className="px-4 py-4 text-center text-xs text-gray-500">Loading history...</td></tr>
|
||||||
|
) : workflowInstances.length === 0 ? (
|
||||||
|
<tr><td colSpan={5} className="px-4 py-4 text-center text-xs text-gray-500">No workflow history found.</td></tr>
|
||||||
|
) : workflowInstances.map((inst) => (
|
||||||
|
<tr key={inst.id}>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-xs font-semibold text-gray-800">{inst.workflow_name}</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-xs text-gray-600">{inst.current_step || '-'}</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-bold uppercase ${
|
||||||
|
inst.status === 'completed' ? 'bg-emerald-100 text-emerald-700' :
|
||||||
|
inst.status === 'active' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
|
||||||
|
}`}>
|
||||||
|
{inst.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-xs text-gray-500">{formatDateTime(inst.started_at)}</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-xs font-bold text-[#112868] hover:underline"
|
||||||
|
onClick={() => void openWorkflowTracker(inst.id)}
|
||||||
|
>
|
||||||
|
View Tracker
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
@ -725,6 +845,112 @@ const ViewDocument = (): ReactElement => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Workflow Instance Tracker Modal */}
|
||||||
|
<Modal
|
||||||
|
isOpen={showWorkflowTracker}
|
||||||
|
onClose={() => setShowWorkflowTracker(false)}
|
||||||
|
title="Workflow Status Tracker"
|
||||||
|
description="Track the progress of this document's approval workflow."
|
||||||
|
maxWidth="2xl"
|
||||||
|
>
|
||||||
|
<div className="p-6">
|
||||||
|
{isWorkflowLoading ? (
|
||||||
|
<div className="flex flex-col items-center justify-center py-10 space-y-3">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#112868]"></div>
|
||||||
|
<p className="text-sm text-gray-500 font-medium">Loading workflow details...</p>
|
||||||
|
</div>
|
||||||
|
) : workflowInstance ? (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Workflow Header Info */}
|
||||||
|
<div className="grid grid-cols-2 gap-4 p-4 bg-gray-50 rounded-lg border border-gray-100">
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] uppercase tracking-wider font-bold text-gray-400 block">Workflow</label>
|
||||||
|
<p className="text-sm font-semibold text-gray-800">{workflowInstance.workflow.name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] uppercase tracking-wider font-bold text-gray-400 block">Current Status</label>
|
||||||
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase ${
|
||||||
|
workflowInstance.status === 'completed' ? 'bg-emerald-100 text-emerald-700' :
|
||||||
|
workflowInstance.status === 'active' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
|
||||||
|
}`}>
|
||||||
|
{workflowInstance.status}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] uppercase tracking-wider font-bold text-gray-400 block">Started By</label>
|
||||||
|
<p className="text-sm font-medium text-gray-700">{workflowInstance.started_by.name}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="text-[10px] uppercase tracking-wider font-bold text-gray-400 block">Started At</label>
|
||||||
|
<p className="text-sm font-medium text-gray-700">{formatDateTime(workflowInstance.started_at)}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Tasks Table View */}
|
||||||
|
<div className="overflow-hidden border border-gray-100 rounded-lg">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead className="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Step</th>
|
||||||
|
<th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Assignee</th>
|
||||||
|
<th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Status</th>
|
||||||
|
<th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Action</th>
|
||||||
|
<th scope="col" className="px-4 py-3 text-left text-[10px] font-bold text-gray-400 uppercase tracking-wider">Completed At</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-100">
|
||||||
|
{workflowInstance.tasks.map((task) => (
|
||||||
|
<tr key={task.id} className={task.status === 'pending' ? 'bg-blue-50/30' : ''}>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
|
<span className={`text-xs font-bold ${task.status === 'pending' ? 'text-blue-700' : 'text-gray-800'}`}>
|
||||||
|
{task.step}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs text-gray-600">
|
||||||
|
<User className="w-3 h-3 text-gray-400" />
|
||||||
|
{task.assigned_to}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
|
<span className={`inline-flex items-center px-2 py-0.5 rounded text-[10px] font-bold uppercase ${
|
||||||
|
task.status === 'completed' ? 'bg-emerald-100 text-emerald-700' :
|
||||||
|
task.status === 'pending' ? 'bg-blue-100 text-blue-700' : 'bg-gray-100 text-gray-700'
|
||||||
|
}`}>
|
||||||
|
{task.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap">
|
||||||
|
{task.action_taken ? (
|
||||||
|
<span className="px-1.5 py-0.5 bg-gray-100 text-gray-700 rounded font-bold uppercase text-[9px]">
|
||||||
|
{task.action_taken}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-gray-400 text-[10px] italic">No action</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 whitespace-nowrap text-[11px] text-gray-500 font-medium">
|
||||||
|
{task.completed_at ? formatDateTime(task.completed_at) : '-'}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-10">
|
||||||
|
<p className="text-sm text-gray-500">No workflow tracking information available.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-8 flex justify-end">
|
||||||
|
<SecondaryButton onClick={() => setShowWorkflowTracker(false)}>
|
||||||
|
Close Tracker
|
||||||
|
</SecondaryButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -34,6 +34,8 @@ export interface CreateDocumentPayload {
|
|||||||
file_size?: number;
|
file_size?: number;
|
||||||
mime_type?: string;
|
mime_type?: string;
|
||||||
file_hash?: string;
|
file_hash?: string;
|
||||||
|
source_module?: string;
|
||||||
|
source_module_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileAttachmentItem {
|
export interface FileAttachmentItem {
|
||||||
@ -72,6 +74,8 @@ export interface CreateDocumentVersionPayload {
|
|||||||
file_size?: number;
|
file_size?: number;
|
||||||
mime_type?: string;
|
mime_type?: string;
|
||||||
file_hash?: string;
|
file_hash?: string;
|
||||||
|
source_module?: string;
|
||||||
|
source_module_id?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const documentService = {
|
export const documentService = {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import type {
|
|||||||
UpdateWorkflowDefinitionData,
|
UpdateWorkflowDefinitionData,
|
||||||
WorkflowDefinitionsResponse,
|
WorkflowDefinitionsResponse,
|
||||||
WorkflowDefinitionResponse,
|
WorkflowDefinitionResponse,
|
||||||
|
WorkflowInstanceResponse,
|
||||||
WorkflowDeleteResponse
|
WorkflowDeleteResponse
|
||||||
} from '@/types/workflow';
|
} from '@/types/workflow';
|
||||||
|
|
||||||
@ -35,6 +36,25 @@ class WorkflowService {
|
|||||||
return response.data;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getInstance(id: string, tenantId?: string): Promise<WorkflowInstanceResponse> {
|
||||||
|
const params = tenantId ? { tenantId: tenantId } : {};
|
||||||
|
const response = await apiClient.get<WorkflowInstanceResponse>(`${this.baseUrl}/instances/${id}`, { params });
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listInstances(params: {
|
||||||
|
entity_type?: string;
|
||||||
|
entity_id?: string;
|
||||||
|
status?: string;
|
||||||
|
source_module?: string;
|
||||||
|
source_module_id?: string;
|
||||||
|
limit?: number;
|
||||||
|
offset?: number;
|
||||||
|
}): Promise<{ success: boolean; data: any[]; pagination: { total: number; limit: number; offset: number } }> {
|
||||||
|
const response = await apiClient.get(`${this.baseUrl}/instances`, { params });
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
async createDefinition(data: CreateWorkflowDefinitionData, tenantId?: string): Promise<WorkflowDefinitionResponse> {
|
async createDefinition(data: CreateWorkflowDefinitionData, tenantId?: string): Promise<WorkflowDefinitionResponse> {
|
||||||
const params = tenantId ? { tenantId: tenantId } : {};
|
const params = tenantId ? { tenantId: tenantId } : {};
|
||||||
const response = await apiClient.post<WorkflowDefinitionResponse>(`${this.baseUrl}/definitions`, data, { params });
|
const response = await apiClient.post<WorkflowDefinitionResponse>(`${this.baseUrl}/definitions`, data, { params });
|
||||||
|
|||||||
@ -22,6 +22,7 @@ export interface DocumentSummary {
|
|||||||
owner?: string | null;
|
owner?: string | null;
|
||||||
next_review_date?: string | null;
|
next_review_date?: string | null;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
module_name?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentDetail {
|
export interface DocumentDetail {
|
||||||
@ -54,6 +55,7 @@ export interface DocumentDetail {
|
|||||||
created_by?: string | null;
|
created_by?: string | null;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
updated_at?: string;
|
updated_at?: string;
|
||||||
|
module_name?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentVersion {
|
export interface DocumentVersion {
|
||||||
@ -70,6 +72,7 @@ export interface DocumentVersion {
|
|||||||
approved_at?: string | null;
|
approved_at?: string | null;
|
||||||
created_by?: string | null;
|
created_by?: string | null;
|
||||||
created_at?: string;
|
created_at?: string;
|
||||||
|
module_name?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentListResponse {
|
export interface DocumentListResponse {
|
||||||
|
|||||||
@ -98,6 +98,57 @@ export interface WorkflowDefinitionResponse {
|
|||||||
data: WorkflowDefinition;
|
data: WorkflowDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface WorkflowInstance {
|
||||||
|
id: string;
|
||||||
|
workflow: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
entity: {
|
||||||
|
type: string;
|
||||||
|
id: string;
|
||||||
|
name: string | null;
|
||||||
|
};
|
||||||
|
current_step: {
|
||||||
|
id: string;
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
status: string;
|
||||||
|
context: Record<string, any>;
|
||||||
|
tasks: Array<{
|
||||||
|
id: string;
|
||||||
|
step: string;
|
||||||
|
assigned_to: string;
|
||||||
|
status: string;
|
||||||
|
action_taken: string | null;
|
||||||
|
completed_at: string | null;
|
||||||
|
due_at: string | null;
|
||||||
|
}>;
|
||||||
|
available_actions: Array<{
|
||||||
|
action: string;
|
||||||
|
name: string;
|
||||||
|
requires_signature: boolean;
|
||||||
|
requires_comment: boolean;
|
||||||
|
next_step: string;
|
||||||
|
}>;
|
||||||
|
started_by: {
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
started_at: string;
|
||||||
|
completed_at: string | null;
|
||||||
|
cancelled_at: string | null;
|
||||||
|
cancelled_by: string | null;
|
||||||
|
cancellation_reason: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkflowInstanceResponse {
|
||||||
|
success: boolean;
|
||||||
|
data: WorkflowInstance;
|
||||||
|
}
|
||||||
|
|
||||||
export interface WorkflowDeleteResponse {
|
export interface WorkflowDeleteResponse {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user