refactor: reorganize sidebar navigation menu structure and update group item rendering logic

This commit is contained in:
Yashwin 2026-04-09 18:01:52 +05:30
parent ccbda2c0ae
commit 68173ac28a
10 changed files with 288 additions and 121 deletions

View File

@ -12,7 +12,6 @@ import {
Shield, Shield,
BadgeCheck, BadgeCheck,
GitBranch, GitBranch,
Briefcase,
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
Bell, Bell,
@ -73,18 +72,6 @@ const tenantAdminPlatformMenu: MenuItem[] = [
path: "/tenant/roles", path: "/tenant/roles",
requiredPermission: { resource: "roles" }, requiredPermission: { resource: "roles" },
}, },
{
icon: Briefcase,
label: "My Tasks",
path: "/tenant/tasks",
requiredPermission: { resource: "workflow" },
},
{
icon: Users,
label: "Users",
path: "/tenant/users",
requiredPermission: { resource: "users" },
},
{ {
icon: Building2, icon: Building2,
label: "Departments", label: "Departments",
@ -97,12 +84,6 @@ const tenantAdminPlatformMenu: MenuItem[] = [
path: "/tenant/designations", path: "/tenant/designations",
requiredPermission: { resource: "designations" }, requiredPermission: { resource: "designations" },
}, },
{
icon: GitBranch,
label: "Workflow Definitions",
path: "/tenant/workflow-definitions",
requiredPermission: { resource: "workflow" },
},
{ {
icon: Users, icon: Users,
label: "Suppliers", label: "Suppliers",
@ -110,23 +91,70 @@ const tenantAdminPlatformMenu: MenuItem[] = [
requiredPermission: { resource: "supplier" }, requiredPermission: { resource: "supplier" },
}, },
{ {
icon: FileText, icon: Users,
label: "Document Services", label: "Users",
path: "/tenant/users",
requiredPermission: { resource: "users" },
},
];
const tenantAdminPlatformServiceMenu: MenuItem[] = [
{
icon: GitBranch,
label: "Workflows",
isGroup: true, isGroup: true,
children: [ children: [
{ label: "Document Lists", path: "/tenant/documents", requiredPermission: { resource: "document" } }, {
{ label: "Create Document", path: "/tenant/documents/create", requiredPermission: { resource: "document", action: "create" } }, label: "Definitions",
{ label: "Categories", path: "/tenant/documents/categories", requiredPermission: { resource: "document" } }, path: "/tenant/workflows/definitions",
{ label: "Due for Review", path: "/tenant/documents/due-for-review", requiredPermission: { resource: "document" } }, requiredPermission: { resource: "workflow" },
},
{
label: "Tasks",
path: "/tenant/workflows/tasks",
requiredPermission: { resource: "workflow" },
},
],
requiredPermission: { resource: "workflow" },
},
{
icon: FileText,
label: "Documents",
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" }, requiredPermission: { resource: "document" },
}, },
{ {
icon: Paperclip, icon: Paperclip,
label: "File Attachment Services", label: "File Attachments",
isGroup: true, isGroup: true,
children: [ children: [
{ label: "Files List", path: "/tenant/files", requiredPermission: { resource: "files" } }, {
label: "Files List",
path: "/tenant/files",
requiredPermission: { resource: "files" },
},
], ],
requiredPermission: { resource: "files" }, requiredPermission: { resource: "files" },
}, },
@ -159,7 +187,7 @@ const GroupMenuItem = ({
location, location,
isSuperAdmin, isSuperAdmin,
theme, theme,
onClose onClose,
}: { }: {
item: MenuItem; item: MenuItem;
childrenItems: any[]; childrenItems: any[];
@ -172,12 +200,18 @@ const GroupMenuItem = ({
// Special handling for Document Lists to NOT show as active when sub-actions are active // Special handling for Document Lists to NOT show as active when sub-actions are active
if (path === "/tenant/documents") { if (path === "/tenant/documents") {
const subActions = ["/create", "/categories", "/due-for-review", "/edit"]; const subActions = ["/create", "/categories", "/due-for-review", "/edit"];
const isSubActionActive = subActions.some(sub => location.pathname.startsWith(path + sub)); const isSubActionActive = subActions.some((sub) =>
location.pathname.startsWith(path + sub),
);
if (isSubActionActive) return false; if (isSubActionActive) return false;
} }
return location.pathname === path || location.pathname.startsWith(`${path}/`); return (
location.pathname === path || location.pathname.startsWith(`${path}/`)
);
}; };
const isAnyChildActive = childrenItems.some(child => isChildActive(child.path)); const isAnyChildActive = childrenItems.some((child) =>
isChildActive(child.path),
);
const [isExpanded, setIsExpanded] = useState(isAnyChildActive); const [isExpanded, setIsExpanded] = useState(isAnyChildActive);
useEffect(() => { useEffect(() => {
@ -192,20 +226,39 @@ const GroupMenuItem = ({
onClick={() => setIsExpanded(!isExpanded)} onClick={() => setIsExpanded(!isExpanded)}
className={cn( className={cn(
"flex items-center justify-between gap-2.5 px-3 py-2 rounded-md transition-all min-h-[44px]", "flex items-center justify-between gap-2.5 px-3 py-2 rounded-md transition-all min-h-[44px]",
isAnyChildActive ? "shadow-[0px_2px_8px_0px_rgba(15,23,42,0.15)]" : "text-[#0f1724] hover:bg-gray-50" isAnyChildActive
? "shadow-[0px_2px_8px_0px_rgba(15,23,42,0.15)]"
: "text-[#0f1724] hover:bg-gray-50",
)} )}
style={isAnyChildActive ? { style={
backgroundColor: !isSuperAdmin && theme?.primary_color ? theme.primary_color : "#112868", isAnyChildActive
color: !isSuperAdmin && theme?.secondary_color ? theme.secondary_color : "#23dce1" ? {
} : undefined} backgroundColor:
!isSuperAdmin && theme?.primary_color
? theme.primary_color
: "#112868",
color:
!isSuperAdmin && theme?.secondary_color
? theme.secondary_color
: "#23dce1",
}
: undefined
}
> >
<div className="flex items-center gap-2.5"> <div className="flex items-center gap-2.5">
<Icon className="w-4 h-4 shrink-0" /> <Icon className="w-4 h-4 shrink-0" />
<span className="text-xs md:text-xs lg:text-[13px] font-medium truncate" title={item.label}> <span
className="text-xs md:text-xs lg:text-[13px] font-medium truncate"
title={item.label}
>
{item.label} {item.label}
</span> </span>
</div> </div>
{isExpanded ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />} {isExpanded ? (
<ChevronDown className="w-3.5 h-3.5" />
) : (
<ChevronRight className="w-3.5 h-3.5" />
)}
</button> </button>
{isExpanded && ( {isExpanded && (
@ -225,11 +278,22 @@ const GroupMenuItem = ({
"flex items-center px-4 py-2 rounded-r-md text-[13px] font-medium transition-all", "flex items-center px-4 py-2 rounded-r-md text-[13px] font-medium transition-all",
isActive isActive
? "text-[#112868] font-bold bg-gray-50" ? "text-[#112868] font-bold bg-gray-50"
: "text-[#475569] hover:text-[#0f1724] hover:bg-gray-50" : "text-[#475569] hover:text-[#0f1724] hover:bg-gray-50",
)} )}
style={isActive ? { color: !isSuperAdmin && theme?.primary_color ? theme.primary_color : "#112868" } : undefined} style={
isActive
? {
color:
!isSuperAdmin && theme?.primary_color
? theme.primary_color
: "#112868",
}
: undefined
}
> >
<span className="truncate" title={child.label}>{child.label}</span> <span className="truncate" title={child.label}>
{child.label}
</span>
</Link> </Link>
); );
})} })}
@ -365,6 +429,9 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
const platformMenu = filterMenuItems( const platformMenu = filterMenuItems(
isSuperAdmin ? superAdminPlatformMenu : tenantAdminPlatformMenu, isSuperAdmin ? superAdminPlatformMenu : tenantAdminPlatformMenu,
); );
const platformServiceMenu = filterMenuItems(
isSuperAdmin ? [] : tenantAdminPlatformServiceMenu,
);
const systemMenu = filterMenuItems( const systemMenu = filterMenuItems(
isSuperAdmin ? superAdminSystemMenu : tenantAdminSystemMenu, isSuperAdmin ? superAdminSystemMenu : tenantAdminSystemMenu,
); );
@ -386,7 +453,8 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
<div className="flex flex-col gap-1 mt-1"> <div className="flex flex-col gap-1 mt-1">
{items.map((item) => { {items.map((item) => {
if (item.isGroup) { if (item.isGroup) {
const children = (item as any)._filteredChildren || item.children || []; const children =
(item as any)._filteredChildren || item.children || [];
return ( return (
<GroupMenuItem <GroupMenuItem
key={item.label} key={item.label}
@ -404,7 +472,8 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
const isTenantDashboardPath = item.path === "/tenant"; const isTenantDashboardPath = item.path === "/tenant";
const isActive = isTenantDashboardPath const isActive = isTenantDashboardPath
? location.pathname === "/tenant" ? location.pathname === "/tenant"
: item.path && (location.pathname === item.path || : item.path &&
(location.pathname === item.path ||
location.pathname.startsWith(`${item.path}/`)); location.pathname.startsWith(`${item.path}/`));
return ( return (
<Link <Link
@ -438,7 +507,10 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
} }
> >
<Icon className="w-4 h-4 shrink-0" /> <Icon className="w-4 h-4 shrink-0" />
<span className="text-xs md:text-xs lg:text-[13px] font-medium truncate" title={item.label}> <span
className="text-xs md:text-xs lg:text-[13px] font-medium truncate"
title={item.label}
>
{item.label} {item.label}
</span> </span>
</Link> </Link>
@ -513,6 +585,7 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
<div className="flex-1 overflow-y-auto pr-1 flex flex-col gap-6 custom-scrollbar"> <div className="flex-1 overflow-y-auto pr-1 flex flex-col gap-6 custom-scrollbar">
{/* Platform Menu */} {/* Platform Menu */}
<MenuSection title="Platform" items={platformMenu} /> <MenuSection title="Platform" items={platformMenu} />
<MenuSection title="Platform Services" items={platformServiceMenu} />
{/* System Menu */} {/* System Menu */}
<MenuSection title="System" items={systemMenu} /> <MenuSection title="System" items={systemMenu} />
@ -551,7 +624,11 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
src={logoUrl} src={logoUrl}
alt="Logo" alt="Logo"
className="h-9 w-auto max-w-[180px] object-contain" className="h-9 w-auto max-w-[180px] object-contain"
fallback={<div className="w-9 h-9 rounded-[10px] flex items-center justify-center shadow-[0px_4px_12px_0px_rgba(15,23,42,0.1)] shrink-0 bg-[#112868]"><Shield className="w-6 h-6 text-white" strokeWidth={1.67} /></div>} fallback={
<div className="w-9 h-9 rounded-[10px] flex items-center justify-center shadow-[0px_4px_12px_0px_rgba(15,23,42,0.1)] shrink-0 bg-[#112868]">
<Shield className="w-6 h-6 text-white" strokeWidth={1.67} />
</div>
}
/> />
) : null} ) : null}
<div <div
@ -593,6 +670,9 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
{platformMenu.length > 0 && ( {platformMenu.length > 0 && (
<MenuSection title="Platform" items={platformMenu} /> <MenuSection title="Platform" items={platformMenu} />
)} )}
{platformServiceMenu.length > 0 && (
<MenuSection title="Platform Services" items={platformServiceMenu} />
)}
{/* System Menu */} {/* System Menu */}
{systemMenu.length > 0 && ( {systemMenu.length > 0 && (

View File

@ -78,7 +78,7 @@ export const NotificationBell = () => {
// Special handling for tasks as requested - redirect to My Tasks tab // Special handling for tasks as requested - redirect to My Tasks tab
if (['workflow', 'training'].includes(notification.category || '')) { if (['workflow', 'training'].includes(notification.category || '')) {
navigate('/tenant/tasks'); navigate('/tenant/workflows/tasks');
setIsOpen(false); setIsOpen(false);
return; return;
} }

View File

@ -41,7 +41,7 @@ export const NotificationProvider = ({ children }: NotificationProviderProps) =>
label: 'View', label: 'View',
onClick: () => { onClick: () => {
if (['workflow', 'training'].includes(notification.category || '')) { if (['workflow', 'training'].includes(notification.category || '')) {
navigate('/tenant/tasks'); navigate('/tenant/workflows/tasks');
} else { } else {
navigate(notification.action_url); navigate(notification.action_url);
} }

View File

@ -335,7 +335,7 @@ const Dashboard = (): ReactElement => {
<div className="flex justify-between items-center mb-5"> <div className="flex justify-between items-center mb-5">
<h2 className="text-[16px] font-bold text-[#111827] tracking-tight">My Tasks</h2> <h2 className="text-[16px] font-bold text-[#111827] tracking-tight">My Tasks</h2>
<button <button
onClick={() => navigate('/tenant/tasks')} onClick={() => navigate('/tenant/workflows/tasks')}
className="text-[11px] font-bold text-[#084cc8] hover:underline" className="text-[11px] font-bold text-[#084cc8] hover:underline"
> >
View all View all

View File

@ -33,6 +33,8 @@ import fileAttachmentService, {
type FileAttachment, type FileAttachment,
type CategoriesFilterOptions, type CategoriesFilterOptions,
} from "@/services/file-attachment-service"; } from "@/services/file-attachment-service";
import { moduleService } from "@/services/module-service";
import { FilterDropdown } from "@/components/shared";
import { FileUploadModal } from "@/components/shared/FileUploadModal"; import { FileUploadModal } from "@/components/shared/FileUploadModal";
import type { RootState } from "@/store/store"; import type { RootState } from "@/store/store";
@ -285,7 +287,8 @@ const FilesList = (): ReactElement => {
// Filters // Filters
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [categoryFilter, setCategoryFilter] = useState<string | null>(null); const [categoryFilter, setCategoryFilter] = useState<string | null>(null);
const [moduleFilter, setModuleFilter] = useState<string | null>(null); const [moduleIdFilter, setModuleIdFilter] = useState<string | null>(null);
const [modules, setModules] = useState<{ id: string; name: string }[]>([]);
// Pagination // Pagination
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
@ -307,6 +310,12 @@ const FilesList = (): ReactElement => {
fileAttachmentService.getCategoriesFilterOptions().then((res) => { fileAttachmentService.getCategoriesFilterOptions().then((res) => {
setCategories(res.data.categories); setCategories(res.data.categories);
}).catch(() => {}); }).catch(() => {});
moduleService.getMyModules().then((res) => {
if (res.success) {
setModules(res.data.map(m => ({ id: m.id, name: m.name })));
}
}).catch(() => {});
}, []); }, []);
// ── Load files ── // ── Load files ──
@ -317,7 +326,7 @@ const FilesList = (): ReactElement => {
const res = await fileAttachmentService.list({ const res = await fileAttachmentService.list({
search: search.trim() || undefined, search: search.trim() || undefined,
category: categoryFilter || undefined, category: categoryFilter || undefined,
source_module: moduleFilter || undefined, source_module_id: moduleIdFilter || undefined,
limit, limit,
offset, offset,
}); });
@ -328,25 +337,12 @@ const FilesList = (): ReactElement => {
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [search, categoryFilter, moduleFilter, limit, offset]); }, [search, categoryFilter, moduleIdFilter, limit, offset]);
useEffect(() => { useEffect(() => {
void loadFiles(); void loadFiles();
}, [loadFiles]); }, [loadFiles]);
// ── Unique module values for filter ──
const moduleOptions = useMemo<DropOption[]>(() => {
const seen = new Set<string>();
const opts: DropOption[] = [];
files.forEach((f) => {
if (f.source_module && !seen.has(f.source_module)) {
seen.add(f.source_module);
opts.push({ value: f.source_module, label: f.source_module });
}
});
return opts;
}, [files]);
const categoryOptions = useMemo<DropOption[]>(() => const categoryOptions = useMemo<DropOption[]>(() =>
categories.map((c) => ({ value: c.category, label: c.category })), categories.map((c) => ({ value: c.category, label: c.category })),
[categories]); [categories]);
@ -372,7 +368,7 @@ const FilesList = (): ReactElement => {
const clearFilters = () => { const clearFilters = () => {
setSearch(""); setSearch("");
setCategoryFilter(null); setCategoryFilter(null);
setModuleFilter(null); setModuleIdFilter(null);
setCurrentPage(1); setCurrentPage(1);
}; };
@ -426,12 +422,16 @@ const FilesList = (): ReactElement => {
onChange={(v) => { setCategoryFilter(v); setCurrentPage(1); }} onChange={(v) => { setCategoryFilter(v); setCurrentPage(1); }}
/> />
{/* More Filters (Source Module) */} {/* Source Module filter */}
<FilterPill <FilterDropdown
label="Source Module" label="Module"
options={moduleOptions} options={modules.map(m => ({ value: m.id, label: m.name }))}
value={moduleFilter} value={moduleIdFilter}
onChange={(v) => { setModuleFilter(v); setCurrentPage(1); }} onChange={(val) => {
setModuleIdFilter(val as string | null);
setCurrentPage(1);
}}
placeholder="All Modules"
/> />
{/* More Filters label pill */} {/* More Filters label pill */}

View File

@ -164,7 +164,7 @@ export const Notifications = () => {
// Special handling for tasks as requested - redirect to My Tasks tab // Special handling for tasks as requested - redirect to My Tasks tab
if (["workflow", "training"].includes(notification.category || "")) { if (["workflow", "training"].includes(notification.category || "")) {
navigate("/tenant/tasks"); navigate("/tenant/workflows/tasks");
return; return;
} }

View File

@ -4,12 +4,14 @@ import { Layout } from "@/components/layout/Layout";
import { import {
DataTable, DataTable,
Pagination, Pagination,
FilterDropdown,
type Column, type Column,
} from "@/components/shared"; } from "@/components/shared";
import { workflowService } from "@/services/workflow-service"; import { workflowService } from "@/services/workflow-service";
import { moduleService } from "@/services/module-service";
import type { WorkflowTask, WorkflowTaskCounts } from "@/types/workflow"; import type { WorkflowTask, WorkflowTaskCounts } from "@/types/workflow";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Inbox, Clock, Calendar, CheckCircle2 } from "lucide-react"; import { Inbox, Clock, Calendar, CheckCircle2, RotateCcw } from "lucide-react";
const formatDate = (value?: string | null): string => { const formatDate = (value?: string | null): string => {
if (!value) return "-"; if (!value) return "-";
@ -44,16 +46,40 @@ const Tasks = (): ReactElement => {
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
// Filters
const [statusFilter, setStatusFilter] = useState<string | null>("pending");
const [moduleFilter, setModuleFilter] = useState<string | null>(null);
const [modules, setModules] = useState<{ id: string; name: string }[]>([]);
const offset = (currentPage - 1) * limit; const offset = (currentPage - 1) * limit;
const totalPages = Math.max(1, Math.ceil(total / limit)); const totalPages = Math.max(1, Math.ceil(total / limit));
useEffect(() => {
const fetchModules = async () => {
try {
const res = await moduleService.getMyModules();
if (res.success) {
setModules(res.data.map(m => ({ id: m.id, name: m.name })));
}
} catch (err) {
console.error("Failed to load modules", err);
}
};
fetchModules();
}, []);
useEffect(() => { useEffect(() => {
const loadData = async () => { const loadData = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const [tasksRes, countsRes] = await Promise.all([ const [tasksRes, countsRes] = await Promise.all([
workflowService.listTasks({ limit, offset }), workflowService.listTasks({
workflowService.getTaskCounts() limit,
offset,
status: statusFilter,
module_id: moduleFilter
}),
workflowService.getTaskCounts({ module_id: moduleFilter })
]); ]);
if (tasksRes.success) { if (tasksRes.success) {
@ -72,7 +98,7 @@ const Tasks = (): ReactElement => {
}; };
loadData(); loadData();
}, [limit, offset]); }, [limit, offset, statusFilter, moduleFilter]);
const columns: Column<WorkflowTask>[] = useMemo( const columns: Column<WorkflowTask>[] = useMemo(
() => [ () => [
@ -114,25 +140,31 @@ const Tasks = (): ReactElement => {
{ {
key: "status", key: "status",
label: "Status", label: "Status",
render: (task) => ( render: (task) => {
const isOverdueActive = task.is_overdue && !["completed", "rejected", "cancelled"].includes(task.status.toLowerCase());
return (
<span className={cn( <span className={cn(
"inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium ring-1 ring-inset", "inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium ring-1 ring-inset",
task.is_overdue isOverdueActive
? "bg-red-50 text-red-700 ring-red-600/10" ? "bg-red-50 text-red-700 ring-red-600/10"
: "bg-green-50 text-green-700 ring-green-600/10" : "bg-green-50 text-green-700 ring-green-600/10"
)}> )}>
{task.is_overdue ? "Overdue" : task.status} {isOverdueActive ? "Overdue" : task.status.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase())}
</span> </span>
), );
},
}, },
{ {
key: "due_at", key: "due_at",
label: "Due Date", label: "Due Date",
render: (task) => ( render: (task) => {
<span className={cn("text-sm", task.is_overdue ? "text-red-600 font-medium" : "text-gray-600")}> const isOverdueActive = task.is_overdue && !["completed", "rejected", "cancelled"].includes(task.status.toLowerCase());
return (
<span className={cn("text-sm", isOverdueActive ? "text-red-600 font-medium" : "text-gray-600")}>
{formatDate(task.due_at)} {formatDate(task.due_at)}
</span> </span>
), );
},
}, },
{ {
key: "actions", key: "actions",
@ -156,7 +188,7 @@ const Tasks = (): ReactElement => {
return ( return (
<Layout <Layout
currentPage="My Tasks" currentPage="Workflow Tasks"
pageHeader={{ pageHeader={{
title: "Workflows & Tasks", title: "Workflows & Tasks",
description: "Manage your pending workflow tasks and approvals.", description: "Manage your pending workflow tasks and approvals.",
@ -193,8 +225,57 @@ const Tasks = (): ReactElement => {
{/* Task Table Area */} {/* Task Table Area */}
<div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden"> <div className="bg-white border border-[rgba(0,0,0,0.08)] rounded-xl shadow-sm overflow-hidden">
<div className="px-6 py-4 border-b border-gray-100 flex items-center justify-between"> <div className="px-6 py-4 border-b border-gray-100 flex flex-col md:flex-row md:items-center justify-between gap-4">
<h3 className="text-lg font-bold text-gray-900">Pending Tasks</h3> <h3 className="text-lg font-bold text-gray-900">
{statusFilter
? `${statusFilter.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase())} Tasks`
: "All Tasks"
}
</h3>
<div className="flex flex-wrap items-center gap-3">
<FilterDropdown
label="Module"
options={modules.map(m => ({ value: m.id, label: m.name }))}
value={moduleFilter}
onChange={(val) => {
setModuleFilter(val as string | null);
setCurrentPage(1);
}}
placeholder="All Modules"
/>
<FilterDropdown
label="Status"
options={[
{ value: "pending", label: "Pending" },
{ value: "in_progress", label: "In Progress" },
{ value: "completed", label: "Completed" },
{ value: "rejected", label: "Rejected" },
{ value: "cancelled", label: "Cancelled" },
]}
value={statusFilter}
onChange={(val) => {
setStatusFilter(val as string | null);
setCurrentPage(1);
}}
placeholder="All Status"
/>
{(statusFilter !== "pending" || moduleFilter) && (
<button
onClick={() => {
setStatusFilter("pending");
setModuleFilter(null);
setCurrentPage(1);
}}
className="flex items-center gap-1.5 text-xs text-red-600 font-medium hover:text-red-700 transition-colors"
>
<RotateCcw className="w-3.5 h-3.5" />
Reset
</button>
)}
</div>
</div> </div>
<DataTable <DataTable

View File

@ -89,7 +89,7 @@ export const tenantAdminRoutes: RouteConfig[] = [
element: <LazyRoute component={Designations} />, element: <LazyRoute component={Designations} />,
}, },
{ {
path: "/tenant/workflow-definitions", path: "/tenant/workflows/definitions",
element: <LazyRoute component={WorkflowDefination} />, element: <LazyRoute component={WorkflowDefination} />,
}, },
{ {
@ -121,7 +121,7 @@ export const tenantAdminRoutes: RouteConfig[] = [
element: <LazyRoute component={DocumentsDueForReview} />, element: <LazyRoute component={DocumentsDueForReview} />,
}, },
{ {
path: "/tenant/tasks", path: "/tenant/workflows/tasks",
element: <LazyRoute component={Tasks} />, element: <LazyRoute component={Tasks} />,
}, },
{ {

View File

@ -154,6 +154,7 @@ export interface FileListParams {
category?: string; category?: string;
category_id?: string; category_id?: string;
source_module?: string; source_module?: string;
source_module_id?: string;
search?: string; search?: string;
tags?: string; tags?: string;
uploaded_by?: string; uploaded_by?: string;

View File

@ -93,13 +93,18 @@ class WorkflowService {
return response.data; return response.data;
} }
async listTasks(params?: { limit?: number; offset?: number }): Promise<WorkflowTasksResponse> { async listTasks(params?: {
limit?: number;
offset?: number;
status?: string | null;
module_id?: string | null;
}): Promise<WorkflowTasksResponse> {
const response = await apiClient.get<WorkflowTasksResponse>(`${this.baseUrl}/tasks`, { params }); const response = await apiClient.get<WorkflowTasksResponse>(`${this.baseUrl}/tasks`, { params });
return response.data; return response.data;
} }
async getTaskCounts(): Promise<WorkflowTaskCountsResponse> { async getTaskCounts(params?: { module_id?: string | null; entity_type?: string | null; entity_id?: string | null }): Promise<WorkflowTaskCountsResponse> {
const response = await apiClient.get<WorkflowTaskCountsResponse>(`${this.baseUrl}/tasks/counts`); const response = await apiClient.get<WorkflowTaskCountsResponse>(`${this.baseUrl}/tasks/counts`, { params });
return response.data; return response.data;
} }