refactor: reorganize sidebar navigation menu structure and update group item rendering logic
This commit is contained in:
parent
ccbda2c0ae
commit
68173ac28a
@ -12,7 +12,6 @@ import {
|
||||
Shield,
|
||||
BadgeCheck,
|
||||
GitBranch,
|
||||
Briefcase,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Bell,
|
||||
@ -73,18 +72,6 @@ const tenantAdminPlatformMenu: MenuItem[] = [
|
||||
path: "/tenant/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,
|
||||
label: "Departments",
|
||||
@ -97,12 +84,6 @@ const tenantAdminPlatformMenu: MenuItem[] = [
|
||||
path: "/tenant/designations",
|
||||
requiredPermission: { resource: "designations" },
|
||||
},
|
||||
{
|
||||
icon: GitBranch,
|
||||
label: "Workflow Definitions",
|
||||
path: "/tenant/workflow-definitions",
|
||||
requiredPermission: { resource: "workflow" },
|
||||
},
|
||||
{
|
||||
icon: Users,
|
||||
label: "Suppliers",
|
||||
@ -110,23 +91,70 @@ const tenantAdminPlatformMenu: MenuItem[] = [
|
||||
requiredPermission: { resource: "supplier" },
|
||||
},
|
||||
{
|
||||
icon: FileText,
|
||||
label: "Document Services",
|
||||
icon: Users,
|
||||
label: "Users",
|
||||
path: "/tenant/users",
|
||||
requiredPermission: { resource: "users" },
|
||||
},
|
||||
];
|
||||
|
||||
const tenantAdminPlatformServiceMenu: MenuItem[] = [
|
||||
{
|
||||
icon: GitBranch,
|
||||
label: "Workflows",
|
||||
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" } },
|
||||
{
|
||||
label: "Definitions",
|
||||
path: "/tenant/workflows/definitions",
|
||||
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" },
|
||||
},
|
||||
{
|
||||
icon: Paperclip,
|
||||
label: "File Attachment Services",
|
||||
label: "File Attachments",
|
||||
isGroup: true,
|
||||
children: [
|
||||
{ label: "Files List", path: "/tenant/files", requiredPermission: { resource: "files" } },
|
||||
{
|
||||
label: "Files List",
|
||||
path: "/tenant/files",
|
||||
requiredPermission: { resource: "files" },
|
||||
},
|
||||
],
|
||||
requiredPermission: { resource: "files" },
|
||||
},
|
||||
@ -153,31 +181,37 @@ const tenantAdminSystemMenu: MenuItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const GroupMenuItem = ({
|
||||
item,
|
||||
childrenItems,
|
||||
location,
|
||||
isSuperAdmin,
|
||||
theme,
|
||||
onClose
|
||||
}: {
|
||||
item: MenuItem;
|
||||
childrenItems: any[];
|
||||
location: any;
|
||||
isSuperAdmin: boolean;
|
||||
theme: any;
|
||||
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));
|
||||
const isSubActionActive = subActions.some((sub) =>
|
||||
location.pathname.startsWith(path + sub),
|
||||
);
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
@ -192,22 +226,41 @@ const GroupMenuItem = ({
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
className={cn(
|
||||
"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 ? {
|
||||
backgroundColor: !isSuperAdmin && theme?.primary_color ? theme.primary_color : "#112868",
|
||||
color: !isSuperAdmin && theme?.secondary_color ? theme.secondary_color : "#23dce1"
|
||||
} : undefined}
|
||||
style={
|
||||
isAnyChildActive
|
||||
? {
|
||||
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">
|
||||
<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}
|
||||
</span>
|
||||
</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>
|
||||
|
||||
|
||||
{isExpanded && (
|
||||
<div className="flex flex-col mt-1 mb-1 border-l-2 border-[rgba(0,0,0,0.08)] ml-5 py-1 gap-0.5">
|
||||
{childrenItems.map((child) => {
|
||||
@ -223,13 +276,24 @@ const GroupMenuItem = ({
|
||||
}}
|
||||
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"
|
||||
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}
|
||||
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>
|
||||
);
|
||||
})}
|
||||
@ -348,11 +412,11 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||
child.requiredPermission.action,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
// We need to return a new object to avoid issues
|
||||
if (filteredChildren.length > 0) {
|
||||
(item as any)._filteredChildren = filteredChildren;
|
||||
return true;
|
||||
(item as any)._filteredChildren = filteredChildren;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -365,6 +429,9 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||
const platformMenu = filterMenuItems(
|
||||
isSuperAdmin ? superAdminPlatformMenu : tenantAdminPlatformMenu,
|
||||
);
|
||||
const platformServiceMenu = filterMenuItems(
|
||||
isSuperAdmin ? [] : tenantAdminPlatformServiceMenu,
|
||||
);
|
||||
const systemMenu = filterMenuItems(
|
||||
isSuperAdmin ? superAdminSystemMenu : tenantAdminSystemMenu,
|
||||
);
|
||||
@ -386,11 +453,12 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||
<div className="flex flex-col gap-1 mt-1">
|
||||
{items.map((item) => {
|
||||
if (item.isGroup) {
|
||||
const children = (item as any)._filteredChildren || item.children || [];
|
||||
const children =
|
||||
(item as any)._filteredChildren || item.children || [];
|
||||
return (
|
||||
<GroupMenuItem
|
||||
key={item.label}
|
||||
item={item}
|
||||
<GroupMenuItem
|
||||
key={item.label}
|
||||
item={item}
|
||||
childrenItems={children}
|
||||
location={location}
|
||||
isSuperAdmin={isSuperAdmin}
|
||||
@ -404,8 +472,9 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||
const isTenantDashboardPath = item.path === "/tenant";
|
||||
const isActive = isTenantDashboardPath
|
||||
? location.pathname === "/tenant"
|
||||
: item.path && (location.pathname === item.path ||
|
||||
location.pathname.startsWith(`${item.path}/`));
|
||||
: item.path &&
|
||||
(location.pathname === item.path ||
|
||||
location.pathname.startsWith(`${item.path}/`));
|
||||
return (
|
||||
<Link
|
||||
key={item.path}
|
||||
@ -438,7 +507,10 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||
}
|
||||
>
|
||||
<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}
|
||||
</span>
|
||||
</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">
|
||||
{/* Platform Menu */}
|
||||
<MenuSection title="Platform" items={platformMenu} />
|
||||
<MenuSection title="Platform Services" items={platformServiceMenu} />
|
||||
|
||||
{/* System Menu */}
|
||||
<MenuSection title="System" items={systemMenu} />
|
||||
@ -551,7 +624,11 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||
src={logoUrl}
|
||||
alt="Logo"
|
||||
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}
|
||||
<div
|
||||
@ -593,6 +670,9 @@ export const Sidebar = ({ isOpen, onClose }: SidebarProps) => {
|
||||
{platformMenu.length > 0 && (
|
||||
<MenuSection title="Platform" items={platformMenu} />
|
||||
)}
|
||||
{platformServiceMenu.length > 0 && (
|
||||
<MenuSection title="Platform Services" items={platformServiceMenu} />
|
||||
)}
|
||||
|
||||
{/* System Menu */}
|
||||
{systemMenu.length > 0 && (
|
||||
|
||||
@ -78,7 +78,7 @@ export const NotificationBell = () => {
|
||||
|
||||
// Special handling for tasks as requested - redirect to My Tasks tab
|
||||
if (['workflow', 'training'].includes(notification.category || '')) {
|
||||
navigate('/tenant/tasks');
|
||||
navigate('/tenant/workflows/tasks');
|
||||
setIsOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -41,7 +41,7 @@ export const NotificationProvider = ({ children }: NotificationProviderProps) =>
|
||||
label: 'View',
|
||||
onClick: () => {
|
||||
if (['workflow', 'training'].includes(notification.category || '')) {
|
||||
navigate('/tenant/tasks');
|
||||
navigate('/tenant/workflows/tasks');
|
||||
} else {
|
||||
navigate(notification.action_url);
|
||||
}
|
||||
|
||||
@ -335,7 +335,7 @@ const Dashboard = (): ReactElement => {
|
||||
<div className="flex justify-between items-center mb-5">
|
||||
<h2 className="text-[16px] font-bold text-[#111827] tracking-tight">My Tasks</h2>
|
||||
<button
|
||||
onClick={() => navigate('/tenant/tasks')}
|
||||
onClick={() => navigate('/tenant/workflows/tasks')}
|
||||
className="text-[11px] font-bold text-[#084cc8] hover:underline"
|
||||
>
|
||||
View all
|
||||
|
||||
@ -33,6 +33,8 @@ import fileAttachmentService, {
|
||||
type FileAttachment,
|
||||
type CategoriesFilterOptions,
|
||||
} from "@/services/file-attachment-service";
|
||||
import { moduleService } from "@/services/module-service";
|
||||
import { FilterDropdown } from "@/components/shared";
|
||||
import { FileUploadModal } from "@/components/shared/FileUploadModal";
|
||||
import type { RootState } from "@/store/store";
|
||||
|
||||
@ -285,7 +287,8 @@ const FilesList = (): ReactElement => {
|
||||
// Filters
|
||||
const [search, setSearch] = useState("");
|
||||
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
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
@ -307,6 +310,12 @@ const FilesList = (): ReactElement => {
|
||||
fileAttachmentService.getCategoriesFilterOptions().then((res) => {
|
||||
setCategories(res.data.categories);
|
||||
}).catch(() => {});
|
||||
|
||||
moduleService.getMyModules().then((res) => {
|
||||
if (res.success) {
|
||||
setModules(res.data.map(m => ({ id: m.id, name: m.name })));
|
||||
}
|
||||
}).catch(() => {});
|
||||
}, []);
|
||||
|
||||
// ── Load files ──
|
||||
@ -317,7 +326,7 @@ const FilesList = (): ReactElement => {
|
||||
const res = await fileAttachmentService.list({
|
||||
search: search.trim() || undefined,
|
||||
category: categoryFilter || undefined,
|
||||
source_module: moduleFilter || undefined,
|
||||
source_module_id: moduleIdFilter || undefined,
|
||||
limit,
|
||||
offset,
|
||||
});
|
||||
@ -328,25 +337,12 @@ const FilesList = (): ReactElement => {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [search, categoryFilter, moduleFilter, limit, offset]);
|
||||
}, [search, categoryFilter, moduleIdFilter, limit, offset]);
|
||||
|
||||
useEffect(() => {
|
||||
void 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[]>(() =>
|
||||
categories.map((c) => ({ value: c.category, label: c.category })),
|
||||
[categories]);
|
||||
@ -372,7 +368,7 @@ const FilesList = (): ReactElement => {
|
||||
const clearFilters = () => {
|
||||
setSearch("");
|
||||
setCategoryFilter(null);
|
||||
setModuleFilter(null);
|
||||
setModuleIdFilter(null);
|
||||
setCurrentPage(1);
|
||||
};
|
||||
|
||||
@ -426,12 +422,16 @@ const FilesList = (): ReactElement => {
|
||||
onChange={(v) => { setCategoryFilter(v); setCurrentPage(1); }}
|
||||
/>
|
||||
|
||||
{/* More Filters (Source Module) */}
|
||||
<FilterPill
|
||||
label="Source Module"
|
||||
options={moduleOptions}
|
||||
value={moduleFilter}
|
||||
onChange={(v) => { setModuleFilter(v); setCurrentPage(1); }}
|
||||
{/* Source Module filter */}
|
||||
<FilterDropdown
|
||||
label="Module"
|
||||
options={modules.map(m => ({ value: m.id, label: m.name }))}
|
||||
value={moduleIdFilter}
|
||||
onChange={(val) => {
|
||||
setModuleIdFilter(val as string | null);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
placeholder="All Modules"
|
||||
/>
|
||||
|
||||
{/* More Filters label pill */}
|
||||
|
||||
@ -164,7 +164,7 @@ export const Notifications = () => {
|
||||
|
||||
// Special handling for tasks as requested - redirect to My Tasks tab
|
||||
if (["workflow", "training"].includes(notification.category || "")) {
|
||||
navigate("/tenant/tasks");
|
||||
navigate("/tenant/workflows/tasks");
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -4,12 +4,14 @@ import { Layout } from "@/components/layout/Layout";
|
||||
import {
|
||||
DataTable,
|
||||
Pagination,
|
||||
FilterDropdown,
|
||||
type Column,
|
||||
} from "@/components/shared";
|
||||
import { workflowService } from "@/services/workflow-service";
|
||||
import { moduleService } from "@/services/module-service";
|
||||
import type { WorkflowTask, WorkflowTaskCounts } from "@/types/workflow";
|
||||
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 => {
|
||||
if (!value) return "-";
|
||||
@ -43,17 +45,41 @@ const Tasks = (): ReactElement => {
|
||||
const [total, setTotal] = useState(0);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
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 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(() => {
|
||||
const loadData = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const [tasksRes, countsRes] = await Promise.all([
|
||||
workflowService.listTasks({ limit, offset }),
|
||||
workflowService.getTaskCounts()
|
||||
workflowService.listTasks({
|
||||
limit,
|
||||
offset,
|
||||
status: statusFilter,
|
||||
module_id: moduleFilter
|
||||
}),
|
||||
workflowService.getTaskCounts({ module_id: moduleFilter })
|
||||
]);
|
||||
|
||||
if (tasksRes.success) {
|
||||
@ -72,7 +98,7 @@ const Tasks = (): ReactElement => {
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, [limit, offset]);
|
||||
}, [limit, offset, statusFilter, moduleFilter]);
|
||||
|
||||
const columns: Column<WorkflowTask>[] = useMemo(
|
||||
() => [
|
||||
@ -114,25 +140,31 @@ const Tasks = (): ReactElement => {
|
||||
{
|
||||
key: "status",
|
||||
label: "Status",
|
||||
render: (task) => (
|
||||
<span className={cn(
|
||||
"inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium ring-1 ring-inset",
|
||||
task.is_overdue
|
||||
? "bg-red-50 text-red-700 ring-red-600/10"
|
||||
: "bg-green-50 text-green-700 ring-green-600/10"
|
||||
)}>
|
||||
{task.is_overdue ? "Overdue" : task.status}
|
||||
</span>
|
||||
),
|
||||
render: (task) => {
|
||||
const isOverdueActive = task.is_overdue && !["completed", "rejected", "cancelled"].includes(task.status.toLowerCase());
|
||||
return (
|
||||
<span className={cn(
|
||||
"inline-flex items-center rounded-md px-2 py-1 text-[11px] font-medium ring-1 ring-inset",
|
||||
isOverdueActive
|
||||
? "bg-red-50 text-red-700 ring-red-600/10"
|
||||
: "bg-green-50 text-green-700 ring-green-600/10"
|
||||
)}>
|
||||
{isOverdueActive ? "Overdue" : task.status.replace(/_/g, " ").replace(/\b\w/g, l => l.toUpperCase())}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "due_at",
|
||||
label: "Due Date",
|
||||
render: (task) => (
|
||||
<span className={cn("text-sm", task.is_overdue ? "text-red-600 font-medium" : "text-gray-600")}>
|
||||
{formatDate(task.due_at)}
|
||||
</span>
|
||||
),
|
||||
render: (task) => {
|
||||
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)}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "actions",
|
||||
@ -156,7 +188,7 @@ const Tasks = (): ReactElement => {
|
||||
|
||||
return (
|
||||
<Layout
|
||||
currentPage="My Tasks"
|
||||
currentPage="Workflow Tasks"
|
||||
pageHeader={{
|
||||
title: "Workflows & Tasks",
|
||||
description: "Manage your pending workflow tasks and approvals.",
|
||||
@ -193,8 +225,57 @@ const Tasks = (): ReactElement => {
|
||||
|
||||
{/* Task Table Area */}
|
||||
<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">
|
||||
<h3 className="text-lg font-bold text-gray-900">Pending Tasks</h3>
|
||||
<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">
|
||||
{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>
|
||||
|
||||
<DataTable
|
||||
|
||||
@ -89,7 +89,7 @@ export const tenantAdminRoutes: RouteConfig[] = [
|
||||
element: <LazyRoute component={Designations} />,
|
||||
},
|
||||
{
|
||||
path: "/tenant/workflow-definitions",
|
||||
path: "/tenant/workflows/definitions",
|
||||
element: <LazyRoute component={WorkflowDefination} />,
|
||||
},
|
||||
{
|
||||
@ -121,7 +121,7 @@ export const tenantAdminRoutes: RouteConfig[] = [
|
||||
element: <LazyRoute component={DocumentsDueForReview} />,
|
||||
},
|
||||
{
|
||||
path: "/tenant/tasks",
|
||||
path: "/tenant/workflows/tasks",
|
||||
element: <LazyRoute component={Tasks} />,
|
||||
},
|
||||
{
|
||||
|
||||
@ -154,6 +154,7 @@ export interface FileListParams {
|
||||
category?: string;
|
||||
category_id?: string;
|
||||
source_module?: string;
|
||||
source_module_id?: string;
|
||||
search?: string;
|
||||
tags?: string;
|
||||
uploaded_by?: string;
|
||||
|
||||
@ -93,13 +93,18 @@ class WorkflowService {
|
||||
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 });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
async getTaskCounts(): Promise<WorkflowTaskCountsResponse> {
|
||||
const response = await apiClient.get<WorkflowTaskCountsResponse>(`${this.baseUrl}/tasks/counts`);
|
||||
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`, { params });
|
||||
return response.data;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user