feat: Implement role-based quick actions and enhance recent activity display with tenant filtering and UI refinements.

This commit is contained in:
Yashwin 2026-03-23 14:24:24 +05:30
parent 93ad8feea9
commit 4b76f71cf4
5 changed files with 414 additions and 213 deletions

View File

@ -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?: {

View File

@ -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 (
<Card className="w-[300px] shrink-0">
<CardHeader className="border-b border-[rgba(0,0,0,0.08)] pb-4 pt-4 px-5 h-12">
<h2 className="text-[15px] font-semibold text-[#0f1724] h-[19px]">Quick Actions</h2>
</CardHeader>
<CardContent className="p-4">
<div className="grid grid-cols-2 gap-4">
{quickActions.map((action, index) => {
const Icon = action.icon;
return (
<button
key={index}
onClick={action.onClick}
className="bg-white border border-dashed border-[rgba(0,0,0,0.08)] rounded-md p-[17px] flex flex-col gap-2 items-center justify-center hover:bg-gray-50 transition-colors"
>
<Icon className="w-6 h-6" />
<span className="text-xs font-medium text-[#0f1724] text-center h-4 leading-4">
{action.label}
</span>
</button>
);
})}
</div>
</CardContent>
</Card>
<div className="bg-white rounded-xl p-6 h-full flex flex-col shadow-sm border border-[#e5e7eb]">
<div className="flex justify-between items-center mb-6">
<h2 className="text-[16px] font-bold text-[#111827] tracking-tight">Quick Actions</h2>
<span className="w-1 h-1 bg-gray-200 rounded-full" />
</div>
<div className="grid grid-cols-2 gap-4">
{actions.map((action, index) => {
const Icon = action.icon;
return (
<button
key={index}
onClick={action.onClick}
className="bg-white border-2 border-dashed border-[#f1f5f9] hover:border-[#cbd5e1] hover:bg-gray-50 flex flex-col items-center justify-center p-4 min-h-[100px] rounded-xl transition-all gap-3 group"
>
<div className="w-8 h-8 rounded-full bg-gray-50 border border-gray-100 flex items-center justify-center group-hover:bg-white transition-colors overflow-hidden">
<Icon className="w-4 h-4 text-[#084cc8]" strokeWidth={2} />
</div>
<span className="text-[11px] font-bold text-[#111827] text-center leading-none">
{action.label}
</span>
</button>
);
})}
</div>
</div>
);
};

View File

@ -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<AuditLog[]>([]);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(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<void> => {
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 (
<div className="bg-white rounded-xl flex flex-col h-full border border-[#e5e7eb] shadow-sm overflow-hidden">
<div className="px-6 py-5 border-b border-[#f1f5f9] flex justify-between items-center">
<h2 className="text-[16px] font-bold text-[#111827] tracking-tight">Recent Activity</h2>
<span className="text-[11px] font-bold text-gray-400">Last 24 hours</span>
</div>
<div className="flex-1">
{isLoading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 text-[#084cc8] animate-spin" />
</div>
) : auditLogs.length === 0 ? (
<div className="p-12 text-center text-[12px] text-gray-400 font-medium">No recent activity</div>
) : (
<div className="flex flex-col">
{auditLogs.map((log, index) => (
<div key={log.id} className={cn("px-6 py-4 flex items-center gap-4 transition-colors hover:bg-gray-50", index !== auditLogs.length - 1 && "border-b border-[#f1f5f9 ]")}>
<span className="text-[11px] font-medium text-gray-400 w-20 shrink-0">{formatRelativeTime(log.created_at)}</span>
<div className="w-8 h-8 rounded-full bg-gray-100 border border-gray-200 flex items-center justify-center shrink-0 overflow-hidden">
<User className="w-4 h-4 text-gray-400" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-1.5 flex-wrap">
<span className="text-[12px] font-bold text-[#111827]">{log.user?.email?.split('@')[0] || 'System User'}</span>
<span className="text-[12px] font-medium text-gray-500 whitespace-nowrap">{log.action.toLowerCase().includes('create') ? 'created' : 'performed'}</span>
<span className="text-[12px] font-bold text-[#084cc8] hover:underline cursor-pointer truncate">{log.resource_type}</span>
</div>
</div>
</div>
))}
</div>
)}
</div>
</div>
);
}
// TABLE VARIANT (Original design for superadmin dashboard)
return (
<Card className="flex-1 border-[rgba(0,0,0,0.2)] w-full">
<Card className="flex-1 border-[rgba(0,0,0,0.12)] w-full">
<CardHeader className="border-b border-[rgba(0,0,0,0.08)] pb-3 md:pb-4 pt-3 md:pt-4 px-4 md:px-5 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 sm:gap-2">
<h2 className="text-sm md:text-[15px] font-semibold text-[#0f1724]">Recent Activity</h2>
<div className="flex items-center gap-2 w-full sm:w-auto">
@ -83,81 +114,57 @@ export const RecentActivity = () => {
<span className="text-[11px] font-normal text-[#6b7280]">All</span>
<ChevronDown className="w-3 h-3" />
</div>
<div className="w-px h-4 bg-[rgba(0,0,0,0.08)] hidden sm:block" />
<Button variant="ghost" size="sm" className="gap-1 px-1 min-h-[44px]">
<Filter className="w-3 h-3" />
<span className="text-[11px] font-normal text-[#6b7280] hidden md:inline">More filters</span>
<span className="text-[11px] font-normal text-[#6b7280] hidden md:inline">Filters</span>
</Button>
</div>
</CardHeader>
<CardContent className="p-0">
{isLoading && (
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 text-[#112868] animate-spin" />
</div>
)}
{error && (
<div className="p-4 text-center">
<p className="text-xs text-[#ef4444]">{error}</p>
</div>
)}
{!isLoading && !error && (
) : (
<div className="overflow-x-auto">
{auditLogs.length === 0 ? (
<div className="p-4 text-center">
<p className="text-xs text-[#6b7280]">No recent activity found</p>
</div>
) : (
<table className="w-full border-collapse min-w-[600px]">
<thead>
<tr className="bg-white border-b border-[rgba(0,0,0,0.08)]">
<th className="px-3 md:px-5 py-2 md:py-3 text-left text-xs md:text-[13px] font-medium text-[#6b7280]">
Action
</th>
<th className="px-3 md:px-5 py-2 md:py-3 text-left text-xs md:text-[13px] font-medium text-[#6b7280]">
Resource Type
</th>
<th className="px-3 md:px-5 py-2 md:py-3 text-left text-xs md:text-[13px] font-medium text-[#6b7280]">
Resource ID
</th>
<th className="px-3 md:px-5 py-2 md:py-3 text-left text-xs md:text-[13px] font-medium text-[#6b7280]">
IP Address
</th>
<th className="px-3 md:px-5 py-2 md:py-3 text-left text-xs md:text-[13px] font-medium text-[#6b7280]">
Timestamp
</th>
<table className="w-full border-collapse">
<thead>
<tr className="bg-white border-b border-[rgba(0,0,0,0.08)]">
<th className="px-5 py-4 text-left text-xs font-bold text-[#374151] uppercase tracking-wider">User Profile</th>
<th className="px-5 py-4 text-left text-xs font-bold text-[#374151] uppercase tracking-wider">Timestamp</th>
<th className="px-5 py-4 text-left text-xs font-bold text-[#374151] uppercase tracking-wider">Resource Type</th>
<th className="px-5 py-4 text-left text-xs font-bold text-[#374151] uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody className="divide-y divide-[rgba(0,0,0,0.04)]">
{auditLogs.map((log) => (
<tr key={log.id} className="hover:bg-gray-50/50 transition-colors">
<td className="px-5 py-4 whitespace-nowrap">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-gray-100 flex items-center justify-center shrink-0 border border-gray-200">
<User className="w-4 h-4 text-gray-400" />
</div>
<div className="flex flex-col">
<span className="text-[13px] font-bold text-[#111827]">
{log.user ? `${log.user.first_name || ''} ${log.user.last_name || ''}`.trim() || log.user.email.split('@')[0] : 'System User'}
</span>
{log.user && <span className="text-[10px] text-gray-400 font-medium">{log.user.email}</span>}
</div>
</div>
</td>
<td className="px-5 py-4 whitespace-nowrap">
<span className="text-[12px] font-medium text-gray-500">{formatRelativeTime(log.created_at)}</span>
</td>
<td className="px-5 py-4 whitespace-nowrap">
<span className="text-[12px] font-bold text-[#084cc8]">{log.resource_type}</span>
</td>
<td className="px-5 py-4 whitespace-nowrap">
<StatusBadge variant={getActionVariant(log.action)}>{log.action}</StatusBadge>
</td>
</tr>
</thead>
<tbody>
{auditLogs.map((log) => (
<tr
key={log.id}
className="border-b border-[rgba(0,0,0,0.08)] last:border-b-0"
>
<td className="px-3 md:px-5 py-2.5 md:py-3.5">
<StatusBadge variant={getActionVariant(log.action)}>
{log.action}
</StatusBadge>
</td>
<td className="px-3 md:px-5 py-2.5 md:py-3.5 text-xs md:text-[13px] font-medium text-[#0f1724]">
{log.resource_type}
</td>
<td className="px-3 md:px-5 py-2.5 md:py-3.5 text-xs md:text-[13px] font-normal text-[#0f1724] whitespace-nowrap">
{log.resource_id || 'N/A'}
</td>
<td className="px-3 md:px-5 py-2 md:py-4 text-xs md:text-[13px] font-normal text-[#6b7280]">
{log.ip_address || 'N/A'}
</td>
<td className="px-3 md:px-5 py-2.5 md:py-3.5 text-xs md:text-[13px] font-normal text-[#6b7280] whitespace-nowrap">
{formatRelativeTime(log.created_at)}
</td>
</tr>
))}
</tbody>
</table>
)}
))}
</tbody>
</table>
</div>
)}
</CardContent>

View File

@ -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 (
<div className="relative p-[1px] rounded-lg" style={{ backgroundImage: 'linear-gradient(172.99deg, rgb(8, 76, 200) 1.15%, rgb(117, 192, 68) 44.3%, rgb(254, 211, 20) 89.74%)' }}>
<div className="bg-white border border-[#d1d5db] rounded-lg p-[17px] flex flex-col gap-3">
{/* Header with icon and status */}
<div className="relative group h-full">
<div className="bg-white border border-[#e5e7eb] rounded-xl p-4.5 flex flex-col gap-4 shadow-sm hover:shadow-md transition-all h-full relative overflow-hidden">
{/* Interaction Gradient */}
<div className="absolute top-0 left-0 w-full h-[2px] bg-gradient-to-r from-[#084cc8] via-[#75c044] to-[#fed314] opacity-0 group-hover:opacity-100 transition-opacity" />
<div className="flex items-start justify-between">
<Icon className="w-5 h-5 text-[#0f1724] shrink-0" />
<div className={`${config.bg} flex gap-1 items-center px-3 py-1 rounded-full`}>
<div className={`${config.dot} rounded-sm w-1.5 h-1.5`} />
<span className={`${config.text} text-xs font-medium capitalize`}>{statusLabel}</span>
<div className="w-10 h-10 rounded-md bg-gray-50 flex items-center justify-center border border-gray-100">
<Icon className="w-5 h-5 text-[#374151]" strokeWidth={1.5} />
</div>
{badge && (
<div className={cn(
"px-2.5 py-1 rounded-full text-[10px] font-bold tracking-tight whitespace-nowrap",
badge.variant === 'success' ? "bg-[#f1fffb] text-[#16c784]" :
badge.variant === 'warning' ? "bg-[#fff5e5] text-[#fca004]" :
badge.variant === 'info' ? "bg-[#f0f9ff] text-[#0ea5e9]" :
"bg-[#fdf5f4] text-[#e0352a]"
)}>
{badge.text}
</div>
)}
</div>
{/* Value and Label */}
<div className="flex flex-col gap-0">
<div className={`text-2xl font-bold tracking-[-0.48px] ${valueColor}`}>
<div className="flex flex-col gap-0.5">
<div className="text-[28px] font-bold tracking-tight text-[#111827] leading-none">
{value}
</div>
<div className="text-xs font-medium text-[#6b7280]">
<div className="text-[11px] font-semibold text-[#6b7280] uppercase tracking-wider mt-1">
{label}
</div>
</div>
@ -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;
}) => (
<div className="border border-[#e5e7eb] rounded-xl p-4 flex flex-col gap-3 bg-white hover:border-[#cbd5e1] transition-colors">
<div className="flex justify-between items-center">
<span className="text-[10px] font-bold text-gray-400 uppercase tracking-widest leading-none">{type}</span>
<span className={cn(
"text-[10px] font-bold",
deadlineLabel === 'Due today' ? "text-[#ef4444]" : "text-gray-400"
)}>{deadlineLabel}</span>
</div>
<div className="text-[13px] font-semibold text-[#111827] leading-tight">{title}</div>
<div className="flex flex-wrap items-center justify-between gap-3 overflow-hidden mt-1">
<div className="flex items-center gap-1.5 shrink-0">
<div className={cn(
"w-2 h-2 rounded-full",
priority === 'High' ? "bg-[#ef4444]" : priority === 'Medium' ? "bg-[#f59e0b]" : "bg-[#10b981]"
)} />
<span className="text-[11px] text-[#6b7280] font-medium leading-none">{priority} Owner: You</span>
</div>
<div className="flex items-center gap-2">
<button className="text-[11px] px-2.5 py-1.5 border border-[#e5e7eb] rounded-md font-bold text-[#374151] hover:bg-gray-50 transition-colors shrink-0">View</button>
<button className="text-[11px] px-2.5 py-1.5 bg-[#084cc8] text-white rounded-md font-bold hover:bg-[#063ba1] transition-colors shadow-sm shadow-[#084cc8]/20 shrink-0">
Complete
</button>
</div>
</div>
</div>
);
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 (
<div className="h-[320px] w-full">
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={data}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#f1f5f9" />
<XAxis
dataKey="name"
axisLine={false}
tickLine={false}
tick={{ fontSize: 11, fill: '#64748b', fontWeight: 500 }}
dy={10}
/>
<YAxis
axisLine={false}
tickLine={false}
tick={{ fontSize: 11, fill: '#64748b', fontWeight: 500 }}
/>
<Tooltip
contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1)', fontSize: '12px' }}
/>
<Legend
verticalAlign="bottom"
align="left"
wrapperStyle={{ paddingTop: '24px', fontSize: '11px', fontWeight: 600, color: '#64748b' }}
iconType="circle"
iconSize={8}
/>
<Bar dataKey="open" name="Open CAPA" fill="#94a3b8" radius={[2, 2, 0, 0]} barSize={8} />
<Bar dataKey="inProgress" name="In Progress" fill="#6366f1" radius={[2, 2, 0, 0]} barSize={8} />
<Bar dataKey="closed" name="Closed" fill="#1e1b4b" radius={[2, 2, 0, 0]} barSize={8} />
<Line
type="monotone"
dataKey="trend"
name="Total Trend"
stroke="#06b6d4"
strokeWidth={2}
dot={false}
/>
</ComposedChart>
</ResponsiveContainer>
</div>
);
};
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 (
<Layout
currentPage="Dashboard Overview"
currentPage="Dashboard"
breadcrumbs={[{ label: "QAssure - Tenant" }, { label: "Dashboard" }]}
pageHeader={{
title: 'Tenant Overview',
description: 'Key quality metrics and performance indicators.',
title: "Tenant Overview",
description: "Key quality metrics and performance indicators.",
}}
>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{statCards.map((card, index) => (
<StatCard key={index} {...card} />
))}
<div className="grid grid-cols-1 lg:grid-cols-12 gap-4 lg:gap-6 pb-8">
{/* Main Content Area (Left) */}
<div className="lg:col-span-8 xl:col-span-9 flex flex-col gap-6">
{/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{statCards.map((card, index) => (
<StatCard key={index} {...card} />
))}
</div>
{/* CAPA Summary Card */}
<div className="bg-white border border-[#e5e7eb] rounded-xl p-6 shadow-sm">
<div className="flex justify-between items-center mb-8">
<h2 className="text-[16px] font-bold text-[#111827] tracking-tight">
CAPA Summary
</h2>
<div className="flex items-center gap-3">
<span className="text-[11px] font-bold text-gray-400 tracking-wide">Data Range</span>
<select className="text-[11px] font-bold border border-[#e2e8f0] rounded-md px-3 py-1.5 focus:outline-none focus:ring-2 focus:ring-[#084cc8]/10 bg-white">
<option>Last Year</option>
<option>Last 6 Months</option>
<option>Last Month</option>
</select>
</div>
</div>
<CAPASummaryChart />
</div>
{/* Recent Activity Card */}
<RecentActivity />
</div>
{/* Sidebar area (Right) */}
<div className="lg:col-span-4 xl:col-span-3 flex flex-col gap-6">
{/* My Tasks Card */}
<div className="bg-white border border-[#e5e7eb] rounded-xl p-6 shadow-sm">
<div className="flex justify-between items-center mb-5">
<h2 className="text-[16px] font-bold text-[#111827] tracking-tight">My Tasks</h2>
<button className="text-[11px] font-bold text-[#084cc8] hover:underline">
View all
</button>
</div>
<div className="flex flex-col gap-3">
<TaskCard
type="CAPA"
title="Approve CAPA #CP-2024-021"
priority="High"
deadlineLabel="Due today"
/>
<TaskCard
type="DOCUMENT"
title="Review SOP-QA-110 v3"
priority="Medium"
deadlineLabel="Tomorrow"
/>
<TaskCard
type="TRAINING"
title="Complete Data Integrity module"
priority="Low"
deadlineLabel="In 5 Days"
/>
</div>
</div>
{/* Quick Actions Card */}
<QuickActions />
</div>
</div>
</Layout>
);

View File

@ -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;
}