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,
|
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 && (
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 */}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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} />,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user