diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index f1e0e86..839c2e7 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -155,14 +155,14 @@ export const Header = ({ breadcrumbs, currentPage, onMenuClick }: HeaderProps): {/* Right Side */}
- + */} {/* Desktop User Dropdown */} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index b43d1d9..655eac9 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -133,7 +133,6 @@ const tenantAdminSystemMenu: MenuItem[] = [ icon: FileText, label: "Audit Logs", path: "/tenant/audit-logs", - requiredPermission: { resource: "audit_logs" }, }, { icon: Settings, diff --git a/src/components/shared/NotificationBell.tsx b/src/components/shared/NotificationBell.tsx index 0ff1b75..7758708 100644 --- a/src/components/shared/NotificationBell.tsx +++ b/src/components/shared/NotificationBell.tsx @@ -60,6 +60,13 @@ export const NotificationBell = () => { }; }, [isOpen]); + // Fetch notifications when opened + useEffect(() => { + if (isOpen) { + dispatch(fetchNotifications({ limit: 20 })); + } + }, [isOpen, dispatch]); + const handleMarkAllRead = () => { dispatch(readAllAsync()); }; @@ -77,14 +84,17 @@ export const NotificationBell = () => { } // Special handling for system as requested - redirect to Dashboard - if (['system'].includes(notification.category || '')) { - navigate('/tenant'); - setIsOpen(false); - return; - } + // if (['system'].includes(notification.category || '')) { + // navigate('/tenant'); + // setIsOpen(false); + // return; + // } if (notification.action_url) { - navigate(notification.action_url); + const targetUrl = notification.action_url.startsWith('/tenant') + ? notification.action_url + : `/tenant${notification.action_url.startsWith('/') ? '' : '/'}${notification.action_url}`; + navigate(targetUrl); } setIsOpen(false); }; diff --git a/src/components/shared/NotificationProvider.tsx b/src/components/shared/NotificationProvider.tsx index 3439498..b86ea1e 100644 --- a/src/components/shared/NotificationProvider.tsx +++ b/src/components/shared/NotificationProvider.tsx @@ -1,7 +1,7 @@ import { useEffect, useCallback, createContext, useContext, useRef, type ReactNode } from 'react'; import { io, Socket } from 'socket.io-client'; import { useAppDispatch, useAppSelector } from '@/hooks/redux-hooks'; -import { addNotification, fetchNotifications, fetchUnreadCount, setUnreadCount, markReadLocal } from '@/store/notificationSlice'; +import { addNotification, fetchUnreadCount, setUnreadCount, markReadLocal } from '@/store/notificationSlice'; import { showToast } from '@/utils/toast'; import { useNavigate } from 'react-router-dom'; @@ -66,6 +66,23 @@ export const NotificationProvider = ({ children }: NotificationProviderProps) => } }, [dispatch]); + // Tracker for initial fetch to avoid duplicate calls on re-renders + const hasInitialized = useRef(false); + + // Initial fetch of unread count only (notifications list fetched on-demand) + useEffect(() => { + if (isAuthenticated && accessToken && !hasInitialized.current) { + console.log('NotificationProvider: Initial unread count fetch'); + dispatch(fetchUnreadCount()); + hasInitialized.current = true; + } + + // Reset initialization tracker on logout + if (!isAuthenticated) { + hasInitialized.current = false; + } + }, [isAuthenticated, accessToken, dispatch]); + useEffect(() => { if (!isAuthenticated || !accessToken) { if (socketRef.current) { @@ -78,8 +95,8 @@ export const NotificationProvider = ({ children }: NotificationProviderProps) => if (socketRef.current?.connected) return; - console.log('WebSocket: Connecting to', SOCKET_URL); // Connect to WebSocket + console.log('WebSocket: Connecting to', SOCKET_URL); const socket = io(SOCKET_URL, { auth: { token: accessToken @@ -87,11 +104,8 @@ export const NotificationProvider = ({ children }: NotificationProviderProps) => transports: ["websocket", "polling"], reconnection: true, reconnectionAttempts: 5, - reconnectionDelay: 1000 - }); - - socket.on('connected', (data) => { - console.log('WebSocket: Authenticated for user', data.userId); + reconnectionDelay: 1000, + autoConnect: true }); socket.on('notification', handleNotification); @@ -100,31 +114,16 @@ export const NotificationProvider = ({ children }: NotificationProviderProps) => socket.on('connect', () => { console.log('WebSocket: Connected'); - // Re-fetch on reconnect to avoid missing updates - dispatch(fetchNotifications({ limit: 20 })); - dispatch(fetchUnreadCount()); - }); - - socket.on('disconnect', (reason) => { - console.log('WebSocket: Disconnected', reason); - }); - - socket.on('connect_error', (err) => { - console.error('WebSocket: Connection error', err.message); + // If we've already initialized, re-fetch count on reconnect to be safe + if (hasInitialized.current) { + dispatch(fetchUnreadCount()); + } }); socketRef.current = socket; - // Initial fetch - dispatch(fetchNotifications({ limit: 20 })); - dispatch(fetchUnreadCount()); - return () => { - if (socketRef.current) { - console.log('WebSocket: Cleaning up connection'); - socketRef.current.disconnect(); - socketRef.current = null; - } + // We don't want to disconnect on every route change, so we persist the socketRef }; }, [isAuthenticated, accessToken, dispatch, handleNotification, handleUnreadCount, handleNotificationRead]); diff --git a/src/components/shared/WorkflowDefinitionModal.tsx b/src/components/shared/WorkflowDefinitionModal.tsx index 284f906..0b0391f 100644 --- a/src/components/shared/WorkflowDefinitionModal.tsx +++ b/src/components/shared/WorkflowDefinitionModal.tsx @@ -649,7 +649,7 @@ export const WorkflowDefinitionModal = ({
{ const navigate = useNavigate(); - const { roles } = useAppSelector((state) => state.auth); + const { roles, permissions } = useAppSelector((state) => state.auth); - // 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 = []; - } - } + // 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); + }; - const isSuperAdmin = rolesArray.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: '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') }, + { 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[] = [ - { 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') }, - ]; + 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 (
diff --git a/src/features/dashboard/components/RecentActivity.tsx b/src/features/dashboard/components/RecentActivity.tsx index 139fd93..83d3e39 100644 --- a/src/features/dashboard/components/RecentActivity.tsx +++ b/src/features/dashboard/components/RecentActivity.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; -import { ChevronDown, Filter, Loader2, User } from 'lucide-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'; @@ -8,16 +9,7 @@ import { Card, CardHeader, CardContent } from '@/components/ui/card'; import { StatusBadge } from '@/components/shared'; import { Button } from '@/components/ui/button'; -// 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'; - if (lowerAction.includes('update') || lowerAction.includes('version_update') || lowerAction.includes('login')) return 'info'; - if (lowerAction.includes('delete') || lowerAction.includes('deregister')) return 'failure'; - if (lowerAction.includes('read') || lowerAction.includes('get') || lowerAction.includes('status_change')) return 'process'; - return 'info'; -}; - +// Helper functions const formatRelativeTime = (dateString: string): string => { const date = new Date(dateString); const now = new Date(); @@ -32,6 +24,23 @@ const formatRelativeTime = (dateString: string): string => { 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]'; +}; + +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'; +}; + export interface RecentActivityProps { variant?: 'list' | 'table'; } @@ -40,6 +49,7 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => { const [auditLogs, setAuditLogs] = useState([]); const [isLoading, setIsLoading] = useState(true); const { tenantId } = useAppSelector((state) => state.auth); + const navigate = useNavigate(); // Default to table variant for a more professional look const activeVariant = variant || 'table'; @@ -48,14 +58,12 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => { const fetchRecentActivity = async (): Promise => { try { setIsLoading(true); - // 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, { tenant_id: filterTenantId }, ['created_at', 'desc']); + const response = await auditLogService.getMyLogs(1, 5); if (response.success) { setAuditLogs(response.data); } } catch (err: any) { - // Fallback or log if needed + console.error('Failed to fetch recent activity:', err); } finally { setIsLoading(false); } @@ -69,7 +77,14 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {

Recent Activity

- Last 24 hours +
@@ -89,8 +104,7 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
- {log.user?.email?.split('@')[0] || 'System User'} - {log.action.toLowerCase().includes('create') ? 'created' : 'performed'} + {log.action} {log.resource_type}
@@ -103,22 +117,19 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => { ); } - // TABLE VARIANT (Original design for superadmin dashboard) + // TABLE VARIANT return ( - +

Recent Activity

-
-
- Action - All - -
- -
+
{isLoading ? ( @@ -129,40 +140,43 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
- - - - - + + + + + + {auditLogs.map((log) => ( - - + + + ))} + {auditLogs.length === 0 && ( + + + + )}
User ProfileTimestampResource TypeAction
TimestampResource TypeMethodStatusIP Address
-
-
- -
-
- - {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.resource_type} - {log.action} + + {log.request_method || 'N/A'} + + + + {log.response_status || '---'} + + + {log.ip_address || '---'}
No recent activity recorded
diff --git a/src/pages/superadmin/Dashboard.tsx b/src/pages/superadmin/Dashboard.tsx index ed29824..6e001f6 100644 --- a/src/pages/superadmin/Dashboard.tsx +++ b/src/pages/superadmin/Dashboard.tsx @@ -3,7 +3,7 @@ import { Layout } from '@/components/layout/Layout'; import { StatsGrid } from '@/features/dashboard/components/StatsGrid'; import { RecentActivity } from '@/features/dashboard/components/RecentActivity'; import { QuickActions } from '@/features/dashboard/components/QuickActions'; -import { SystemHealth } from '@/features/dashboard/components/SystemHealth'; +// import { SystemHealth } from '@/features/dashboard/components/SystemHealth'; const Dashboard = (): ReactElement => { return ( @@ -22,7 +22,7 @@ const Dashboard = (): ReactElement => {
- + {/* */}
diff --git a/src/pages/superadmin/Modules.tsx b/src/pages/superadmin/Modules.tsx index d2a72a8..eeb3c6a 100644 --- a/src/pages/superadmin/Modules.tsx +++ b/src/pages/superadmin/Modules.tsx @@ -329,13 +329,13 @@ const Modules = (): ReactElement => { {/* Actions */}
{/* Export Button */} - + */} {/* New Module Button */} { {/* Actions */}
{/* Export Button */} - + */} {/* New Tenant Button (Old) - Commented out, using wizard instead */} {/* { task.is_overdue ? "bg-[#ef4444]" : "bg-[#10b981]" )} /> - {task.step.name} • {task.assignment.assigned_role} + {task.step.name} • {task.assignment?.assigned_role || task.assignment?.assigned_to_name || 'Unassigned'}
@@ -137,6 +128,7 @@ const TaskCard = ({ task }: { task: WorkflowTask }) => { ); }; +/* const CAPASummaryChart = () => { const data = [ { name: 'Jan', open: 35, inProgress: 48, closed: 25, trend: 15 }, @@ -196,68 +188,98 @@ const CAPASummaryChart = () => {
); }; +*/ const Dashboard = (): ReactElement => { const navigate = useNavigate(); const [tasks, setTasks] = useState([]); + const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); + const [tasksLoading, setTasksLoading] = useState(true); useEffect(() => { - fetchTasks(); + fetchDashboardData(); }, []); - const fetchTasks = async () => { + const fetchDashboardData = async () => { + setLoading(true); + setTasksLoading(true); try { - setLoading(true); - const response = await workflowService.listTasks({ limit: 3 }); - if (response.success) { - setTasks(response.data); - } + // Fetch tasks independently to avoid one failing the other + workflowService.listTasks({ limit: 3 }) + .then(response => { + console.log("[Dashboard] Tasks response:", response); + if (response.success && Array.isArray(response.data)) { + setTasks(response.data); + } + }) + .catch(error => { + console.error("Error fetching tasks:", error); + }) + .finally(() => { + setTasksLoading(false); + }); + + // Fetch statistics independently + dashboardService.getTenantStatistics() + .then(response => { + if (response.success) { + setStats(response.data); + } + }) + .catch(error => { + console.error("Error fetching dashboard statistics:", error); + }) + .finally(() => { + setLoading(false); + }); } catch (error) { - console.error("Error fetching tasks:", error); - } finally { + console.error("Critical error in fetchDashboardData:", error); setLoading(false); + setTasksLoading(false); } }; + const statCards: StatCardProps[] = [ - { - icon: Info, - value: 18, - label: "Open CAPAs", - badge: { text: "2 New This Week", variant: "success" } - }, - { - icon: FileCheck, - value: 7, - label: "Pending Approvals", - badge: { text: "Awaiting Manager Review", variant: "warning" } - }, - { - icon: Briefcase, - value: 9, - label: "Active Projects", - badge: { text: "3 At Risk Of Delay", variant: "error" } - }, - { - icon: Info, - value: 3, - label: "Overdue Tasks", - badge: { text: "Action Needed", variant: "error" } - }, - { + stats?.documentsCount !== undefined && { icon: FileText, - value: 14, - label: "Docs Pending Review", - badge: { text: "Due Within 7 Days", variant: "info" } + value: stats.documentsCount, + label: "Total Documents", + badge: { text: "Controlled", variant: "info" } }, - { - icon: GraduationCap, - value: "94%", - label: "Training Compliance", - badge: { text: "Target Met", variant: "success" } + stats?.pendingTasks !== undefined && { + icon: FileCheck, + value: stats.pendingTasks, + label: "My Tasks", + badge: { text: "Action Needed", variant: "warning" } }, - ]; + stats?.usersCount !== undefined && { + icon: Users, + value: stats.usersCount, + label: "Total Users", + badge: { text: "Team Members", variant: "success" } + }, + stats?.unreadNotificationsCount !== undefined && { + icon: Bell, + value: stats.unreadNotificationsCount, + label: "Notifications", + badge: { text: "Unread", variant: "error" } + }, + stats?.activeModulesCount !== undefined && { + icon: Briefcase, + value: stats.activeModulesCount, + label: "Running Modules", + 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 ( {
{/* Stats Grid */}
- {statCards.map((card, index) => ( - - ))} + {loading ? ( +
Loading statistics...
+ ) : statCards.length > 0 ? ( + statCards.map((card, index) => ( + + )) + ) : ( +
No statistics available
+ )}
- {/* CAPA Summary Card */} -
+ {/* CAPA Summary Card (Commented out for now) */} + {/*

CAPA Summary @@ -295,7 +323,7 @@ const Dashboard = (): ReactElement => {

-
+
*/} {/* Recent Activity Card */} @@ -316,7 +344,7 @@ const Dashboard = (): ReactElement => {
- {loading ? ( + {tasksLoading ? (
Loading tasks...
) : tasks.length > 0 ? ( tasks.map((task) => ( diff --git a/src/pages/tenant/Documents.tsx b/src/pages/tenant/Documents.tsx index 4e2b932..9a90654 100644 --- a/src/pages/tenant/Documents.tsx +++ b/src/pages/tenant/Documents.tsx @@ -11,7 +11,7 @@ import { import { documentService } from "@/services/document-service"; import { moduleService } from "@/services/module-service"; import type { DocumentCategory, DocumentSummary } from "@/types/document"; -import type { MyModule } from "@/types/module"; +import type { Module } from "@/types/module"; import { Plus, Search } from "lucide-react"; const formatDate = (value?: string | null): string => { @@ -42,7 +42,7 @@ const Documents = (): ReactElement => { const [categoryFilter, setCategoryFilter] = useState(null); const [typeFilter, setTypeFilter] = useState(null); const [moduleFilter, setModuleFilter] = useState(null); - const [modules, setModules] = useState([]); + const [modules, setModules] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [limit, setLimit] = useState(10); const [total, setTotal] = useState(0); @@ -59,7 +59,7 @@ const Documents = (): ReactElement => { documentService.getCategories(), documentService.getStatuses(), documentService.getTypes(), - moduleService.getMyModules(), + moduleService.getAvailable(), ]); setCategories(categoriesRes.data || []); setStatuses(statusesRes.data || []); diff --git a/src/pages/tenant/Notifications.tsx b/src/pages/tenant/Notifications.tsx index 1444782..4706e35 100644 --- a/src/pages/tenant/Notifications.tsx +++ b/src/pages/tenant/Notifications.tsx @@ -169,13 +169,16 @@ export const Notifications = () => { } // Special handling for system as requested - redirect to Dashboard - if (["system"].includes(notification.category || "")) { - navigate("/tenant"); - return; - } + // if (["system"].includes(notification.category || "")) { + // navigate("/tenant"); + // return; + // } if (notification.action_url) { - navigate(notification.action_url); + const targetUrl = notification.action_url.startsWith('/tenant') + ? notification.action_url + : `/tenant${notification.action_url.startsWith('/') ? '' : '/'}${notification.action_url}`; + navigate(targetUrl); } }; diff --git a/src/pages/tenant/Roles.tsx b/src/pages/tenant/Roles.tsx index be192ae..fb44c8c 100644 --- a/src/pages/tenant/Roles.tsx +++ b/src/pages/tenant/Roles.tsx @@ -342,13 +342,13 @@ const Roles = (): ReactElement => { {/* Actions */}
{/* Export Button */} - + */} {/* New Role Button */} {canCreate('roles') && ( diff --git a/src/pages/tenant/Users.tsx b/src/pages/tenant/Users.tsx index 144ca31..54e5d36 100644 --- a/src/pages/tenant/Users.tsx +++ b/src/pages/tenant/Users.tsx @@ -482,13 +482,13 @@ const Users = (): ReactElement => { {/* Actions */}
{/* Export Button */} - + */} {/* New User Button */} {canCreate("users") && ( diff --git a/src/services/dashboard-service.ts b/src/services/dashboard-service.ts index ee7508a..a30bba3 100644 --- a/src/services/dashboard-service.ts +++ b/src/services/dashboard-service.ts @@ -14,9 +14,29 @@ export interface DashboardStatisticsResponse { data: DashboardStatistics; } +export interface TenantDashboardStats { + documentsCount?: number; + pendingTasks?: number; + usersCount?: number; + unreadNotificationsCount?: number; + activeModulesCount?: number; + permissions: string[]; +} + +export interface TenantDashboardResponse { + success: boolean; + data: TenantDashboardStats; +} + export const dashboardService = { getStatistics: async (): Promise => { const response = await apiClient.get('/dashboard/statistics'); return response.data; }, + + getTenantStatistics: async (): Promise => { + const response = await apiClient.get('/dashboard/tenant'); + return response.data; + } }; + diff --git a/src/store/notificationSlice.ts b/src/store/notificationSlice.ts index bb94ef5..d82a260 100644 --- a/src/store/notificationSlice.ts +++ b/src/store/notificationSlice.ts @@ -67,6 +67,10 @@ const notificationSlice = createSlice({ // Add to notifications if not already present (avoid duplicates from WebSocket/initial load overlap) if (!state.notifications.find((n) => n.id === action.payload.id)) { state.notifications = [action.payload, ...state.notifications]; + // Increment unread count if it's a new unread notification + if (!action.payload.is_read) { + state.unread_count += 1; + } } }, setUnreadCount: (state, action: PayloadAction) => {