From 4b76f71cf47ee92a3fcda2d0116a7f32feb9ffbb Mon Sep 17 00:00:00 2001 From: Yashwin Date: Mon, 23 Mar 2026 14:24:24 +0530 Subject: [PATCH] feat: Implement role-based quick actions and enhance recent activity display with tenant filtering and UI refinements. --- src/components/layout/Sidebar.tsx | 2 +- .../dashboard/components/QuickActions.tsx | 89 +++-- .../dashboard/components/RecentActivity.tsx | 209 ++++++------ src/pages/tenant/Dashboard.tsx | 323 +++++++++++++----- src/types/dashboard.ts | 4 +- 5 files changed, 414 insertions(+), 213 deletions(-) diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index fc89efe..e27ea16 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -19,7 +19,7 @@ import { useTenantTheme } from "@/hooks/useTenantTheme"; import { AuthenticatedImage } from "@/components/shared"; interface MenuItem { - icon: React.ComponentType<{ className?: string }>; + icon: React.ComponentType<{ className?: string; strokeWidth?: number }>; label: string; path: string; requiredPermission?: { diff --git a/src/features/dashboard/components/QuickActions.tsx b/src/features/dashboard/components/QuickActions.tsx index 10ed81a..aa23730 100644 --- a/src/features/dashboard/components/QuickActions.tsx +++ b/src/features/dashboard/components/QuickActions.tsx @@ -1,42 +1,69 @@ import { useNavigate } from 'react-router-dom'; -import { Plus, UserPlus, Shield, Settings } from 'lucide-react'; -import { Card, CardHeader, CardContent } from '@/components/ui/card'; +import { Plus, UserPlus, Shield, Settings, Building2, BadgeCheck } from 'lucide-react'; +import { useAppSelector } from '@/hooks/redux-hooks'; import type { QuickAction } from '@/types/dashboard'; export const QuickActions = () => { const navigate = useNavigate(); + const { roles } = useAppSelector((state) => state.auth); - const quickActions: QuickAction[] = [ - { icon: Plus, label: 'New Tenant', onClick: () => navigate('/tenants') }, - { icon: UserPlus, label: 'Invite User', onClick: () => navigate('/tenants') }, - { icon: Shield, label: 'Add Role', onClick: () => navigate('/tenants') }, - { icon: Settings, label: 'Config', onClick: () => console.log('Config') }, + // Simple roles array parsing + let rolesArray: string[] = []; + if (Array.isArray(roles)) { + rolesArray = roles; + } else if (typeof roles === "string") { + try { + rolesArray = JSON.parse(roles); + } catch { + rolesArray = []; + } + } + + const isSuperAdmin = rolesArray.includes('super_admin'); + + // Define actions based on role + const superAdminActions: QuickAction[] = [ + { icon: Plus, label: 'New Tenant', onClick: () => navigate('/tenants/create-wizard') }, + { icon: UserPlus, label: 'New User', onClick: () => console.log('New User') }, + { icon: Shield, label: 'New Role', onClick: () => console.log('New Role') }, + { icon: Settings, label: 'New Config', onClick: () => console.log('Config') }, ]; + const tenantAdminActions: QuickAction[] = [ + { icon: UserPlus, label: 'New User', onClick: () => navigate('/tenant/users') }, + { icon: Shield, label: 'New Role', onClick: () => navigate('/tenant/roles') }, + { icon: Building2, label: 'New Dept', onClick: () => navigate('/tenant/departments') }, + { icon: BadgeCheck, label: 'New Desig', onClick: () => navigate('/tenant/designations') }, + ]; + + const actions = isSuperAdmin ? superAdminActions : tenantAdminActions; + return ( - - -

Quick Actions

-
- -
- {quickActions.map((action, index) => { - const Icon = action.icon; - return ( - - ); - })} -
-
-
+
+
+

Quick Actions

+ +
+ +
+ {actions.map((action, index) => { + const Icon = action.icon; + return ( + + ); + })} +
+
); }; diff --git a/src/features/dashboard/components/RecentActivity.tsx b/src/features/dashboard/components/RecentActivity.tsx index fdd22ad..174bd93 100644 --- a/src/features/dashboard/components/RecentActivity.tsx +++ b/src/features/dashboard/components/RecentActivity.tsx @@ -1,12 +1,14 @@ import { useState, useEffect } from 'react'; -import { ChevronDown, Filter, Loader2 } from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Card, CardHeader, CardContent } from '@/components/ui/card'; -import { StatusBadge } from '@/components/shared'; +import { ChevronDown, Filter, Loader2, User } from 'lucide-react'; import { auditLogService } from '@/services/audit-log-service'; import type { AuditLog } from '@/types/audit-log'; +import { useAppSelector } from '@/hooks/redux-hooks'; +import { cn } from '@/lib/utils'; +import { Card, CardHeader, CardContent } from '@/components/ui/card'; +import { StatusBadge } from '@/components/shared'; +import { Button } from '@/components/ui/button'; -// Helper function to get action badge variant +// Helper functions (kept from original) const getActionVariant = (action: string): 'success' | 'failure' | 'info' | 'process' => { const lowerAction = action.toLowerCase(); if (lowerAction.includes('create') || lowerAction.includes('register')) return 'success'; @@ -16,65 +18,94 @@ const getActionVariant = (action: string): 'success' | 'failure' | 'info' | 'pro return 'info'; }; -// Helper function to format relative time const formatRelativeTime = (dateString: string): string => { const date = new Date(dateString); const now = new Date(); const diffInSeconds = Math.floor((now.getTime() - date.getTime()) / 1000); - - if (diffInSeconds < 60) { - return `${diffInSeconds} sec${diffInSeconds !== 1 ? 's' : ''} ago`; - } - + if (diffInSeconds < 60) return 'Just now'; const diffInMinutes = Math.floor(diffInSeconds / 60); - if (diffInMinutes < 60) { - return `${diffInMinutes} min${diffInMinutes !== 1 ? 's' : ''} ago`; - } - + if (diffInMinutes < 60) return `${diffInMinutes} min ago`; const diffInHours = Math.floor(diffInMinutes / 60); - if (diffInHours < 24) { - return `${diffInHours} hour${diffInHours !== 1 ? 's' : ''} ago`; - } - + if (diffInHours < 24) return `${diffInHours} hours ago`; const diffInDays = Math.floor(diffInHours / 24); - if (diffInDays < 7) { - return `${diffInDays} day${diffInDays !== 1 ? 's' : ''} ago`; - } - - return date.toLocaleDateString('en-US', { - month: 'short', - day: 'numeric', - year: date.getFullYear() !== now.getFullYear() ? 'numeric' : undefined, - }); + if (diffInDays === 1) return 'Yesterday'; + return date.toLocaleDateString(); }; -export const RecentActivity = () => { +export interface RecentActivityProps { + variant?: 'list' | 'table'; +} + +export const RecentActivity = ({ variant }: RecentActivityProps) => { const [auditLogs, setAuditLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); + const { tenantId } = useAppSelector((state) => state.auth); + + // Default to table variant for a more professional look + const activeVariant = variant || 'table'; useEffect(() => { const fetchRecentActivity = async (): Promise => { try { setIsLoading(true); - setError(null); - const response = await auditLogService.getAll(1, 5, null, ['created_at', 'desc']); + // Pass tenantId if we're on a tenant route to get tenant-specific logs + const filterTenantId = window.location.pathname.startsWith('/tenant') ? tenantId : null; + const response = await auditLogService.getAll(1, 5, null, ['created_at', 'desc'], filterTenantId); if (response.success) { setAuditLogs(response.data); - } else { - setError('Failed to load recent activity'); } } catch (err: any) { - setError(err?.response?.data?.error?.message || 'Failed to load recent activity'); + // Fallback or log if needed } finally { setIsLoading(false); } }; fetchRecentActivity(); - }, []); + }, [tenantId, activeVariant]); + + if (activeVariant === 'list') { + return ( +
+
+

Recent Activity

+ Last 24 hours +
+ +
+ {isLoading ? ( +
+ +
+ ) : auditLogs.length === 0 ? ( +
No recent activity
+ ) : ( +
+ {auditLogs.map((log, index) => ( +
+ {formatRelativeTime(log.created_at)} +
+ +
+
+
+ {log.user?.email?.split('@')[0] || 'System User'} + {log.action.toLowerCase().includes('create') ? 'created' : 'performed'} + {log.resource_type} +
+
+
+ ))} +
+ )} +
+
+ ); + } + + // TABLE VARIANT (Original design for superadmin dashboard) return ( - +

Recent Activity

@@ -83,81 +114,57 @@ export const RecentActivity = () => { All
-
- {isLoading && ( + {isLoading ? (
- )} - - {error && ( -
-

{error}

-
- )} - - {!isLoading && !error && ( + ) : (
- {auditLogs.length === 0 ? ( -
-

No recent activity found

-
- ) : ( - - - - - - - - +
- Action - - Resource Type - - Resource ID - - IP Address - - Timestamp -
+ + + + + + + + + + {auditLogs.map((log) => ( + + + + + - - - {auditLogs.map((log) => ( - - - - - - - - ))} - -
User ProfileTimestampResource TypeAction
+
+
+ +
+
+ + {log.user ? `${log.user.first_name || ''} ${log.user.last_name || ''}`.trim() || log.user.email.split('@')[0] : 'System User'} + + {log.user && {log.user.email}} +
+
+
+ {formatRelativeTime(log.created_at)} + + {log.resource_type} + + {log.action} +
- - {log.action} - - - {log.resource_type} - - {log.resource_id || 'N/A'} - - {log.ip_address || 'N/A'} - - {formatRelativeTime(log.created_at)} -
- )} + ))} + +
)}
diff --git a/src/pages/tenant/Dashboard.tsx b/src/pages/tenant/Dashboard.tsx index 5ea3a0b..56247ef 100644 --- a/src/pages/tenant/Dashboard.tsx +++ b/src/pages/tenant/Dashboard.tsx @@ -1,60 +1,71 @@ -import { Layout } from '@/components/layout/Layout'; -import type { ReactElement } from 'react'; -import { Info, FileCheck, Briefcase, FileText, GraduationCap } from 'lucide-react'; +import { Layout } from "@/components/layout/Layout"; +import type { ReactElement } from "react"; +import { + Info, + FileCheck, + Briefcase, + FileText, + GraduationCap, +} from "lucide-react"; +import { QuickActions } from "@/features/dashboard/components/QuickActions"; +import { RecentActivity } from "@/features/dashboard/components/RecentActivity"; +import { cn } from "@/lib/utils"; +import { + ResponsiveContainer, + ComposedChart, + CartesianGrid, + XAxis, + YAxis, + Tooltip, + Legend, + Bar, + Line +} from 'recharts'; interface StatCardProps { - icon: React.ComponentType<{ className?: string }>; + icon: React.ComponentType<{ className?: string; strokeWidth?: number }>; value: string | number; label: string; - status: 'success' | 'process' | 'warning' | 'disabled'; - statusLabel: string; + badge?: { + text: string; + variant: 'success' | 'warning' | 'info' | 'error'; + }; } -const StatCard = ({ icon: Icon, value, label, status, statusLabel }: StatCardProps): ReactElement => { - const statusConfig = { - success: { - bg: 'bg-[#f1fffb]', - dot: 'bg-[#16c784]', - text: 'text-[#16c784]', - }, - process: { - bg: 'bg-[#fff5e5]', - dot: 'bg-[#fca004]', - text: 'text-[#fca004]', - }, - warning: { - bg: 'bg-[#fdf5f4]', - dot: 'bg-[#e0352a]', - text: 'text-[#e0352a]', - }, - disabled: { - bg: 'bg-[#e5e7eb]', - dot: 'bg-[#9ca3af]', - text: 'text-[#9ca3af]', - }, - }; - - const config = statusConfig[status]; - const valueColor = status === 'warning' && label === 'Overdue Tasks' ? 'text-[#e0352a]' : 'text-[#0f1724]'; - +const StatCard = ({ + icon: Icon, + value, + label, + badge +}: StatCardProps): ReactElement => { return ( -
-
- {/* Header with icon and status */} +
+
+ {/* Interaction Gradient */} +
+
- -
-
- {statusLabel} +
+
+ {badge && ( +
+ {badge.text} +
+ )}
- {/* Value and Label */} -
-
+
+
{value}
-
+
{label}
@@ -63,64 +74,220 @@ const StatCard = ({ icon: Icon, value, label, status, statusLabel }: StatCardPro ); }; +const TaskCard = ({ type, title, priority, deadlineLabel }: { + type: string; + title: string; + priority: 'High' | 'Medium' | 'Low'; + deadlineLabel: string; +}) => ( +
+
+ {type} + {deadlineLabel} +
+ +
{title}
+ +
+
+
+ {priority} • Owner: You +
+ +
+ + +
+
+
+); + +const CAPASummaryChart = () => { + const data = [ + { name: 'Jan', open: 35, inProgress: 48, closed: 25, trend: 15 }, + { name: 'Feb', open: 28, inProgress: 35, closed: 20, trend: 12 }, + { name: 'Mar', open: 45, inProgress: 75, closed: 38, trend: 32 }, + { name: 'Apr', open: 40, inProgress: 65, closed: 42, trend: 28 }, + { name: 'May', open: 55, inProgress: 95, closed: 78, trend: 52 }, + { name: 'Jun', open: 42, inProgress: 82, closed: 72, trend: 45 }, + { name: 'Jul', open: 38, inProgress: 70, closed: 65, trend: 38 }, + { name: 'Aug', open: 48, inProgress: 94, closed: 82, trend: 48 }, + { name: 'Sep', open: 32, inProgress: 65, closed: 58, trend: 35 }, + { name: 'Oct', open: 44, inProgress: 88, closed: 85, trend: 58 }, + { name: 'Nov', open: 52, inProgress: 92, closed: 98, trend: 62 }, + { name: 'Dec', open: 60, inProgress: 105, closed: 115, trend: 58 }, + ]; + + return ( +
+ + + + + + + + + + + + + +
+ ); +}; + const Dashboard = (): ReactElement => { const statCards: StatCardProps[] = [ - // { - // icon: Info, - // value: '18', - // label: 'Open CAPAs', - // status: 'success', - // statusLabel: 'Success', - // }, + { + icon: Info, + value: 18, + label: "Open CAPAs", + badge: { text: "2 New This Week", variant: "success" } + }, { icon: FileCheck, - value: '7', - label: 'Pending Approvals', - status: 'process', - statusLabel: 'Process', + value: 7, + label: "Pending Approvals", + badge: { text: "Awaiting Manager Review", variant: "warning" } }, { icon: Briefcase, - value: '9', - label: 'Active Projects', - status: 'warning', - statusLabel: 'Warning', + value: 9, + label: "Active Projects", + badge: { text: "3 At Risk Of Delay", variant: "error" } }, { icon: Info, - value: '3', - label: 'Overdue Tasks', - status: 'warning', - statusLabel: 'Warning', + value: 3, + label: "Overdue Tasks", + badge: { text: "Action Needed", variant: "error" } }, { icon: FileText, - value: '14', - label: 'Docs Pending Review', - status: 'disabled', - statusLabel: 'Disabled', + value: 14, + label: "Docs Pending Review", + badge: { text: "Due Within 7 Days", variant: "info" } }, { icon: GraduationCap, - value: '94%', - label: 'Training Compliance', - status: 'success', - statusLabel: 'Success', + value: "94%", + label: "Training Compliance", + badge: { text: "Target Met", variant: "success" } }, ]; return ( -
- {statCards.map((card, index) => ( - - ))} +
+ {/* Main Content Area (Left) */} +
+ {/* Stats Grid */} +
+ {statCards.map((card, index) => ( + + ))} +
+ + {/* CAPA Summary Card */} +
+
+

+ CAPA Summary +

+
+ Data Range + +
+
+ + +
+ + {/* Recent Activity Card */} + +
+ + {/* Sidebar area (Right) */} +
+ {/* My Tasks Card */} +
+
+

My Tasks

+ +
+ +
+ + + +
+
+ + {/* Quick Actions Card */} + +
); diff --git a/src/types/dashboard.ts b/src/types/dashboard.ts index 44ae806..c792b52 100644 --- a/src/types/dashboard.ts +++ b/src/types/dashboard.ts @@ -1,5 +1,5 @@ export interface StatCardData { - icon: React.ComponentType<{ className?: string }>; + icon: React.ComponentType<{ className?: string; strokeWidth?: number }>; value: string | number; label: string; badge?: { @@ -17,7 +17,7 @@ export interface ActivityLog { } export interface QuickAction { - icon: React.ComponentType<{ className?: string }>; + icon: React.ComponentType<{ className?: string; strokeWidth?: number }>; label: string; onClick: () => void; }