feat: implement document editing functionality with metadata and classification updates
This commit is contained in:
parent
7a368b09bc
commit
14bb57a574
@ -232,7 +232,7 @@ const WorkflowDefinitionsTable = ({
|
||||
key: "source_module",
|
||||
label: "Module",
|
||||
render: (wf) => (
|
||||
<span className="text-sm text-[#6b7280]">{wf.source_module}</span>
|
||||
<span className="text-sm text-[#6b7280]">{wf.source_module?.join(", ")}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import { useEffect, useState, type ReactElement } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import { Layout } from "@/components/layout/Layout";
|
||||
import { FormField, FormSelect, FormTextArea, PrimaryButton, RichTextEditor } from "@/components/shared";
|
||||
import { documentService, type FileAttachmentItem } from "@/services/document-service";
|
||||
@ -9,22 +12,50 @@ import { ArrowLeft, FileText, Info, Paperclip } from "lucide-react";
|
||||
import { moduleService } from "@/services/module-service";
|
||||
import type { MyModule } from "@/types/module";
|
||||
|
||||
const documentSchema = z.object({
|
||||
title: z.string().min(1, "Document title is required"),
|
||||
document_number: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
document_type: z.string().min(1, "Document type is required"),
|
||||
category_id: z.string().optional(),
|
||||
department: z.string().optional(),
|
||||
tags: z.string().optional(),
|
||||
selectedModuleId: z.string().min(1, "Source module is required"),
|
||||
content: z.string().optional(),
|
||||
contentHtml: z.string().min(1, "Document content is required"),
|
||||
});
|
||||
|
||||
type DocumentFormData = z.infer<typeof documentSchema>;
|
||||
|
||||
const CreateDocument = (): ReactElement => {
|
||||
const navigate = useNavigate();
|
||||
const [title, setTitle] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [documentNumber, setDocumentNumber] = useState("");
|
||||
const [documentType, setDocumentType] = useState("");
|
||||
const [categoryId, setCategoryId] = useState("");
|
||||
const [department, setDepartment] = useState("");
|
||||
const [tags, setTags] = useState("");
|
||||
const [content, setContent] = useState("");
|
||||
const [contentHtml, setContentHtml] = useState("");
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [types, setTypes] = useState<Array<{ code: string; name: string }>>([]);
|
||||
const [categories, setCategories] = useState<DocumentCategory[]>([]);
|
||||
const [modules, setModules] = useState<MyModule[]>([]);
|
||||
const [selectedModuleId, setSelectedModuleId] = useState("");
|
||||
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useForm<DocumentFormData>({
|
||||
resolver: zodResolver(documentSchema),
|
||||
defaultValues: {
|
||||
title: "",
|
||||
document_number: "",
|
||||
description: "",
|
||||
document_type: "",
|
||||
category_id: "",
|
||||
department: "",
|
||||
tags: "",
|
||||
selectedModuleId: "",
|
||||
content: "",
|
||||
contentHtml: "",
|
||||
},
|
||||
});
|
||||
|
||||
// File attachment fields
|
||||
const [files, setFiles] = useState<FileAttachmentItem[]>([]);
|
||||
@ -70,34 +101,31 @@ const CreateDocument = (): ReactElement => {
|
||||
const handleFileSelect = async (fileId: string): Promise<void> => {
|
||||
setSelectedFileId(fileId);
|
||||
if (!fileId) {
|
||||
// Clear file fields when deselected
|
||||
setFileName("");
|
||||
setFilePath("");
|
||||
setFileSize(undefined);
|
||||
setMimeType("");
|
||||
setFileHash("");
|
||||
setContentHtml("");
|
||||
setContent("");
|
||||
setValue("contentHtml", "");
|
||||
setValue("content", "");
|
||||
return;
|
||||
}
|
||||
|
||||
const selected = files.find((f) => f.id === fileId);
|
||||
if (!selected) return;
|
||||
|
||||
// Populate file metadata fields
|
||||
setFileName(selected.original_name);
|
||||
setFilePath(selected.file_path);
|
||||
setFileSize(selected.file_size);
|
||||
setMimeType(selected.mime_type);
|
||||
setFileHash(selected.checksum);
|
||||
|
||||
// Extract actual file content from backend
|
||||
try {
|
||||
showToast.success(`Extracting content from "${selected.original_name}"...`);
|
||||
const res = await documentService.getFileContent(fileId);
|
||||
if (res.success && res.data) {
|
||||
setContentHtml(res.data.html || "");
|
||||
setContent(res.data.text || "");
|
||||
setValue("contentHtml", res.data.html || "");
|
||||
setValue("content", res.data.text || "");
|
||||
showToast.success(`Content loaded from "${selected.original_name}"`);
|
||||
} else {
|
||||
showToast.error("Failed to extract file content");
|
||||
@ -105,42 +133,37 @@ const CreateDocument = (): ReactElement => {
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.error?.message || "Failed to extract file content";
|
||||
showToast.error(msg);
|
||||
// Fallback: set a placeholder
|
||||
const html = `<p>Document sourced from file: <strong>${selected.original_name}</strong></p>`;
|
||||
setContentHtml(html);
|
||||
setContent(`Document sourced from file: ${selected.original_name}`);
|
||||
setValue("contentHtml", html);
|
||||
setValue("content", `Document sourced from file: ${selected.original_name}`);
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (event: React.FormEvent): Promise<void> => {
|
||||
event.preventDefault();
|
||||
if (!title.trim() || !documentType) {
|
||||
showToast.error("Title and document type are required");
|
||||
return;
|
||||
}
|
||||
|
||||
const onFormSubmit = async (data: DocumentFormData): Promise<void> => {
|
||||
try {
|
||||
setIsSaving(true);
|
||||
const response = await documentService.create({
|
||||
title: title.trim(),
|
||||
description: description.trim() || undefined,
|
||||
document_number: documentNumber.trim() || undefined,
|
||||
document_type: documentType,
|
||||
category_id: categoryId || undefined,
|
||||
department: department.trim() || undefined,
|
||||
tags: tags
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean),
|
||||
content: content.trim() || undefined,
|
||||
content_html: contentHtml.trim() || undefined,
|
||||
title: data.title.trim(),
|
||||
description: data.description?.trim() || undefined,
|
||||
document_number: data.document_number?.trim() || undefined,
|
||||
document_type: data.document_type,
|
||||
category_id: data.category_id || undefined,
|
||||
department: data.department?.trim() || undefined,
|
||||
tags: data.tags
|
||||
? data.tags
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
content: data.content?.trim() || undefined,
|
||||
content_html: data.contentHtml.trim() || undefined,
|
||||
file_name: fileName || undefined,
|
||||
file_path: filePath || undefined,
|
||||
file_size: fileSize,
|
||||
mime_type: mimeType || undefined,
|
||||
file_hash: fileHash || undefined,
|
||||
source_module: selectedModuleId ? modules.find(m => m.id === selectedModuleId)?.module_id : undefined,
|
||||
source_module_id: selectedModuleId || undefined,
|
||||
source_module: modules.find((m) => m.id === data.selectedModuleId)!.module_id,
|
||||
source_module_id: data.selectedModuleId,
|
||||
});
|
||||
showToast.success("Document created successfully");
|
||||
navigate(`/tenant/documents/${response.data.id}`);
|
||||
@ -171,7 +194,7 @@ const CreateDocument = (): ReactElement => {
|
||||
],
|
||||
}}
|
||||
>
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-4">
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] p-4 md:p-5">
|
||||
<div className="flex items-start justify-between gap-4 border-b border-[rgba(0,0,0,0.08)] pb-4 mb-4">
|
||||
<div className="flex items-start gap-2">
|
||||
@ -201,23 +224,23 @@ const CreateDocument = (): ReactElement => {
|
||||
<FormField
|
||||
label="Document Title"
|
||||
required
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Enter document title"
|
||||
error={errors.title?.message}
|
||||
{...register("title")}
|
||||
/>
|
||||
<FormField
|
||||
label="Document Number"
|
||||
value={documentNumber}
|
||||
onChange={(e) => setDocumentNumber(e.target.value)}
|
||||
placeholder="Auto-generated if empty"
|
||||
error={errors.document_number?.message}
|
||||
{...register("document_number")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormTextArea
|
||||
label="Description"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
placeholder="Brief description of this document"
|
||||
error={errors.description?.message}
|
||||
{...register("description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -226,48 +249,67 @@ const CreateDocument = (): ReactElement => {
|
||||
Classification
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-4">
|
||||
<FormSelect
|
||||
label="Document Type"
|
||||
required
|
||||
value={documentType}
|
||||
onValueChange={setDocumentType}
|
||||
options={types.map((type) => ({ value: type.code, label: type.name }))}
|
||||
placeholder="Select type"
|
||||
<Controller
|
||||
name="document_type"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormSelect
|
||||
label="Document Type"
|
||||
required
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
options={types.map((type) => ({ value: type.code, label: type.name }))}
|
||||
placeholder="Select type"
|
||||
error={errors.document_type?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormSelect
|
||||
label="Category"
|
||||
value={categoryId}
|
||||
onValueChange={setCategoryId}
|
||||
options={categories.map((category) => ({
|
||||
value: category.id,
|
||||
label: `${category.name} (${category.code})`,
|
||||
}))}
|
||||
placeholder="Select category"
|
||||
<Controller
|
||||
name="category_id"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormSelect
|
||||
label="Category"
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
options={categories.map((category) => ({
|
||||
value: category.id,
|
||||
label: `${category.name} (${category.code})`,
|
||||
}))}
|
||||
placeholder="Select category"
|
||||
error={errors.category_id?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
label="Department"
|
||||
value={department}
|
||||
onChange={(e) => setDepartment(e.target.value)}
|
||||
placeholder="Optional"
|
||||
error={errors.department?.message}
|
||||
{...register("department")}
|
||||
/>
|
||||
<FormField
|
||||
label="Tags"
|
||||
value={tags}
|
||||
onChange={(e) => setTags(e.target.value)}
|
||||
placeholder="Comma separated tags (e.g. quality, sop)"
|
||||
error={errors.tags?.message}
|
||||
{...register("tags")}
|
||||
/>
|
||||
<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)"
|
||||
<Controller
|
||||
name="selectedModuleId"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormSelect
|
||||
label="Source Module"
|
||||
required
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
options={modules.map((m) => ({
|
||||
value: m.id,
|
||||
label: m.name,
|
||||
}))}
|
||||
placeholder="Select originating module"
|
||||
error={errors.selectedModuleId?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -309,19 +351,26 @@ const CreateDocument = (): ReactElement => {
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-[#0f1724]">Initial Content</h3>
|
||||
<span className="text-[11px] text-[#94a3b8]">
|
||||
{content.length} characters
|
||||
{watch("content")?.length || 0} characters
|
||||
</span>
|
||||
</div>
|
||||
<RichTextEditor
|
||||
label="Content"
|
||||
value={contentHtml}
|
||||
required
|
||||
placeholder="Write the initial document content..."
|
||||
minHeightClassName="min-h-[280px]"
|
||||
onChange={(html, text) => {
|
||||
setContentHtml(html);
|
||||
setContent(text);
|
||||
}}
|
||||
<Controller
|
||||
name="contentHtml"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<RichTextEditor
|
||||
label="Content"
|
||||
value={field.value}
|
||||
required
|
||||
placeholder="Write the initial document content..."
|
||||
minHeightClassName="min-h-[280px]"
|
||||
onChange={(html, text) => {
|
||||
field.onChange(html);
|
||||
setValue("content", text);
|
||||
}}
|
||||
error={errors.contentHtml?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-1 flex items-start gap-1.5 text-[11px] text-[#6b7280]">
|
||||
<Info className="w-3.5 h-3.5 mt-[1px] shrink-0" />
|
||||
@ -341,13 +390,18 @@ const CreateDocument = (): ReactElement => {
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
type="button"
|
||||
disabled={isSaving}
|
||||
className="h-10 px-4 border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#112868] hover:bg-[#f8fafc] disabled:opacity-60"
|
||||
onClick={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
{isSaving ? "Saving..." : "Save As Draft"}
|
||||
</button>
|
||||
<PrimaryButton type="submit" disabled={isSaving}>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
onClick={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
{isSaving ? "Creating..." : "Create Document"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
|
||||
@ -156,6 +156,22 @@ const Documents = (): ReactElement => {
|
||||
<span className="text-[#6b7280]">{formatDate(doc.updated_at)}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
label: "Actions",
|
||||
render: (doc) => (
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-[#084cc8] hover:underline font-medium"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
navigate(`/tenant/documents/edit/${doc.id}`);
|
||||
}}
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
),
|
||||
},
|
||||
],
|
||||
[navigate],
|
||||
);
|
||||
|
||||
266
src/pages/tenant/EditDocument.tsx
Normal file
266
src/pages/tenant/EditDocument.tsx
Normal file
@ -0,0 +1,266 @@
|
||||
import { useEffect, useState, type ReactElement } from "react";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import * as z from "zod";
|
||||
import { Layout } from "@/components/layout/Layout";
|
||||
import { FormField, FormSelect, FormTextArea, PrimaryButton } from "@/components/shared";
|
||||
import { documentService } from "@/services/document-service";
|
||||
import type { DocumentCategory } from "@/types/document";
|
||||
import { showToast } from "@/utils/toast";
|
||||
import { ArrowLeft, FileText } from "lucide-react";
|
||||
import { moduleService } from "@/services/module-service";
|
||||
import type { MyModule } from "@/types/module";
|
||||
|
||||
const documentSchema = z.object({
|
||||
title: z.string().min(1, "Document title is required"),
|
||||
description: z.string().optional(),
|
||||
category_id: z.string().optional(),
|
||||
department: z.string().optional(),
|
||||
tags: z.string().optional(),
|
||||
selectedModuleId: z.string().min(1, "Source module is required"),
|
||||
});
|
||||
|
||||
type DocumentFormData = z.infer<typeof documentSchema>;
|
||||
|
||||
const EditDocument = (): ReactElement => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [categories, setCategories] = useState<DocumentCategory[]>([]);
|
||||
const [modules, setModules] = useState<MyModule[]>([]);
|
||||
|
||||
const {
|
||||
control,
|
||||
register,
|
||||
handleSubmit,
|
||||
reset,
|
||||
formState: { errors },
|
||||
} = useForm<DocumentFormData>({
|
||||
resolver: zodResolver(documentSchema),
|
||||
defaultValues: {
|
||||
title: "",
|
||||
description: "",
|
||||
category_id: "",
|
||||
department: "",
|
||||
tags: "",
|
||||
selectedModuleId: "",
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async (): Promise<void> => {
|
||||
if (!id) return;
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const [categoriesRes, modulesRes, docRes] = await Promise.all([
|
||||
documentService.getCategories(),
|
||||
moduleService.getMyModules(),
|
||||
documentService.getById(id),
|
||||
]);
|
||||
|
||||
setCategories(categoriesRes.data || []);
|
||||
const myModules = modulesRes.data || [];
|
||||
setModules(myModules);
|
||||
|
||||
const doc = docRes.data;
|
||||
// Find matching module by id (UUID) or module_id (code)
|
||||
const matchedModule = myModules.find(m =>
|
||||
(doc.source_module_id && m.id === doc.source_module_id) ||
|
||||
(doc.source_module && m.module_id === doc.source_module)
|
||||
);
|
||||
|
||||
reset({
|
||||
title: doc.title,
|
||||
description: doc.description || "",
|
||||
category_id: doc.category_id || "",
|
||||
department: doc.department || "",
|
||||
tags: (doc.tags || []).join(", "),
|
||||
selectedModuleId: matchedModule?.id || "",
|
||||
});
|
||||
} catch (err: any) {
|
||||
showToast.error(err?.response?.data?.error?.message || "Failed to load document data");
|
||||
navigate("/tenant/documents");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
void loadData();
|
||||
}, [id, reset, navigate]);
|
||||
|
||||
const onFormSubmit = async (data: DocumentFormData): Promise<void> => {
|
||||
if (!id) return;
|
||||
try {
|
||||
setIsSaving(true);
|
||||
await documentService.update(id, {
|
||||
title: data.title.trim(),
|
||||
description: data.description?.trim() || undefined,
|
||||
category_id: data.category_id || undefined,
|
||||
department: data.department?.trim() || undefined,
|
||||
tags: data.tags
|
||||
? data.tags
|
||||
.split(",")
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
source_module: modules.find((m) => m.id === data.selectedModuleId)!.module_id,
|
||||
source_module_id: data.selectedModuleId,
|
||||
});
|
||||
showToast.success("Document updated successfully");
|
||||
navigate(`/tenant/documents/${id}`);
|
||||
} catch (err: any) {
|
||||
showToast.error(
|
||||
err?.response?.data?.error?.message || "Failed to update document",
|
||||
);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Layout currentPage="Document Service">
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<p className="text-sm text-[#6b7280]">Loading document details...</p>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout
|
||||
currentPage="Document Service"
|
||||
breadcrumbs={[
|
||||
{ label: "Document Service", path: "/tenant/documents" },
|
||||
{ label: "Edit Document" },
|
||||
]}
|
||||
pageHeader={{
|
||||
title: "Edit Document",
|
||||
description: "Modify document metadata and classification details.",
|
||||
}}
|
||||
>
|
||||
<form onSubmit={handleSubmit(onFormSubmit)} className="space-y-4">
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] p-4 md:p-5">
|
||||
<div className="flex items-start justify-between gap-4 border-b border-[rgba(0,0,0,0.08)] pb-4 mb-4">
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="mt-0.5 w-8 h-8 rounded-md bg-[#112868]/10 flex items-center justify-center">
|
||||
<FileText className="w-4 h-4 text-[#112868]" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-[#0f1724]">
|
||||
Edit Document Metadata
|
||||
</h3>
|
||||
<p className="text-xs text-[#6b7280] mt-1">
|
||||
Updates will apply to the current version of the document.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(-1)}
|
||||
className="inline-flex items-center gap-1.5 text-xs text-[#475569] hover:text-[#0f1724]"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" />
|
||||
Back
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-x-4">
|
||||
<FormField
|
||||
label="Document Title"
|
||||
required
|
||||
placeholder="Enter document title"
|
||||
error={errors.title?.message}
|
||||
{...register("title")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormTextArea
|
||||
label="Description"
|
||||
placeholder="Brief description of this document"
|
||||
error={errors.description?.message}
|
||||
{...register("description")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] p-4 md:p-5">
|
||||
<h3 className="text-sm font-semibold text-[#0f1724] mb-3">
|
||||
Classification
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-x-4">
|
||||
<Controller
|
||||
name="category_id"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormSelect
|
||||
label="Category"
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
options={categories.map((category) => ({
|
||||
value: category.id,
|
||||
label: `${category.name} (${category.code})`,
|
||||
}))}
|
||||
placeholder="Select category"
|
||||
error={errors.category_id?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
label="Department"
|
||||
placeholder="Optional"
|
||||
error={errors.department?.message}
|
||||
{...register("department")}
|
||||
/>
|
||||
<FormField
|
||||
label="Tags"
|
||||
placeholder="Comma separated tags (e.g. quality, sop)"
|
||||
error={errors.tags?.message}
|
||||
{...register("tags")}
|
||||
/>
|
||||
<Controller
|
||||
name="selectedModuleId"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<FormSelect
|
||||
label="Source Module"
|
||||
required
|
||||
value={field.value}
|
||||
onValueChange={field.onChange}
|
||||
options={modules.map((m) => ({
|
||||
value: m.id,
|
||||
label: m.name,
|
||||
}))}
|
||||
placeholder="Select originating module"
|
||||
error={errors.selectedModuleId?.message}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-lg shadow-[0px_4px_24px_0px_rgba(0,0,0,0.02)] p-4 md:p-5">
|
||||
<div className="flex gap-2 mt-1">
|
||||
<button
|
||||
type="button"
|
||||
className="h-10 px-4 border border-[rgba(0,0,0,0.08)] rounded-md text-xs text-[#0f1724] hover:bg-gray-50"
|
||||
onClick={() => navigate(-1)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
disabled={isSaving}
|
||||
onClick={handleSubmit(onFormSubmit)}
|
||||
>
|
||||
{isSaving ? "Updating..." : "Update Document"}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditDocument;
|
||||
@ -75,6 +75,7 @@ const ViewDocument = (): ReactElement => {
|
||||
const [newVersionChangeReason, setNewVersionChangeReason] = useState("minor_edit");
|
||||
const [newVersionChangeSummary, setNewVersionChangeSummary] = useState("");
|
||||
const [isMajorVersion, setIsMajorVersion] = useState(false);
|
||||
const [versionErrors, setVersionErrors] = useState<Record<string, string>>({});
|
||||
const [isVersionSaving, setIsVersionSaving] = useState(false);
|
||||
const [showWorkflowTracker, setShowWorkflowTracker] = useState(false);
|
||||
const [workflowInstance, setWorkflowInstance] = useState<WorkflowInstance | null>(null);
|
||||
@ -244,6 +245,7 @@ const ViewDocument = (): ReactElement => {
|
||||
const response = await workflowService.listDefinitions({
|
||||
status: "active",
|
||||
entity_type: "document",
|
||||
source_module_id: document?.source_module_id || undefined,
|
||||
limit: 100,
|
||||
offset: 0,
|
||||
});
|
||||
@ -320,12 +322,20 @@ const ViewDocument = (): ReactElement => {
|
||||
|
||||
const handleCreateVersion = async (): Promise<void> => {
|
||||
if (!id) return;
|
||||
|
||||
// Clear previous errors
|
||||
setVersionErrors({});
|
||||
const localErrors: Record<string, string> = {};
|
||||
|
||||
if (!newVersionChangeReason) {
|
||||
showToast.error("Change reason is required");
|
||||
return;
|
||||
localErrors.change_reason = "Change reason is required";
|
||||
}
|
||||
if (!newVersionContent.trim()) {
|
||||
showToast.error("Document content is required");
|
||||
localErrors.content = "Document content is required";
|
||||
}
|
||||
|
||||
if (Object.keys(localErrors).length > 0) {
|
||||
setVersionErrors(localErrors);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -462,13 +472,22 @@ const ViewDocument = (): ReactElement => {
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{document?.status === "draft" && (
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md bg-[#084cc8] text-white text-xs font-medium hover:bg-[#063a99]"
|
||||
onClick={() => void openActionModal("submit")}
|
||||
>
|
||||
{ACTION_LABELS["submit"]}
|
||||
</button>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md border border-[#e2e8f0] text-[#475569] bg-white text-xs font-medium hover:bg-[#f8fafc] transition-colors"
|
||||
onClick={() => navigate(`/tenant/documents/edit/${id}`)}
|
||||
>
|
||||
Edit Metadata
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="inline-flex items-center gap-2 h-9 px-4 rounded-md bg-[#084cc8] text-white text-xs font-medium hover:bg-[#063a99] transition-colors"
|
||||
onClick={() => void openActionModal("submit")}
|
||||
>
|
||||
{ACTION_LABELS["submit"]}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{document?.status === "in_review" && (
|
||||
<>
|
||||
@ -675,7 +694,9 @@ const ViewDocument = (): ReactElement => {
|
||||
onChange={(html, text) => {
|
||||
setNewVersionContentHtml(html);
|
||||
setNewVersionContent(text);
|
||||
if (text.trim()) setVersionErrors(prev => ({ ...prev, content: "" }));
|
||||
}}
|
||||
error={versionErrors.content}
|
||||
/>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<FormSelect
|
||||
@ -688,8 +709,12 @@ const ViewDocument = (): ReactElement => {
|
||||
{ value: "major_rewrite", label: "major_rewrite" },
|
||||
]}
|
||||
value={newVersionChangeReason}
|
||||
onValueChange={setNewVersionChangeReason}
|
||||
onValueChange={(val) => {
|
||||
setNewVersionChangeReason(val);
|
||||
if (val) setVersionErrors(prev => ({ ...prev, change_reason: "" }));
|
||||
}}
|
||||
placeholder="Select change reason"
|
||||
error={versionErrors.change_reason}
|
||||
/>
|
||||
<div className="flex items-center gap-2 pt-8">
|
||||
<input
|
||||
@ -822,14 +847,36 @@ const ViewDocument = (): ReactElement => {
|
||||
>
|
||||
<div className="p-5 space-y-3">
|
||||
{activeAction === "submit" && (
|
||||
<FormSelect
|
||||
label="Workflow Definition"
|
||||
required
|
||||
options={workflowOptions}
|
||||
value={workflowDefinitionId}
|
||||
onValueChange={setWorkflowDefinitionId}
|
||||
placeholder="Select active workflow definition"
|
||||
/>
|
||||
<div className="space-y-3">
|
||||
<FormSelect
|
||||
label="Workflow Definition"
|
||||
required
|
||||
options={workflowOptions}
|
||||
value={workflowDefinitionId}
|
||||
onValueChange={setWorkflowDefinitionId}
|
||||
placeholder="Select active workflow definition"
|
||||
/>
|
||||
{/* <div className="flex flex-col gap-1.5 px-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-medium text-[#64748b] min-w-[80px]">Entity Type:</span>
|
||||
<span className="text-[11px] font-semibold text-[#0f1724] bg-[#f1f5f9] px-2 py-0.5 rounded border border-[#e2e8f0]">
|
||||
Document
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-medium text-[#64748b] min-w-[80px]">Source Module:</span>
|
||||
<span className="text-[11px] font-semibold text-[#0f1724] bg-[#f1f5f9] px-2 py-0.5 rounded border border-[#e2e8f0]">
|
||||
{document?.module_name || "Platform"}
|
||||
</span>
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="mt-2 p-2 bg-red-50 border border-red-100 rounded text-[10px] leading-relaxed text-[#e11d48]">
|
||||
<strong>Note:</strong> Currently displaying active workflow definitions registered for
|
||||
<span className="font-semibold mx-0.5">Entity Type: Document</span> and
|
||||
<span className="font-semibold mx-0.5">Module: {document?.module_name || "Platform"}</span>.
|
||||
If the list is empty, please go to Workflow Management and create a definition for this specific configuration.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{activeAction === "approve" && (
|
||||
<div>
|
||||
|
||||
@ -16,6 +16,7 @@ const WorkflowDefination = lazy(
|
||||
const Suppliers = lazy(() => import("@/pages/tenant/Suppliers"));
|
||||
const Documents = lazy(() => import("@/pages/tenant/Documents"));
|
||||
const CreateDocument = lazy(() => import("@/pages/tenant/CreateDocument"));
|
||||
const EditDocument = lazy(() => import("@/pages/tenant/EditDocument"));
|
||||
const ViewDocument = lazy(() => import("@/pages/tenant/ViewDocument"));
|
||||
const DocumentCategories = lazy(() => import("@/pages/tenant/DocumentCategories"));
|
||||
const Tasks = lazy(() => import("@/pages/tenant/Tasks"));
|
||||
@ -93,6 +94,10 @@ export const tenantAdminRoutes: RouteConfig[] = [
|
||||
path: "/tenant/documents/create",
|
||||
element: <LazyRoute component={CreateDocument} />,
|
||||
},
|
||||
{
|
||||
path: "/tenant/documents/edit/:id",
|
||||
element: <LazyRoute component={EditDocument} />,
|
||||
},
|
||||
{
|
||||
path: "/tenant/documents/:id",
|
||||
element: <LazyRoute component={ViewDocument} />,
|
||||
|
||||
@ -55,6 +55,9 @@ export interface DocumentDetail {
|
||||
created_by?: string | null;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
source_module?: string | null;
|
||||
source_module_id?: string | null;
|
||||
category_id?: string | null;
|
||||
module_name?: string | null;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user