diff --git a/src/components/layout/Layout.tsx b/src/components/layout/Layout.tsx index 1637e90..aed4a8f 100644 --- a/src/components/layout/Layout.tsx +++ b/src/components/layout/Layout.tsx @@ -12,6 +12,7 @@ interface LayoutProps { title: string; description?: string; tabs?: TabItem[]; + action?: React.ReactNode; }; } @@ -67,6 +68,7 @@ export const Layout = ({ title={pageHeader.title} description={pageHeader.description} tabs={pageHeader.tabs} + action={pageHeader.action} /> )} {children} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index b30f061..8d5a11a 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from "react"; import { Link, useLocation } from "react-router-dom"; import { LayoutDashboard, @@ -12,6 +13,8 @@ import { BadgeCheck, GitBranch, Briefcase, + ChevronDown, + ChevronRight, } from "lucide-react"; import { cn } from "@/lib/utils"; @@ -22,7 +25,16 @@ import { AuthenticatedImage } from "@/components/shared"; interface MenuItem { icon: React.ComponentType<{ className?: string; strokeWidth?: number }>; label: string; - path: string; + path?: string; + isGroup?: boolean; + children?: Array<{ + label: string; + path: string; + requiredPermission?: { + resource: string; + action?: string; + }; + }>; requiredPermission?: { resource: string; action?: string; // If not provided, checks for '*' or 'read' @@ -95,8 +107,14 @@ const tenantAdminPlatformMenu: MenuItem[] = [ }, { icon: FileText, - label: "Document Service", - path: "/tenant/documents", + label: "Document Services", + isGroup: true, + children: [ + { label: "Document Lists", path: "/tenant/documents", requiredPermission: { resource: "document" } }, + { label: "Create Document", path: "/tenant/documents/create", requiredPermission: { resource: "document", action: "create" } }, + { label: "Categories", path: "/tenant/documents/categories", requiredPermission: { resource: "document" } }, + { label: "Due for Review", path: "/tenant/documents/due-for-review", requiredPermission: { resource: "document" } }, + ], requiredPermission: { resource: "document" }, }, { icon: Package, label: "Modules", path: "/tenant/modules" }, @@ -117,6 +135,92 @@ const tenantAdminSystemMenu: MenuItem[] = [ }, ]; +const GroupMenuItem = ({ + item, + childrenItems, + location, + isSuperAdmin, + theme, + onClose +}: { + item: MenuItem; + childrenItems: any[]; + location: any; + isSuperAdmin: boolean; + theme: any; + onClose: () => void; +}) => { + const isChildActive = (path: string) => { + // Special handling for Document Lists to NOT show as active when sub-actions are active + if (path === "/tenant/documents") { + const subActions = ["/create", "/categories", "/due-for-review", "/edit"]; + const isSubActionActive = subActions.some(sub => location.pathname.startsWith(path + sub)); + if (isSubActionActive) return false; + } + return location.pathname === path || location.pathname.startsWith(`${path}/`); + }; + const isAnyChildActive = childrenItems.some(child => isChildActive(child.path)); + const [isExpanded, setIsExpanded] = useState(isAnyChildActive); + + useEffect(() => { + if (isAnyChildActive) setIsExpanded(true); + }, [isAnyChildActive]); + + const Icon = item.icon; + + return ( +
+ + + {isExpanded && ( +
+ {childrenItems.map((child) => { + const isActive = isChildActive(child.path); + return ( + { + if (window.innerWidth < 768) { + onClose(); + } + }} + className={cn( + "flex items-center px-4 py-2 rounded-r-md text-[13px] font-medium transition-all", + isActive + ? "text-[#112868] font-bold bg-gray-50" + : "text-[#475569] hover:text-[#0f1724] hover:bg-gray-50" + )} + style={isActive ? { color: !isSuperAdmin && theme?.primary_color ? theme.primary_color : "#112868" } : undefined} + > + {child.label} + + ); + })} +
+ )} +
+ ); +}; + export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { const location = useLocation(); const { roles, permissions } = useAppSelector((state) => state.auth); @@ -200,22 +304,42 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { }); }; - // Filter menu items based on permissions for tenant users const filterMenuItems = (items: MenuItem[]): MenuItem[] => { if (isSuperAdmin) { - return items; // Show all items for super admin + return items; } return items.filter((item) => { - // If no required permission, always show (e.g., Dashboard, Modules, Settings) if (!item.requiredPermission) { return true; } - return hasPermission( + const hasParentPermission = hasPermission( item.requiredPermission.resource, item.requiredPermission.action, ); + + if (!hasParentPermission) return false; + + if (item.isGroup && item.children) { + // Deep copy children to avoid mutating original menu arrays + const filteredChildren = item.children.filter((child) => { + if (!child.requiredPermission) return true; + return hasPermission( + child.requiredPermission.resource, + child.requiredPermission.action, + ); + }); + + // We need to return a new object to avoid issues + if (filteredChildren.length > 0) { + (item as any)._filteredChildren = filteredChildren; + return true; + } + return false; + } + + return true; }); }; @@ -243,16 +367,31 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
{items.map((item) => { + if (item.isGroup) { + const children = (item as any)._filteredChildren || item.children || []; + return ( + + ); + } + const Icon = item.icon; const isTenantDashboardPath = item.path === "/tenant"; const isActive = isTenantDashboardPath ? location.pathname === "/tenant" - : location.pathname === item.path || - location.pathname.startsWith(`${item.path}/`); + : item.path && (location.pathname === item.path || + location.pathname.startsWith(`${item.path}/`)); return ( { // Close sidebar on mobile when navigating if (window.innerWidth < 768) { @@ -281,7 +420,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { } > - + {item.label} @@ -352,11 +491,14 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
- {/* Platform Menu */} - + {/* Menu Sections (Only this part should scroll) */} +
+ {/* Platform Menu */} + - {/* System Menu */} - + {/* System Menu */} + +
{/* Support Center */}
@@ -427,15 +569,18 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
- {/* Platform Menu */} - {platformMenu.length > 0 && ( - - )} + {/* Menu Sections (Only this part should scroll) */} +
+ {/* Platform Menu */} + {platformMenu.length > 0 && ( + + )} - {/* System Menu */} - {systemMenu.length > 0 && ( - - )} + {/* System Menu */} + {systemMenu.length > 0 && ( + + )} +
{/* Support Center */}
diff --git a/src/components/shared/ActionDropdown.tsx b/src/components/shared/ActionDropdown.tsx index adcb860..edf56f5 100644 --- a/src/components/shared/ActionDropdown.tsx +++ b/src/components/shared/ActionDropdown.tsx @@ -1,15 +1,24 @@ -import { useState, useRef, useEffect } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; import type { ReactElement } from 'react'; import { MoreVertical, Eye, Edit, Trash2, Users, BarChart3 } from 'lucide-react'; import { cn } from '@/lib/utils'; +export interface ActionItem { + label: string; + onClick: () => void | Promise; + icon?: React.ReactNode; + variant?: 'danger' | 'default'; +} + interface ActionDropdownProps { onView?: () => void; onEdit?: () => void; onDelete?: () => void; onContacts?: () => void; onScorecards?: () => void; + actions?: ActionItem[]; + trigger?: React.ReactNode; className?: string; } @@ -19,6 +28,8 @@ export const ActionDropdown = ({ onDelete, onContacts, onScorecards, + actions, + trigger, className, }: ActionDropdownProps): ReactElement => { const [isOpen, setIsOpen] = useState(false); @@ -56,14 +67,14 @@ export const ActionDropdown = ({ const rect = buttonRef.current.getBoundingClientRect(); const spaceBelow = window.innerHeight - rect.bottom; const spaceAbove = rect.top; - const dropdownHeight = 120; // Approximate height of dropdown menu + const dropdownHeight = actions ? actions.length * 32 + 16 : 120; // Approximate height based on actions // Determine if should open upward or downward const shouldOpenUp = spaceBelow < dropdownHeight && spaceAbove > spaceBelow; // Calculate dropdown position const right = window.innerWidth - rect.right; - const width = 76; // Fixed width of dropdown + const width = actions ? 140 : 76; // Wider for custom actions if (shouldOpenUp) { // Position above the button @@ -88,90 +99,123 @@ export const ActionDropdown = ({ document.removeEventListener('mousedown', handleClickOutside); window.removeEventListener('scroll', handleScroll, true); }; - }, [isOpen]); + }, [isOpen, actions]); - const handleAction = (action: () => void | undefined) => { + const handleAction = (action?: () => void | Promise) => { if (action) { - action(); + void Promise.resolve(action()).catch(console.error); } setIsOpen(false); }; return (
- + {trigger ? ( + React.cloneElement(trigger as React.ReactElement, { + ref: buttonRef, + onClick: (e: React.MouseEvent) => { + e.stopPropagation(); + setIsOpen(!isOpen); + const triggerProps = (trigger as React.ReactElement).props; + if (triggerProps.onClick) triggerProps.onClick(e); + }, + }) + ) : ( + + )} {isOpen && buttonRef.current && createPortal(
- {onView && ( - - )} - {onEdit && ( - - )} - {onDelete && ( - - )} - {onContacts && ( - - )} - {onScorecards && ( - + {actions ? ( + actions.map((action, index) => ( + + )) + ) : ( + <> + {onView && ( + + )} + {onEdit && ( + + )} + {onDelete && ( + + )} + {onContacts && ( + + )} + {onScorecards && ( + + )} + )}
, @@ -180,3 +224,4 @@ export const ActionDropdown = ({
); }; + diff --git a/src/components/shared/FilterDropdown.tsx b/src/components/shared/FilterDropdown.tsx index 1fa8250..0c6140b 100644 --- a/src/components/shared/FilterDropdown.tsx +++ b/src/components/shared/FilterDropdown.tsx @@ -37,6 +37,7 @@ export const FilterDropdown = ({ bottom?: string; left: string; width: string; + minWidth?: string; }>({ left: '0', width: '0' }); // Handle click outside @@ -82,14 +83,16 @@ export const FilterDropdown = ({ setDropdownStyle({ bottom: `${bottom}px`, left: `${left}px`, - width: `${width}px`, + minWidth: `${width}px`, + width: 'auto', }); } else { const top = rect.bottom; setDropdownStyle({ top: `${top}px`, left: `${left}px`, - width: `${width}px`, + minWidth: `${width}px`, + width: 'auto', }); } } @@ -120,13 +123,20 @@ export const FilterDropdown = ({ ref={buttonRef} type="button" onClick={() => setIsOpen(!isOpen)} - className="flex items-center gap-2 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-sm text-[#475569] hover:bg-gray-50 transition-colors min-h-[44px]" + className={cn( + "flex items-center gap-2 px-3 py-2 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-sm transition-all min-h-[40px] hover:bg-gray-50", + value ? "border-[#112868]/20 bg-[#112868]/5" : "text-[#475569]" + )} > - {showIcon && icon && {icon}} - {label} - {displayText} + {showIcon && icon && {icon}} + {label} + {value && ( + + {displayText} + + )} @@ -148,7 +158,7 @@ export const FilterDropdown = ({ setIsOpen(false); }} className={cn( - 'w-full px-3 py-2 text-left text-sm text-[#0e1b2a] hover:bg-gray-50 transition-colors', + 'w-full px-3 py-2 text-left text-sm text-[#0e1b2a] hover:bg-gray-50 transition-colors whitespace-nowrap', !value && 'bg-gray-50' )} > @@ -169,7 +179,7 @@ export const FilterDropdown = ({ setIsOpen(false); }} className={cn( - 'w-full px-3 py-2 text-left text-sm text-[#0e1b2a] hover:bg-gray-50 transition-colors', + 'w-full px-3 py-2 text-left text-sm text-[#0e1b2a] hover:bg-gray-50 transition-colors whitespace-nowrap', isSelected && 'bg-gray-50' )} > diff --git a/src/components/shared/PageHeader.tsx b/src/components/shared/PageHeader.tsx index 33600d1..478d9c2 100644 --- a/src/components/shared/PageHeader.tsx +++ b/src/components/shared/PageHeader.tsx @@ -13,6 +13,7 @@ interface PageHeaderProps { title: string; description?: string; tabs?: TabItem[]; + action?: React.ReactNode; } const defaultTabs: TabItem[] = [ @@ -28,6 +29,7 @@ export const PageHeader = ({ title, description, tabs, + action, }: PageHeaderProps): ReactElement => { const location = useLocation(); const { roles } = useAppSelector((state) => state.auth); @@ -58,7 +60,7 @@ export const PageHeader = ({ .sort((a, b) => b.path.length - a.path.length)[0]?.path ?? null; return ( -
+
{/* Title and Description */}

@@ -71,28 +73,38 @@ export const PageHeader = ({ )}

- {/* Tabs Navigation - Only show for super_admin */} - {resolvedTabs.length > 0 && ( -
- {resolvedTabs.map((tab) => { - const isActive = tab.path === activeTabPath; - return ( - - {tab.label} - - ); - })} -
- )} + {/* Navigation Area: Tabs and Actions */} +
+ {/* Action Button */} + {action && ( +
+ {action} +
+ )} + + {/* Tabs Navigation - Only show for super_admin */} + {resolvedTabs.length > 0 && ( +
+ {resolvedTabs.map((tab) => { + const isActive = tab.path === activeTabPath; + return ( + + {tab.label} + + ); + })} +
+ )} +
); }; diff --git a/src/pages/tenant/CreateDocument.tsx b/src/pages/tenant/CreateDocument.tsx index e7efb93..62a72cd 100644 --- a/src/pages/tenant/CreateDocument.tsx +++ b/src/pages/tenant/CreateDocument.tsx @@ -4,22 +4,30 @@ 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"; +import { + FormField, + FormSelect, + FormTextArea, + PrimaryButton, + RichTextEditor, +} from "@/components/shared"; +import { + documentService, + type FileAttachmentItem, +} from "@/services/document-service"; import type { DocumentCategory } from "@/types/document"; import { showToast } from "@/utils/toast"; -import { ArrowLeft, FileText, Info, Paperclip } from "lucide-react"; +import { ArrowLeft, FileText, Info, Paperclip, X } 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(), + tags: z.array(z.string()), selectedModuleId: z.string().min(1, "Source module is required"), content: z.string().optional(), contentHtml: z.string().min(1, "Document content is required"), @@ -45,12 +53,11 @@ const CreateDocument = (): ReactElement => { resolver: zodResolver(documentSchema), defaultValues: { title: "", - document_number: "", description: "", document_type: "", category_id: "", department: "", - tags: "", + tags: [], selectedModuleId: "", content: "", contentHtml: "", @@ -121,7 +128,9 @@ const CreateDocument = (): ReactElement => { setFileHash(selected.checksum); try { - showToast.success(`Extracting content from "${selected.original_name}"...`); + showToast.success( + `Extracting content from "${selected.original_name}"...`, + ); const res = await documentService.getFileContent(fileId); if (res.success && res.data) { setValue("contentHtml", res.data.html || ""); @@ -131,11 +140,15 @@ const CreateDocument = (): ReactElement => { showToast.error("Failed to extract file content"); } } catch (err: any) { - const msg = err?.response?.data?.error?.message || "Failed to extract file content"; + const msg = + err?.response?.data?.error?.message || "Failed to extract file content"; showToast.error(msg); const html = `

Document sourced from file: ${selected.original_name}

`; setValue("contentHtml", html); - setValue("content", `Document sourced from file: ${selected.original_name}`); + setValue( + "content", + `Document sourced from file: ${selected.original_name}`, + ); } }; @@ -145,16 +158,10 @@ const CreateDocument = (): ReactElement => { const response = await documentService.create({ 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) - : [], + tags: data.tags || [], content: data.content?.trim() || undefined, content_html: data.contentHtml.trim() || undefined, file_name: fileName || undefined, @@ -162,7 +169,8 @@ const CreateDocument = (): ReactElement => { file_size: fileSize, mime_type: mimeType || undefined, file_hash: fileHash || undefined, - source_module: modules.find((m) => m.id === data.selectedModuleId)!.module_id, + source_module: modules.find((m) => m.id === data.selectedModuleId)! + .module_id, source_module_id: data.selectedModuleId, }); showToast.success("Document created successfully"); @@ -187,11 +195,6 @@ const CreateDocument = (): ReactElement => { title: "Create Document", description: "Fill in document details, classification and draft content before submitting for workflow.", - tabs: [ - { label: "Document List", path: "/tenant/documents" }, - { label: "Create Document", path: "/tenant/documents/create" }, - { label: "Category Management", path: "/tenant/documents/categories" }, - ], }} >
@@ -206,7 +209,8 @@ const CreateDocument = (): ReactElement => { New Controlled Document

- Document will be created in Draft status. + Document will be created in{" "} + Draft status.

@@ -220,7 +224,7 @@ const CreateDocument = (): ReactElement => { -
+
{ error={errors.title?.message} {...register("title")} /> -
{ required value={field.value} onValueChange={field.onChange} - options={types.map((type) => ({ value: type.code, label: type.name }))} + options={types.map((type) => ({ + value: type.code, + label: type.name, + }))} placeholder="Select type" error={errors.document_type?.message} /> @@ -283,16 +284,62 @@ const CreateDocument = (): ReactElement => { /> - +
+ + ( +
+ {(field.value || []).map((tag, tagIdx) => ( + + {tag} + + + ))} + { + if (e.key === "Enter") { + e.preventDefault(); + const val = e.currentTarget.value.trim(); + if (val && !(field.value || []).includes(val)) { + field.onChange([...(field.value || []), val]); + e.currentTarget.value = ""; + } + } + }} + /> +
+ )} + /> + {errors.tags && ( +

{errors.tags.message}

+ )} +
{
-

Attach File (Optional)

+

+ Attach File (Optional) +

- Select a previously uploaded file to automatically populate content and file metadata. + Select a previously uploaded file to automatically populate content + and file metadata.

{ label: `${f.original_name} (${f.file_size_formatted})`, })), ]} - placeholder={isLoadingFiles ? "Loading files..." : "Select a file to attach"} + placeholder={ + isLoadingFiles ? "Loading files..." : "Select a file to attach" + } /> {selectedFileId && (
-

File: {fileName}

-

Type: {mimeType}

-

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

-

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

+

+ File: {fileName} +

+

+ Type: {mimeType} +

+

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

+

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

)}
-

Initial Content

+

+ Initial Content +

{watch("content")?.length || 0} characters @@ -363,7 +427,7 @@ const CreateDocument = (): ReactElement => { value={field.value} required placeholder="Write the initial document content..." - minHeightClassName="min-h-[280px]" + minHeightClassName="h-[400px] overflow-y-auto" onChange={(html, text) => { field.onChange(html); setValue("content", text); diff --git a/src/pages/tenant/DocumentCategories.tsx b/src/pages/tenant/DocumentCategories.tsx index 3b76dcd..7b2db99 100644 --- a/src/pages/tenant/DocumentCategories.tsx +++ b/src/pages/tenant/DocumentCategories.tsx @@ -1,19 +1,70 @@ import { useEffect, useMemo, useState, type ReactElement } from "react"; -import { useNavigate } from "react-router-dom"; +// 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 { DataTable, FormField, PrimaryButton, type Column } from "@/components/shared"; +import { + DataTable, + FormField, + FormSelect, + FormTextArea, + PrimaryButton, + Modal, + ActionDropdown, + DeleteConfirmationModal, + type Column +} from "@/components/shared"; import { documentService } from "@/services/document-service"; import type { DocumentCategory } from "@/types/document"; import { showToast } from "@/utils/toast"; +import { Plus, Eye, Edit, Trash2 } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const categorySchema = z.object({ + name: z.string().min(1, "Category name is required"), + code: z.string().min(1, "Code is required").max(10, "Code must be 10 characters or less"), + description: z.string().optional(), + reviewFrequency: z.string().min(1, "Review frequency is required"), + retentionYears: z.string().min(1, "Retention years is required"), + requiresTraining: z.boolean().optional(), + parentId: z.string().optional(), +}); + +type CategoryFormData = z.infer; const DocumentCategories = (): ReactElement => { - const navigate = useNavigate(); + // const navigate = useNavigate(); const [categories, setCategories] = useState([]); const [isLoading, setIsLoading] = useState(true); - const [name, setName] = useState(""); - const [code, setCode] = useState(""); - const [description, setDescription] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isViewModalOpen, setIsViewModalOpen] = useState(false); + const [editingCategory, setEditingCategory] = useState(null); + const [viewingCategory, setViewingCategory] = useState(null); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [categoryToDelete, setCategoryToDelete] = useState(null); + const [isDeleting, setIsDeleting] = useState(false); + + const { + register, + handleSubmit, + control, + reset, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(categorySchema), + defaultValues: { + name: "", + code: "", + description: "", + reviewFrequency: "12", + retentionYears: "7", + requiresTraining: false, + parentId: "", + }, + }); const loadCategories = async (): Promise => { try { @@ -33,77 +84,145 @@ const DocumentCategories = (): ReactElement => { void loadCategories(); }, []); + const handleEdit = (category: DocumentCategory) => { + setEditingCategory(category); + setValue("name", category.name); + setValue("code", category.code); + setValue("description", category.description || ""); + setValue("reviewFrequency", category.review_frequency_months?.toString() || "12"); + setValue("retentionYears", category.retention_years?.toString() || "7"); + setValue("requiresTraining", !!category.requires_training); + setValue("parentId", category.parent_id || ""); + setIsModalOpen(true); + }; + + const handleView = (category: DocumentCategory) => { + setViewingCategory(category); + setIsViewModalOpen(true); + }; + + const handleDeleteClick = (category: DocumentCategory) => { + setCategoryToDelete(category); + setIsDeleteModalOpen(true); + }; + + const handleConfirmDelete = async () => { + if (!categoryToDelete) return; + try { + setIsDeleting(true); + await documentService.deleteCategory(categoryToDelete.id); + showToast.success("Category deleted successfully"); + setIsDeleteModalOpen(false); + setCategoryToDelete(null); + await loadCategories(); + } catch (err: any) { + showToast.error( + err?.response?.data?.error?.message || "Failed to delete category", + ); + } finally { + setIsDeleting(false); + } + }; + const columns: Column[] = useMemo( () => [ - { key: "name", label: "Name" }, - { key: "code", label: "Code" }, - { - key: "description", - label: "Description", - render: (category) => category.description || "-", + { + key: "name", + label: "Name", + render: (cat) => {cat.name} + }, + { + key: "code", + label: "Code", + render: (cat) => {cat.code} }, { key: "review_frequency_months", - label: "Review (months)", + label: "Review Frequency", render: (category) => - category.review_frequency_months?.toString() || "-", + category.review_frequency_months ? `${category.review_frequency_months} months` : "-", + }, + { + key: "parent_id", + label: "Parent Category", + render: (category) => { + const parent = categories.find(c => c.id === category.parent_id); + return {parent ? parent.name : "-"}; + } }, { key: "retention_years", - label: "Retention (years)", - render: (category) => category.retention_years?.toString() || "-", + label: "Retention", + render: (category) => category.retention_years ? `${category.retention_years} years` : "-", + }, + { + key: "requires_training", + label: "Requires Training", + render: (category) => ( +
+
+
+
+
+ ) + }, + { + key: "description", + label: "Description", + render: (category) => {category.description || "-"}, }, { key: "actions", label: "Actions", align: "right", render: (category) => ( - + handleView(category), icon: }, + { label: "Edit Category", onClick: () => handleEdit(category), icon: }, + { label: "Delete", onClick: () => handleDeleteClick(category), icon: , variant: "danger" }, + ]} + /> ), }, ], - [], + [categories], ); - const onCreateCategory = async (event: React.FormEvent): Promise => { - event.preventDefault(); - if (!name.trim() || !code.trim()) { - showToast.error("Name and code are required"); - return; - } - + const onFormSubmit = async (data: CategoryFormData): Promise => { try { setIsSubmitting(true); - await documentService.createCategory({ - name: name.trim(), - code: code.trim().toUpperCase(), - description: description.trim() || undefined, - }); - showToast.success("Category created"); - setName(""); - setCode(""); - setDescription(""); + const payload = { + name: data.name.trim(), + code: data.code.trim().toUpperCase(), + description: data.description?.trim() || undefined, + review_frequency_months: parseInt(data.reviewFrequency), + retention_years: parseInt(data.retentionYears), + requires_training: data.requiresTraining, + parent_id: data.parentId || null, + }; + + if (editingCategory) { + await documentService.updateCategory(editingCategory.id, payload); + showToast.success("Category updated"); + } else { + await documentService.createCategory(payload); + showToast.success("Category created"); + } + + setIsModalOpen(false); + reset(); + setEditingCategory(null); await loadCategories(); } catch (err: any) { showToast.error( - err?.response?.data?.error?.message || "Failed to create category", + err?.response?.data?.error?.message || "Failed to process category", ); } finally { setIsSubmitting(false); @@ -113,60 +232,18 @@ const DocumentCategories = (): ReactElement => { return ( { setEditingCategory(null); reset(); setIsModalOpen(true); }}> + + Create Category + + ), }} >
- -
- setName(e.target.value)} - placeholder="e.g. SOP" - /> - setCode(e.target.value)} - placeholder="e.g. SOP" - /> - setDescription(e.target.value)} - placeholder="Optional" - /> -
-
- - {isSubmitting ? "Saving..." : "Add Category"} - - -
-
{ />
+ + { setIsModalOpen(false); setEditingCategory(null); }} + title={editingCategory ? "Update Document Category" : "Create Document Category"} + maxWidth="lg" + > +
+

+ Add a document category with review, retention, and training requirements. +

+ +
+ + + + +
+ ( + + )} + /> + ( + + )} + /> +
+ + ( + !editingCategory || c.id !== editingCategory.id) + .map(c => ({ value: c.id, label: c.name })) + ]} + placeholder="Select parent category" + /> + )} + /> + + + +
+
+ +

Users must acknowledge documents in this category

+
+ ( +
field.onChange(!field.value)} + > +
+
+ )} + /> +
+
+ +
+ + + {isSubmitting ? "Processing..." : editingCategory ? "Update Category" : "Create Category"} + +
+ + + + {/* View Modal */} + { setIsViewModalOpen(false); setViewingCategory(null); }} + title="Document Category Details" + maxWidth="lg" + > +
+
+
+ +

{viewingCategory?.name}

+
+
+ +

+ {viewingCategory?.code} +

+
+
+ +

{viewingCategory?.review_frequency_months} months

+
+
+ +

{viewingCategory?.retention_years} years

+
+
+ +

+ {categories.find(c => c.id === viewingCategory?.parent_id)?.name || "None (Root Category)"} +

+
+
+
+ +

{viewingCategory?.description || "No description provided."}

+
+
+
+

Requires Training

+

Training acknowledgement is {viewingCategory?.requires_training ? "enabled" : "disabled"} for this category.

+
+
+
+
+
+
+ +
+
+ + + { + setIsDeleteModalOpen(false); + setCategoryToDelete(null); + }} + onConfirm={handleConfirmDelete} + title="Delete Document Category" + message="Are you sure you want to delete this category? This action cannot be undone and will fail if the category is currently associated with documents." + itemName={categoryToDelete?.name || ""} + isLoading={isDeleting} + /> ); }; export default DocumentCategories; - diff --git a/src/pages/tenant/Documents.tsx b/src/pages/tenant/Documents.tsx index 63433de..1b8f183 100644 --- a/src/pages/tenant/Documents.tsx +++ b/src/pages/tenant/Documents.tsx @@ -10,7 +10,7 @@ import { } from "@/components/shared"; import { documentService } from "@/services/document-service"; import type { DocumentCategory, DocumentSummary } from "@/types/document"; -import { Plus } from "lucide-react"; +import { Plus, Search } from "lucide-react"; const formatDate = (value?: string | null): string => { if (!value) return "-"; @@ -183,81 +183,132 @@ const Documents = (): ReactElement => { title: "Document List", description: "Manage controlled documents, track versions and open document details.", - tabs: [ - { label: "Document List", path: "/tenant/documents" }, - { label: "Create Document", path: "/tenant/documents/create" }, - { label: "Category Management", path: "/tenant/documents/categories" }, - ], + action: ( +
+ {/* */} + navigate("/tenant/documents/create")}> + + New Document + +
+ ), }} >
-
-
-
- ({ - value: status.code, - label: status.name, - }))} - value={statusFilter} - onChange={(value) => { - setStatusFilter(value as string | null); - setCurrentPage(1); - }} - placeholder="All" - /> - ({ - value: category.id, - label: category.name, - }))} - value={categoryFilter} - onChange={(value) => { - setCategoryFilter(value as string | null); - setCurrentPage(1); - }} - placeholder="All" - /> - ({ - value: type.code, - label: type.name, - }))} - value={typeFilter} - onChange={(value) => { - setTypeFilter(value as string | null); - setCurrentPage(1); - }} - placeholder="All" - /> +
+
+ {/* Left side: Search and Filters */} +
+ {/* Search Bar */} +
+
+ +
+ { + setSearch(e.target.value); + setCurrentPage(1); + }} + placeholder="Search by name, ID..." + className="h-10 w-full pl-9 pr-3 bg-white border border-[rgba(0,0,0,0.08)] rounded-md text-sm focus:outline-none focus:ring-1 focus:ring-[#112868]/10 transition-all" + /> +
+ + {/* Filters */} +
+ ({ + value: status.code, + label: status.name, + }))} + value={statusFilter} + onChange={(value) => { + setStatusFilter(value as string | null); + setCurrentPage(1); + }} + placeholder="All" + /> + + ({ + value: category.id, + label: category.name, + }))} + value={categoryFilter} + onChange={(value) => { + setCategoryFilter(value as string | null); + setCurrentPage(1); + }} + placeholder="All" + /> + + ({ + value: type.code, + label: type.name, + }))} + value={typeFilter} + onChange={(value) => { + setTypeFilter(value as string | null); + setCurrentPage(1); + }} + placeholder="All" + /> + + {/* {}} + placeholder="All" + /> + + } + options={[ + { value: "more", label: "More Filters..." }, + ]} + value={null} + onChange={() => {}} + placeholder="More" + /> */} +
-
+ + {/* Right side: Clear Filters */} +
- navigate("/tenant/documents/create")}> - - New Document -
- { - setSearch(e.target.value); - setCurrentPage(1); - }} - placeholder="Search by title, description or document number" - className="h-10 w-full max-w-xl px-3 border border-[rgba(0,0,0,0.08)] rounded-md text-sm" - />
{ + if (!value) return "-"; + return new Date(value).toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); +}; + +const getDaysRemaining = (dateStr?: string | null): number | null => { + if (!dateStr) return null; + const target = new Date(dateStr); + const now = new Date(); + const diffTime = target.getTime() - now.getTime(); + return Math.ceil(diffTime / (1000 * 60 * 60 * 24)); +}; + +const DocumentsDueForReview = (): ReactElement => { + const navigate = useNavigate(); + const [documents, setDocuments] = useState([]); + const [daysFilter, setDaysFilter] = useState("30"); + const [isLoading, setIsLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const loadDueDocuments = async (): Promise => { + try { + setIsLoading(true); + setError(null); + const response = await documentService.getDueForReview(parseInt(daysFilter)); + setDocuments(response.data || []); + } catch (err: any) { + setError( + err?.response?.data?.error?.message || "Failed to load documents due for review", + ); + } finally { + setIsLoading(false); + } + }; + + void loadDueDocuments(); + }, [daysFilter]); + + const columns: Column[] = useMemo( + () => [ + { + key: "document_number", + label: "Document No", + render: (doc) => ( + + ), + }, + { + key: "title", + label: "Title", + render: (doc) => {doc.title}, + }, + { + key: "category", + label: "Category", + render: (doc) => {doc.category || "-"}, + }, + { + key: "owner", + label: "Owner", + render: (doc) => {doc.owner || "-"}, + }, + { + key: "next_review_date", + label: "Review Due Date", + render: (doc) => ( + {formatDate(doc.next_review_date)} + ), + }, + { + key: "days_remaining", + label: "Status", + render: (doc) => { + const days = getDaysRemaining(doc.next_review_date); + if (days === null) return "-"; + + let colorClass = "bg-emerald-100 text-emerald-700"; + if (days <= 0) colorClass = "bg-rose-100 text-rose-700"; + else if (days <= 7) colorClass = "bg-amber-100 text-amber-700"; + + return ( + + {days <= 0 ? "Overdue" : `${days} days left`} + + ); + }, + }, + { + key: "actions", + label: "Actions", + render: (doc) => ( + + ), + }, + ], + [navigate], + ); + + return ( + +
+
+
+ +

Heads up: These documents need attention soon.

+
+ setDaysFilter(value as string)} + /> +
+ + doc.id} + emptyMessage="No documents due for review within this period." + isLoading={isLoading} + error={error} + /> +
+
+ ); +}; + +export default DocumentsDueForReview; diff --git a/src/pages/tenant/ViewDocument.tsx b/src/pages/tenant/ViewDocument.tsx index 133ef7f..0561683 100644 --- a/src/pages/tenant/ViewDocument.tsx +++ b/src/pages/tenant/ViewDocument.tsx @@ -10,7 +10,10 @@ import { SecondaryButton, type Column, } from "@/components/shared"; -import { documentService, type FileAttachmentItem } from "@/services/document-service"; +import { + documentService, + type FileAttachmentItem, +} from "@/services/document-service"; import { workflowService } from "@/services/workflow-service"; import type { DocumentDetail, DocumentVersion } from "@/types/document"; import type { WorkflowInstance } from "@/types/workflow"; @@ -54,10 +57,10 @@ const ViewDocument = (): ReactElement => { const [versions, setVersions] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); - const [activeTab, setActiveTab] = useState<"overview" | "version-history" | "workflow-history">( - "overview", - ); - const [workflowInstances, setWorkflowInstances] = useState([]); + const [activeTab, setActiveTab] = useState< + "overview" | "version-history" | "workflow-history" + >("overview"); + const [workflowHistory, setWorkflowHistory] = useState([]); const [isHistoryLoading, setIsHistoryLoading] = useState(false); const [activeAction, setActiveAction] = useState(null); @@ -72,13 +75,18 @@ const ViewDocument = (): ReactElement => { const [showNewVersionForm, setShowNewVersionForm] = useState(false); const [newVersionContent, setNewVersionContent] = useState(""); const [newVersionContentHtml, setNewVersionContentHtml] = useState(""); - const [newVersionChangeReason, setNewVersionChangeReason] = useState("minor_edit"); + const [newVersionChangeReason, setNewVersionChangeReason] = + useState("minor_edit"); const [newVersionChangeSummary, setNewVersionChangeSummary] = useState(""); const [isMajorVersion, setIsMajorVersion] = useState(false); - const [versionErrors, setVersionErrors] = useState>({}); + const [versionErrors, setVersionErrors] = useState>( + {}, + ); + const [actionErrors, setActionErrors] = useState>({}); const [isVersionSaving, setIsVersionSaving] = useState(false); const [showWorkflowTracker, setShowWorkflowTracker] = useState(false); - const [workflowInstance, setWorkflowInstance] = useState(null); + const [workflowInstance, setWorkflowInstance] = + useState(null); const [isWorkflowLoading, setIsWorkflowLoading] = useState(false); // File attachment fields for new version @@ -86,10 +94,13 @@ const ViewDocument = (): ReactElement => { const [versionSelectedFileId, setVersionSelectedFileId] = useState(""); const [versionFileName, setVersionFileName] = useState(""); const [transitionComment, setTransitionComment] = useState(""); - const [selectedWorkflowAction, setSelectedWorkflowAction] = useState(null); + const [selectedWorkflowAction, setSelectedWorkflowAction] = + useState(null); const [isTransitioning, setIsTransitioning] = useState(false); const [versionFilePath, setVersionFilePath] = useState(""); - const [versionFileSize, setVersionFileSize] = useState(undefined); + const [versionFileSize, setVersionFileSize] = useState( + undefined, + ); const [versionMimeType, setVersionMimeType] = useState(""); const [versionFileHash, setVersionFileHash] = useState(""); const [isLoadingVersionFiles, setIsLoadingVersionFiles] = useState(false); @@ -109,7 +120,8 @@ const ViewDocument = (): ReactElement => { setVersions(versionsRes.data || []); } catch (err: any) { const message = - err?.response?.data?.error?.message || "Failed to load document details"; + err?.response?.data?.error?.message || + "Failed to load document details"; setError(message); showToast.error(message); } finally { @@ -128,16 +140,28 @@ const ViewDocument = (): ReactElement => { }, [activeTab, id]); const loadWorkflowHistory = async (): Promise => { + if (!document?.workflow_instance_id) { + setWorkflowHistory([]); + return; + } + try { setIsHistoryLoading(true); - const res = await workflowService.listInstances({ - entity_type: "document", - entity_id: id, - limit: 100, - offset: 0, - }); - setWorkflowInstances(res.data || []); + const res = await workflowService.getInstanceHistory( + document.workflow_instance_id, + ); + const history = res.data || []; + + // Sort history descending by default + const sortedHistory = [...history].sort( + (a: any, b: any) => + new Date(b.performed_at).getTime() - + new Date(a.performed_at).getTime(), + ); + + setWorkflowHistory(sortedHistory); } catch { + setWorkflowHistory([]); showToast.error("Failed to load workflow history"); } finally { setIsHistoryLoading(false); @@ -177,7 +201,9 @@ const ViewDocument = (): ReactElement => { setVersionFileHash(selected.checksum); try { - showToast.success(`Extracting content from "${selected.original_name}"...`); + showToast.success( + `Extracting content from "${selected.original_name}"...`, + ); const res = await documentService.getFileContent(fileId); if (res.success && res.data) { setNewVersionContentHtml(res.data.html || ""); @@ -187,11 +213,14 @@ const ViewDocument = (): ReactElement => { showToast.error("Failed to extract file content"); } } catch (err: any) { - const msg = err?.response?.data?.error?.message || "Failed to extract file content"; + const msg = + err?.response?.data?.error?.message || "Failed to extract file content"; showToast.error(msg); const html = `

Document sourced from file: ${selected.original_name}

`; setNewVersionContentHtml(html); - setNewVersionContent(`Document sourced from file: ${selected.original_name}`); + setNewVersionContent( + `Document sourced from file: ${selected.original_name}`, + ); } }; @@ -217,7 +246,9 @@ const ViewDocument = (): ReactElement => { const res = await workflowService.getInstance(targetId); setWorkflowInstance(res.data); } catch (err: any) { - const msg = err?.response?.data?.error?.message || "Failed to load workflow tracker"; + const msg = + err?.response?.data?.error?.message || + "Failed to load workflow tracker"; showToast.error(msg); setShowWorkflowTracker(false); } finally { @@ -232,6 +263,7 @@ const ViewDocument = (): ReactElement => { setEffectiveDate(""); setSignatureId(""); setWorkflowOptions([]); + setActionErrors({}); }; const openActionModal = async (action: DocumentAction): Promise => { @@ -265,28 +297,28 @@ const ViewDocument = (): ReactElement => { const handleAction = async (action: DocumentAction): Promise => { if (!id) return; + setActionErrors({}); + const localErrors: Record = {}; + if (action === "submit" && !workflowDefinitionId) { showToast.error("workflow_definition_id is required"); return; } if (action === "reject" && !actionComment.trim()) { - showToast.error("Reason is required for reject"); - return; + localErrors.comment = "Reason is required for reject"; } if (action === "effective" && !effectiveDate) { - showToast.error("Effective date is required"); - return; - } - if (action === "effective" && !signatureId.trim()) { - showToast.error("signature_id is required"); - return; + localErrors.effective_date = "Effective date is required"; } if (action === "obsolete" && !actionComment.trim()) { - showToast.error("Reason is required to obsolete"); - return; + localErrors.comment = "Reason is required to obsolete"; } if (action === "checkout" && !actionComment.trim()) { - showToast.error("Reason is required for checkout"); + localErrors.comment = "Reason is required for checkout"; + } + + if (Object.keys(localErrors).length > 0) { + setActionErrors(localErrors); return; } @@ -294,12 +326,17 @@ const ViewDocument = (): ReactElement => { setIsActionLoading(true); if (action === "submit") await documentService.submitForReview(id, workflowDefinitionId); - if (action === "approve") await documentService.approve(id, actionComment); + if (action === "approve") + await documentService.approve(id, actionComment); if (action === "reject") { await documentService.reject(id, actionComment.trim()); } if (action === "effective") { - await documentService.makeEffective(id, effectiveDate, signatureId.trim()); + await documentService.makeEffective( + id, + effectiveDate, + signatureId.trim(), + ); } if (action === "obsolete") { await documentService.makeObsolete(id, actionComment.trim()); @@ -322,7 +359,7 @@ const ViewDocument = (): ReactElement => { const handleCreateVersion = async (): Promise => { if (!id) return; - + // Clear previous errors setVersionErrors({}); const localErrors: Record = {}; @@ -376,7 +413,9 @@ const ViewDocument = (): ReactElement => { const handleWorkflowTransition = async (): Promise => { if (!workflowInstance || !selectedWorkflowAction) return; - const pendingTask = workflowInstance.tasks.find((t) => t.status === "pending"); + const pendingTask = workflowInstance.tasks.find( + (t) => t.status === "pending", + ); if (!pendingTask) { showToast.error("No pending task found for this workflow instance"); return; @@ -395,20 +434,21 @@ const ViewDocument = (): ReactElement => { comments: transitionComment.trim() || undefined, }); showToast.success(`Action "${selectedWorkflowAction.name}" completed`); - + // Refresh workflow instance data const res = await workflowService.getInstance(workflowInstance.id); setWorkflowInstance(res.data); - + // Reset transition state setSelectedWorkflowAction(null); setTransitionComment(""); - + // Also refresh document data as it might have changed status await refreshData(); } catch (err: any) { showToast.error( - err?.response?.data?.error?.message || "Failed to complete workflow action", + err?.response?.data?.error?.message || + "Failed to complete workflow action", ); } finally { setIsTransitioning(false); @@ -440,8 +480,6 @@ const ViewDocument = (): ReactElement => { }, ]; - - return ( { pageHeader={{ title: document?.title || "View Document", description: "View document metadata and version history.", - tabs: [ - { label: "Document List", path: "/tenant/documents" }, - { label: "Create Document", path: "/tenant/documents/create" }, - { label: "Category Management", path: "/tenant/documents/categories" }, - ], - }} - > -
-
-
-
-

- {document?.title || "Document Detail"} -

-

- {document?.document_number || "-"} -

-
-
- {document?.status === "draft" && ( -
- - -
- )} - {document?.status === "in_review" && ( - <> - - - - - )} - {document?.status === "approved" && ( + action: ( +
+ {document?.status === "draft" && ( +
+ + +
+ )} + {document?.status === "in_review" && ( + <> + + + + + )} + {document?.status === "approved" && ( +
- )} - {document?.status === "effective" && ( - )} +
+ )} + {document?.status === "effective" && ( + + )} +
+ ), + }} + > +
+
+
+
+

+ Document Properties +

+

+ Details and classification for{" "} + {document?.document_number || "-"} +

@@ -588,7 +633,9 @@ const ViewDocument = (): ReactElement => {
Document Number: -

{document.document_number}

+

+ {document.document_number} +

Title: @@ -596,19 +643,27 @@ const ViewDocument = (): ReactElement => {
Category: -

{document.category?.name || "-"}

+

+ {document.category?.name || "-"} +

Department: -

{document.department || "-"}

+

+ {document.department || "-"} +

Effective Date: -

{formatDateTime(document.effective_date)}

+

+ {formatDateTime(document.effective_date)} +

Next Review Date: -

{formatDateTime(document.next_review_date)}

+

+ {formatDateTime(document.next_review_date)} +

Tags: @@ -620,15 +675,21 @@ const ViewDocument = (): ReactElement => {
-

Document Content

+

+ Document Content +

{document.content_html ? (
) : ( -
{document.content || "-"}
+
+ {document.content || "-"} +
)}
@@ -652,13 +713,17 @@ const ViewDocument = (): ReactElement => {
{showNewVersionForm && (
-

Create New Version

+

+ Create New Version +

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

Select a file to extract and auto-fill content below. @@ -666,7 +731,9 @@ const ViewDocument = (): ReactElement => { void handleVersionFileSelect(val)} + onValueChange={(val) => + void handleVersionFileSelect(val) + } options={[ { value: "", label: "— None —" }, ...versionFiles.map((f) => ({ @@ -674,14 +741,34 @@ const ViewDocument = (): ReactElement => { label: `${f.original_name} (${f.file_size_formatted})`, })), ]} - placeholder={isLoadingVersionFiles ? "Loading files..." : "Select a file to attach"} + placeholder={ + isLoadingVersionFiles + ? "Loading files..." + : "Select a file to attach" + } /> {versionSelectedFileId && (

-

File: {versionFileName}

-

Type: {versionMimeType}

-

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

-

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

+

+ File:{" "} + {versionFileName} +

+

+ Type:{" "} + {versionMimeType} +

+

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

+

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

)}
@@ -690,11 +777,15 @@ const ViewDocument = (): ReactElement => { label="Document Content" value={newVersionContentHtml} required - minHeightClassName="min-h-[200px]" + minHeightClassName="h-[400px] overflow-y-auto" onChange={(html, text) => { setNewVersionContentHtml(html); setNewVersionContent(text); - if (text.trim()) setVersionErrors(prev => ({ ...prev, content: "" })); + if (text.trim()) + setVersionErrors((prev) => ({ + ...prev, + content: "", + })); }} error={versionErrors.content} /> @@ -705,13 +796,20 @@ const ViewDocument = (): ReactElement => { options={[ { value: "minor_edit", label: "minor_edit" }, { value: "correction", label: "correction" }, - { value: "regulatory_update", label: "regulatory_update" }, + { + 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: "" })); + if (val) + setVersionErrors((prev) => ({ + ...prev, + change_reason: "", + })); }} placeholder="Select change reason" error={versionErrors.change_reason} @@ -721,10 +819,15 @@ const ViewDocument = (): ReactElement => { id="major-version" type="checkbox" checked={isMajorVersion} - onChange={(event) => setIsMajorVersion(event.target.checked)} + onChange={(event) => + setIsMajorVersion(event.target.checked) + } className="w-4 h-4" /> -
@@ -736,13 +839,17 @@ const ViewDocument = (): ReactElement => {