From f4838422c2a264885b1c0af2c61af6244ea60c22 Mon Sep 17 00:00:00 2001 From: Yashwin Date: Thu, 7 May 2026 19:14:11 +0530 Subject: [PATCH] refactor: update UI styling and layout for dashboard task components --- .../dashboard/components/QuickActions.tsx | 111 ++++++-- .../dashboard/components/RecentActivity.tsx | 247 ++++++++++------- .../dashboard/components/SystemHealth.tsx | 114 ++++---- src/pages/tenant/Dashboard.tsx | 261 ++++++------------ 4 files changed, 370 insertions(+), 363 deletions(-) diff --git a/src/features/dashboard/components/QuickActions.tsx b/src/features/dashboard/components/QuickActions.tsx index d1f4532..77ddec5 100644 --- a/src/features/dashboard/components/QuickActions.tsx +++ b/src/features/dashboard/components/QuickActions.tsx @@ -1,8 +1,15 @@ -import { useNavigate } from 'react-router-dom'; -import { Plus, UserPlus, Shield, Settings, Building2, BadgeCheck } from 'lucide-react'; -import { useAppSelector } from '@/hooks/redux-hooks'; -import type { QuickAction } from '@/types/dashboard'; -import { useAppTheme } from '@/hooks/useAppTheme'; +import { useNavigate } from "react-router-dom"; +import { + Plus, + UserPlus, + Shield, + Settings, + Building2, + BadgeCheck, +} from "lucide-react"; +import { useAppSelector } from "@/hooks/redux-hooks"; +import type { QuickAction } from "@/types/dashboard"; +import { useAppTheme } from "@/hooks/useAppTheme"; export const QuickActions = () => { const { primaryColor } = useAppTheme(); @@ -11,50 +18,102 @@ export const QuickActions = () => { // Helper to check permission const hasPermission = (resource: string, action: string) => { - if (roles.includes('super_admin') || roles.includes('tenant_admin')) return true; - return permissions.some(p => p.resource === resource && p.action === action); + if (roles.includes("super_admin") || roles.includes("tenant_admin")) + return true; + return permissions.some( + (p) => p.resource === resource && p.action === action, + ); }; - const isSuperAdmin = roles.includes('super_admin'); + const isSuperAdmin = roles.includes("super_admin"); // Define actions based on role const superAdminActions: QuickAction[] = [ - { icon: Plus, label: 'New Tenant', onClick: () => navigate('/tenants/create-wizard') }, - { icon: UserPlus, label: 'Module', onClick: () => navigate('/modules') }, - { icon: Shield, label: 'Notification', onClick: () => navigate('/notifications') }, - { icon: Settings, label: 'Audit Logs', onClick: () => navigate('/audit-logs') }, + { + icon: Plus, + label: "New Tenant", + onClick: () => navigate("/tenants/create-wizard"), + }, + { icon: UserPlus, label: "Module", onClick: () => navigate("/modules") }, + { + icon: Shield, + label: "Notification", + onClick: () => navigate("/notifications"), + }, + { + icon: Settings, + label: "Audit Logs", + onClick: () => navigate("/audit-logs"), + }, ]; const tenantAdminActions: QuickAction[] = [ - hasPermission('users', 'create') && { icon: UserPlus, label: 'New User', onClick: () => navigate('/tenant/users') }, - hasPermission('roles', 'create') && { icon: Shield, label: 'New Role', onClick: () => navigate('/tenant/roles') }, - hasPermission('departments', 'create') && { icon: Building2, label: 'New Dept', onClick: () => navigate('/tenant/departments') }, - hasPermission('designations', 'create') && { icon: BadgeCheck, label: 'New Desig', onClick: () => navigate('/tenant/designations') }, + hasPermission("users", "create") && { + icon: UserPlus, + label: "New User", + onClick: () => navigate("/tenant/users"), + }, + hasPermission("roles", "create") && { + icon: Shield, + label: "New Role", + onClick: () => navigate("/tenant/roles"), + }, + hasPermission("departments", "create") && { + icon: Building2, + label: "New Dept", + onClick: () => navigate("/tenant/departments"), + }, + hasPermission("designations", "create") && { + icon: BadgeCheck, + label: "New Desig", + onClick: () => navigate("/tenant/designations"), + }, ].filter(Boolean) as QuickAction[]; const actions = isSuperAdmin ? superAdminActions : tenantAdminActions; - return ( -
-
-

Quick Actions

- +
+ {/* Header */} +
+

+ Quick Actions +

-
+ {/* Actions Grid */} +
{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 1cbd76d..8b21602 100644 --- a/src/features/dashboard/components/RecentActivity.tsx +++ b/src/features/dashboard/components/RecentActivity.tsx @@ -1,49 +1,50 @@ -import { useState, useEffect } from 'react'; -import { Loader2, User, ArrowRight } from 'lucide-react'; -import { useNavigate } from 'react-router-dom'; -import { auditLogService } from '@/services/audit-log-service'; -import type { AuditLog } from '@/types/audit-log'; -import { useAppSelector } from '@/hooks/redux-hooks'; -import { useAppTheme } from '@/hooks/useAppTheme'; -import { cn } from '@/lib/utils'; -import { Card, CardHeader, CardContent } from '@/components/ui/card'; -import { StatusBadge } from '@/components/shared'; -import { Button } from '@/components/ui/button'; +import { useState, useEffect, useMemo } from "react"; +import { Loader2, User, ArrowRight } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { auditLogService } from "@/services/audit-log-service"; +import type { AuditLog } from "@/types/audit-log"; +import { useAppSelector } from "@/hooks/redux-hooks"; +import { useAppTheme } from "@/hooks/useAppTheme"; +import { cn } from "@/lib/utils"; +import { StatusBadge, DataTable, type Column } from "@/components/shared"; +import { Button } from "@/components/ui/button"; // Helper functions 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 'Just now'; + if (diffInSeconds < 60) return "Just now"; const diffInMinutes = Math.floor(diffInSeconds / 60); if (diffInMinutes < 60) return `${diffInMinutes} min ago`; const diffInHours = Math.floor(diffInMinutes / 60); if (diffInHours < 24) return `${diffInHours} hours ago`; const diffInDays = Math.floor(diffInHours / 24); - if (diffInDays === 1) return 'Yesterday'; + if (diffInDays === 1) return "Yesterday"; return date.toLocaleDateString(); }; const getStatusColor = (status: number | null): string => { - if (!status) return 'text-[#6b7280]'; - if (status >= 200 && status < 300) return 'text-[#10b981]'; - if (status >= 400) return 'text-[#ef4444]'; - return 'text-[#f59e0b]'; + if (!status) return "text-[#6b7280]"; + if (status >= 200 && status < 300) return "text-[#10b981]"; + if (status >= 400) return "text-[#ef4444]"; + return "text-[#f59e0b]"; }; -const getMethodVariant = (method: string | null): 'success' | 'failure' | 'info' | 'process' => { - if (!method) return 'info'; +const getMethodVariant = ( + method: string | null, +): "success" | "failure" | "info" | "process" => { + if (!method) return "info"; const upperMethod = method.toUpperCase(); - if (upperMethod === 'GET') return 'success'; - if (upperMethod === 'POST') return 'info'; - if (upperMethod === 'PUT' || upperMethod === 'PATCH') return 'process'; - if (upperMethod === 'DELETE') return 'failure'; - return 'info'; + if (upperMethod === "GET") return "success"; + if (upperMethod === "POST") return "info"; + if (upperMethod === "PUT" || upperMethod === "PATCH") return "process"; + if (upperMethod === "DELETE") return "failure"; + return "info"; }; export interface RecentActivityProps { - variant?: 'list' | 'table'; + variant?: "list" | "table"; } export const RecentActivity = ({ variant }: RecentActivityProps) => { @@ -53,10 +54,12 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => { const { tenantId, roles } = useAppSelector((state) => state.auth); const navigate = useNavigate(); - const auditLogPath = roles?.includes('super_admin') ? '/audit-logs' : '/tenant/audit-logs'; + const auditLogPath = roles?.includes("super_admin") + ? "/audit-logs" + : "/tenant/audit-logs"; // Default to table variant for a more professional look - const activeVariant = variant || 'table'; + const activeVariant = variant || "table"; useEffect(() => { const fetchRecentActivity = async (): Promise => { @@ -67,7 +70,7 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => { setAuditLogs(response.data); } } catch (err: any) { - console.error('Failed to fetch recent activity:', err); + console.error("Failed to fetch recent activity:", err); } finally { setIsLoading(false); } @@ -76,15 +79,73 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => { fetchRecentActivity(); }, [tenantId, activeVariant]); - if (activeVariant === 'list') { + const columns = useMemo[]>(() => [ + { + key: "created_at", + label: "Timestamp", + render: (log) => ( + + {formatRelativeTime(log.created_at)} + + ), + }, + { + key: "resource_type", + label: "Resource Type", + render: (log) => ( + + {log.resource_type} + + ), + }, + { + key: "request_method", + label: "Method", + render: (log) => ( + + {log.request_method || "N/A"} + + ), + }, + { + key: "response_status", + label: "Status", + render: (log) => ( + + {log.response_status || "---"} + + ), + }, + { + key: "ip_address", + label: "IP Address", + render: (log) => ( + + {log.ip_address || "---"} + + ), + }, + ], [primaryColor]); + + if (activeVariant === "list") { return ( -
-
-

Recent Activity

-
-
+
{isLoading ? (
- +
) : auditLogs.length === 0 ? ( -
No recent activity
+
+ No recent activity +
) : (
{auditLogs.map((log, index) => ( -
- {formatRelativeTime(log.created_at)} +
+ + {formatRelativeTime(log.created_at)} +
- {log.action} - + {log.action} + + @@ -129,74 +206,30 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => { // TABLE VARIANT return ( - - -

Recent Activity

- -
- - {isLoading ? ( -
- -
- ) : ( -
- - - - - - - - - - - - {auditLogs.map((log) => ( - - - - - - - - ))} - {auditLogs.length === 0 && ( - - - - )} - -
TimestampResource TypeMethodStatusIP Address
- {formatRelativeTime(log.created_at)} - - - {log.resource_type} - - - - {log.request_method || 'N/A'} - - - - {log.response_status || '---'} - - - {log.ip_address || '---'} -
No recent activity recorded
-
- )} -
-
+
+ +
+ log.id} + isLoading={isLoading} + emptyMessage="No recent activity recorded" + /> +
+
); }; diff --git a/src/features/dashboard/components/SystemHealth.tsx b/src/features/dashboard/components/SystemHealth.tsx index 4ec193b..40e61ae 100644 --- a/src/features/dashboard/components/SystemHealth.tsx +++ b/src/features/dashboard/components/SystemHealth.tsx @@ -1,60 +1,60 @@ -import { Activity } from 'lucide-react'; -import { Card, CardHeader, CardContent } from '@/components/ui/card'; -import { cn } from '@/lib/utils'; -import type { HealthMetric } from '@/types/dashboard'; +// import { Activity } from 'lucide-react'; +// import { Card, CardHeader, CardContent } from '@/components/ui/card'; +// import { cn } from '@/lib/utils'; +// import type { HealthMetric } from '@/types/dashboard'; -const healthMetrics: HealthMetric[] = [ - { label: 'API Latency', value: '45ms', percentage: 100, variant: 'success' }, - { label: 'Database Load', value: '42%', percentage: 42, variant: 'info' }, - { label: 'Storage Usage', value: '78%', percentage: 78, variant: 'warning' }, -]; +// const healthMetrics: HealthMetric[] = [ +// { label: 'API Latency', value: '45ms', percentage: 100, variant: 'success' }, +// { label: 'Database Load', value: '42%', percentage: 42, variant: 'info' }, +// { label: 'Storage Usage', value: '78%', percentage: 78, variant: 'warning' }, +// ]; -const getVariantStyles = (variant: HealthMetric['variant']) => { - switch (variant) { - case 'success': - return { text: 'text-[#059669]', bg: 'bg-[#059669]' }; - case 'info': - return { text: 'text-[#23dce1]', bg: 'bg-[#23dce1]' }; - case 'warning': - return { text: 'text-[#f59e0b]', bg: 'bg-[#f59e0b]' }; - } -}; +// const getVariantStyles = (variant: HealthMetric['variant']) => { +// switch (variant) { +// case 'success': +// return { text: 'text-[#059669]', bg: 'bg-[#059669]' }; +// case 'info': +// return { text: 'text-[#23dce1]', bg: 'bg-[#23dce1]' }; +// case 'warning': +// return { text: 'text-[#f59e0b]', bg: 'bg-[#f59e0b]' }; +// } +// }; -export const SystemHealth = () => { - return ( - - -

System Health

- -
- -
- {healthMetrics.map((metric, index) => { - const styles = getVariantStyles(metric.variant); - return ( -
-
- - {metric.label} - - - {metric.value} - -
-
-
-
-
- ); - })} -
- - - ); -}; +// export const SystemHealth = () => { +// return ( +// +// +//

System Health

+// +//
+// +//
+// {healthMetrics.map((metric, index) => { +// const styles = getVariantStyles(metric.variant); +// return ( +//
+//
+// +// {metric.label} +// +// +// {metric.value} +// +//
+//
+//
+//
+//
+// ); +// })} +//
+// +// +// ); +// }; diff --git a/src/pages/tenant/Dashboard.tsx b/src/pages/tenant/Dashboard.tsx index 409fb40..6e5295e 100644 --- a/src/pages/tenant/Dashboard.tsx +++ b/src/pages/tenant/Dashboard.tsx @@ -1,19 +1,16 @@ import { Layout } from "@/components/layout/Layout"; import type { ReactElement } from "react"; -import { - FileCheck, - Briefcase, - FileText, - Users, - Bell, -} from "lucide-react"; +import { FileCheck, Briefcase, FileText, Users, Bell } from "lucide-react"; import { QuickActions } from "@/features/dashboard/components/QuickActions"; import { RecentActivity } from "@/features/dashboard/components/RecentActivity"; import { cn } from "@/lib/utils"; import { useState, useEffect } from "react"; import { useAppTheme } from "@/hooks/useAppTheme"; import { workflowService } from "@/services/workflow-service"; -import { dashboardService, type TenantDashboardStats } from "@/services/dashboard-service"; +import { + dashboardService, + type TenantDashboardStats, +} from "@/services/dashboard-service"; import type { WorkflowTask } from "@/types/workflow"; import { useNavigate } from "react-router-dom"; import { GradientStatCard } from "@/components/shared"; @@ -25,148 +22,86 @@ interface StatCardProps { label: string; badge?: { text: string; - variant: 'success' | 'warning' | 'info' | 'error'; + variant: "success" | "warning" | "info" | "error"; }; } const TaskCard = ({ task }: { task: WorkflowTask }) => { - // const { primaryColor } = useAppTheme(); const navigate = useNavigate(); const formatDeadline = (dueDate: string) => { const now = new Date(); const due = new Date(dueDate); const diffTime = due.getTime() - now.getTime(); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); - - if (diffDays === 0) return 'Due today'; - if (diffDays === 1) return 'Tomorrow'; - if (diffDays < 0) return 'Overdue'; + + if (diffDays === 0) return "Due today"; + if (diffDays === 1) return "Tomorrow"; + if (diffDays < 0) return "Overdue"; return `In ${diffDays} Days`; }; const handleView = () => { - if (task.entity.type.toLowerCase() === 'document') { + if (task.entity.type.toLowerCase() === "document") { navigate(`/tenant/documents/${task.entity.id}`); } }; return ( -
-
- {task.entity.type} - {formatDeadline(task.due_at)} +
+
+ + {task.entity.type} + + + {formatDeadline(task.due_at)} +
-
{task.entity.name}
+
+ {task.entity.name} +
-
-
-
- - {task.step.name} • { - (() => { - const role = task.assignment?.assigned_role; - if (role) return Array.isArray(role) ? role.join(", ") : role; - return task.assignment?.assigned_to_name || 'Unassigned'; - })() - } +
+
+
+ + {task.step.name} •{" "} + {(() => { + const role = task.assignment?.assigned_role; + if (role) return Array.isArray(role) ? role.join(", ") : role; + return task.assignment?.assigned_to_name || "Unassigned"; + })()}
-
- - {/* */} -
+
); }; -/* -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 { primaryColor } = useAppTheme(); const navigate = useNavigate(); + const { primaryColor } = useAppTheme(); const [tasks, setTasks] = useState([]); + const [tasksLoading, setTasksLoading] = useState(true); const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); - const [tasksLoading, setTasksLoading] = useState(true); useEffect(() => { fetchDashboardData(); @@ -176,15 +111,15 @@ const Dashboard = (): ReactElement => { setLoading(true); setTasksLoading(true); try { - // Fetch tasks independently to avoid one failing the other - workflowService.listTasks({ limit: 3 }) - .then(response => { - console.log("[Dashboard] Tasks response:", response); + // Fetch tasks independently + workflowService + .listTasks({ limit: 3 }) + .then((response) => { if (response.success && Array.isArray(response.data)) { setTasks(response.data); } }) - .catch(error => { + .catch((error) => { console.error("Error fetching tasks:", error); }) .finally(() => { @@ -192,13 +127,14 @@ const Dashboard = (): ReactElement => { }); // Fetch statistics independently - dashboardService.getTenantStatistics() - .then(response => { + dashboardService + .getTenantStatistics() + .then((response) => { if (response.success) { setStats(response.data); } }) - .catch(error => { + .catch((error) => { console.error("Error fetching dashboard statistics:", error); }) .finally(() => { @@ -211,51 +147,42 @@ const Dashboard = (): ReactElement => { } }; - const statCards: StatCardProps[] = [ stats?.documentsCount !== undefined && { icon: FileText, value: stats.documentsCount, label: "Total Documents", - badge: { text: "Controlled", variant: "info" } + badge: { text: "Controlled", variant: "info" }, }, stats?.pendingTasks !== undefined && { icon: FileCheck, value: stats.pendingTasks, label: "My Tasks", - badge: { text: "Action Needed", variant: "warning" } + badge: { text: "Action Needed", variant: "warning" }, }, stats?.usersCount !== undefined && { icon: Users, value: stats.usersCount, label: "Total Users", - badge: { text: "Team Members", variant: "success" } + badge: { text: "Team Members", variant: "success" }, }, stats?.unreadNotificationsCount !== undefined && { icon: Bell, value: stats.unreadNotificationsCount, label: "Notifications", - badge: { text: "Unread", variant: "error" } + badge: { text: "Unread", variant: "error" }, }, stats?.activeModulesCount !== undefined && { icon: Briefcase, value: stats.activeModulesCount, label: "Running Modules", - badge: { text: "Operational", variant: "success" } + badge: { text: "Operational", variant: "success" }, }, - // Training Compliance is still static/placeholder for now - // { - // icon: GraduationCap, - // value: "94%", - // label: "Compliance", - // badge: { text: "Target Met", variant: "success" } - // }, ].filter(Boolean) as StatCardProps[]; return ( { >
{/* Main Content Area (Left) */} -
+
{/* Stats Grid */} -
+
{loading ? ( -
Loading statistics...
+
+ Loading statistics... +
) : statCards.length > 0 ? ( statCards.map((card, index) => ( )) ) : ( -
No statistics available
+
+ No statistics available +
)}
- {/* CAPA Summary Card (Commented out for now) */} - {/*
-
-

- CAPA Summary -

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

My Tasks

-
-
+
{tasksLoading ? ( -
Loading tasks...
+
+ Loading tasks... +
) : tasks.length > 0 ? ( - tasks.map((task) => ( - - )) + tasks.map((task) => ) ) : ( -
No pending tasks
+
+ No pending tasks +
)}
- {/* Quick Actions Card */}