refactor: update UI styling and layout for dashboard task components

This commit is contained in:
Yashwin 2026-05-07 19:14:11 +05:30
parent d8d7e542d0
commit f4838422c2
4 changed files with 370 additions and 363 deletions

View File

@ -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 (
<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 className="flex flex-col items-start self-stretch rounded-[8px] border border-[#D1D5DB] bg-white overflow-hidden">
{/* Header */}
<div className="flex w-full h-[48px] px-[20px] py-[16px] items-center border-b border-[#D1D5DB]">
<h2 className="text-[16px] font-semibold text-[#111827] leading-none">
Quick Actions
</h2>
</div>
<div className="grid grid-cols-2 gap-4">
{/* Actions Grid */}
<div className="inline-grid grid-cols-2 gap-[10px] p-[16px] w-full">
{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"
className="
flex flex-col justify-center items-center
w-full min-h-[100px]
p-[16px]
gap-[8px]
rounded-[6px]
border border-dashed border-[#D1D5DB]
bg-white
transition-all duration-200
hover:bg-[#F9FAFB]
hover:border-[#9CA3AF]
"
>
<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" color={primaryColor} strokeWidth={2} />
{/* Icon */}
<div className="flex items-center justify-center">
<Icon
className="w-[20px] h-[20px]"
color={primaryColor}
strokeWidth={2}
/>
</div>
<span className="text-[11px] font-bold text-[#111827] text-center leading-none">
{/* Label */}
<span className="text-[14px] font-medium text-[#111827] text-center leading-[20px]">
{action.label}
</span>
</button>

View File

@ -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<void> => {
@ -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<Column<AuditLog>[]>(() => [
{
key: "created_at",
label: "Timestamp",
render: (log) => (
<span className="text-[12px] font-medium text-gray-500">
{formatRelativeTime(log.created_at)}
</span>
),
},
{
key: "resource_type",
label: "Resource Type",
render: (log) => (
<span
className="text-[12px] font-bold truncate max-w-[150px] inline-block"
style={{ color: primaryColor }}
>
{log.resource_type}
</span>
),
},
{
key: "request_method",
label: "Method",
render: (log) => (
<StatusBadge variant={getMethodVariant(log.request_method)}>
{log.request_method || "N/A"}
</StatusBadge>
),
},
{
key: "response_status",
label: "Status",
render: (log) => (
<span
className={cn(
"text-[12px] font-bold",
getStatusColor(log.response_status),
)}
>
{log.response_status || "---"}
</span>
),
},
{
key: "ip_address",
label: "IP Address",
render: (log) => (
<span className="text-[12px] font-mono text-gray-500">
{log.ip_address || "---"}
</span>
),
},
], [primaryColor]);
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>
<Button
variant="ghost"
size="sm"
className="text-[11px] font-bold gap-1 h-7"
<div className="flex flex-col items-start self-stretch rounded-[8px] border border-[#D1D5DB] bg-white overflow-hidden">
<div className="flex w-full h-[48px] px-[20px] py-[16px] items-center border-b border-[#D1D5DB]">
<h2 className="text-[15px] font-bold text-[#111827] tracking-tight">
Recent Activity
</h2>
<Button
variant="ghost"
size="sm"
className="text-[15px] font-bold gap-1 h-7"
style={{ color: primaryColor }}
onClick={() => navigate(auditLogPath)}
>
@ -92,25 +153,41 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
</Button>
</div>
<div className="flex-1">
<div className="flex-1 w-full">
{isLoading ? (
<div className="flex items-center justify-center py-20">
<Loader2 className="w-6 h-6 animate-spin" style={{ color: primaryColor }} />
<Loader2
className="w-6 h-6 animate-spin"
style={{ color: primaryColor }}
/>
</div>
) : auditLogs.length === 0 ? (
<div className="p-12 text-center text-[12px] text-gray-400 font-medium">No recent activity</div>
<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
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">{log.action}</span>
<span
<span className="text-[12px] font-bold">
{log.action}
</span>
<span
className="text-[12px] font-bold hover:underline cursor-pointer truncate"
style={{ color: primaryColor }}
>
@ -129,74 +206,30 @@ export const RecentActivity = ({ variant }: RecentActivityProps) => {
// TABLE VARIANT
return (
<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-row items-center justify-between gap-2">
<h2 className="text-sm md:text-[15px] font-semibold text-[#0f1724]">Recent Activity</h2>
<Button
variant="outline"
size="sm"
<div className="flex flex-col items-start self-stretch rounded-[8px] border border-[#D1D5DB] bg-white overflow-hidden w-full">
<div className="flex w-full h-[48px] px-[20px] py-[16px] items-center border-b border-[#D1D5DB] justify-between">
<h2 className="text-[16px] font-semibold text-[#111827] leading-none">
Recent Activity
</h2>
<Button
variant="outline"
size="sm"
className="h-8 text-[11px] gap-1.5 border-[rgba(0,0,0,0.08)] hover:bg-gray-50"
onClick={() => navigate(auditLogPath)}
>
View All <ArrowRight className="w-3 h-3" />
</Button>
</CardHeader>
<CardContent className="p-0">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<Loader2 className="w-5 h-5 text-[#112868] animate-spin" />
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-gray-50/50 border-b border-[rgba(0,0,0,0.08)]">
<th className="px-5 py-3 text-left text-[11px] font-bold text-[#6b7280] uppercase tracking-wider">Timestamp</th>
<th className="px-5 py-3 text-left text-[11px] font-bold text-[#6b7280] uppercase tracking-wider">Resource Type</th>
<th className="px-5 py-3 text-left text-[11px] font-bold text-[#6b7280] uppercase tracking-wider">Method</th>
<th className="px-5 py-3 text-left text-[11px] font-bold text-[#6b7280] uppercase tracking-wider">Status</th>
<th className="px-5 py-3 text-left text-[11px] font-bold text-[#6b7280] uppercase tracking-wider">IP Address</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/30 transition-colors">
<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 truncate max-w-[150px] inline-block"
style={{ color: primaryColor }}
>
{log.resource_type}
</span>
</td>
<td className="px-5 py-4 whitespace-nowrap">
<StatusBadge variant={getMethodVariant(log.request_method)}>
{log.request_method || 'N/A'}
</StatusBadge>
</td>
<td className="px-5 py-4 whitespace-nowrap">
<span className={cn("text-[12px] font-bold", getStatusColor(log.response_status))}>
{log.response_status || '---'}
</span>
</td>
<td className="px-5 py-4 whitespace-nowrap">
<span className="text-[12px] font-mono text-gray-500">{log.ip_address || '---'}</span>
</td>
</tr>
))}
{auditLogs.length === 0 && (
<tr>
<td colSpan={5} className="px-5 py-10 text-center text-xs text-gray-400">No recent activity recorded</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
</div>
<div className="w-full">
<DataTable
data={auditLogs}
columns={columns}
keyExtractor={(log) => log.id}
isLoading={isLoading}
emptyMessage="No recent activity recorded"
/>
</div>
</div>
);
};

View File

@ -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 (
<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 flex items-center justify-between">
<h2 className="text-[15px] font-semibold text-[#0f1724] h-[19px]">System Health</h2>
<Activity className="w-4 h-4" />
</CardHeader>
<CardContent className="p-4">
<div className="flex flex-col gap-4">
{healthMetrics.map((metric, index) => {
const styles = getVariantStyles(metric.variant);
return (
<div key={index} className="flex flex-col gap-2">
<div className="flex items-center justify-between">
<span className="text-xs font-medium text-[#0f1724] h-4 leading-4">
{metric.label}
</span>
<span className={cn('text-xs font-medium h-4 leading-4', styles.text)}>
{metric.value}
</span>
</div>
<div className="bg-white border border-[rgba(0,0,0,0.2)] rounded-full h-1.5 overflow-hidden relative">
<div
className={cn(
'absolute top-[-1px] bottom-[-1px] left-[-1px] rounded-full',
styles.bg
)}
style={{ width: `${metric.percentage}%` }}
/>
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
);
};
// export const SystemHealth = () => {
// 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 flex items-center justify-between">
// <h2 className="text-[15px] font-semibold text-[#0f1724] h-[19px]">System Health</h2>
// <Activity className="w-4 h-4" />
// </CardHeader>
// <CardContent className="p-4">
// <div className="flex flex-col gap-4">
// {healthMetrics.map((metric, index) => {
// const styles = getVariantStyles(metric.variant);
// return (
// <div key={index} className="flex flex-col gap-2">
// <div className="flex items-center justify-between">
// <span className="text-xs font-medium text-[#0f1724] h-4 leading-4">
// {metric.label}
// </span>
// <span className={cn('text-xs font-medium h-4 leading-4', styles.text)}>
// {metric.value}
// </span>
// </div>
// <div className="bg-white border border-[rgba(0,0,0,0.2)] rounded-full h-1.5 overflow-hidden relative">
// <div
// className={cn(
// 'absolute top-[-1px] bottom-[-1px] left-[-1px] rounded-full',
// styles.bg
// )}
// style={{ width: `${metric.percentage}%` }}
// />
// </div>
// </div>
// );
// })}
// </div>
// </CardContent>
// </Card>
// );
// };

View File

@ -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 (
<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">{task.entity.type}</span>
<span className={cn(
"text-[10px] font-bold",
task.is_overdue ? "text-[#ef4444]" : "text-gray-400"
)}>{formatDeadline(task.due_at)}</span>
<div className="flex flex-col items-start self-stretch p-3 gap-2.5 rounded-[6px] border border-[#D1D5DB] bg-white hover:border-[#9CA3AF] transition-colors group">
<div className="flex justify-between items-start self-stretch">
<span className="text-[10px] font-bold text-[#94A3B8] uppercase tracking-[0.05em] leading-none">
{task.entity.type}
</span>
<span
className={cn(
"text-[10px] font-bold leading-none",
task.is_overdue ? "text-[#F87171]" : "text-[#94A3B8]",
)}
>
{formatDeadline(task.due_at)}
</span>
</div>
<div className="text-[13px] font-semibold text-[#111827] leading-tight">{task.entity.name}</div>
<div className="text-[14px] font-semibold text-[#1E293B] leading-[20px] self-stretch line-clamp-2">
{task.entity.name}
</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",
task.is_overdue ? "bg-[#ef4444]" : "bg-[#10b981]"
)} />
<span className="text-[11px] text-[#6b7280] font-medium leading-none">
{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';
})()
}
<div className="flex justify-between items-center self-stretch mt-0.5">
<div className="flex items-center gap-1.5 overflow-hidden mr-2">
<div
className={cn(
"w-2 h-2 rounded-full shrink-0",
task.is_overdue ? "bg-[#F87171]" : "bg-[#34D399]",
)}
/>
<span className="text-[12px] text-[#64748B] font-medium leading-[16px] truncate">
{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";
})()}
</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleView}
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 text-white rounded-md font-bold transition-colors shadow-sm"
style={{ backgroundColor: primaryColor, boxShadow: `0 2px 4px ${primaryColor}33` }}
>
Complete
</button> */}
</div>
<button
onClick={handleView}
className="flex px-3 py-1.5 justify-center items-center rounded-[4px] border border-[#D1D5DB] bg-white text-[12px] font-bold text-[#1E293B] hover:bg-[#F9FAFB] hover:border-[#9CA3AF] transition-all shrink-0"
>
View
</button>
</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 { primaryColor } = useAppTheme();
const navigate = useNavigate();
const { primaryColor } = useAppTheme();
const [tasks, setTasks] = useState<WorkflowTask[]>([]);
const [tasksLoading, setTasksLoading] = useState(true);
const [stats, setStats] = useState<TenantDashboardStats | null>(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 (
<Layout
currentPage="Dashboard"
// breadcrumbs={[{ label: "QAssure - Tenant" }, { label: "Dashboard" }]}
pageHeader={{
title: "Tenant Overview",
description: "Key quality metrics and performance indicators.",
@ -263,51 +190,38 @@ const Dashboard = (): ReactElement => {
>
<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">
<div className="lg:col-span-8 flex flex-col gap-6">
{/* Stats Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-5 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
{loading ? (
<div className="col-span-full text-center py-8 text-gray-400 text-sm">Loading statistics...</div>
<div className="col-span-full text-center py-8 text-gray-400 text-sm">
Loading statistics...
</div>
) : statCards.length > 0 ? (
statCards.map((card, index) => (
<GradientStatCard key={index} {...card} />
))
) : (
<div className="col-span-full text-center py-8 text-gray-400 text-sm">No statistics available</div>
<div className="col-span-full text-center py-8 text-gray-400 text-sm">
No statistics available
</div>
)}
</div>
{/* CAPA Summary Card (Commented out for now) */}
{/* <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">
<div className="lg:col-span-4 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
onClick={() => navigate('/tenant/workflows/tasks')}
<div className="flex flex-col items-start self-stretch rounded-[8px] border border-[#D1D5DB] bg-white overflow-hidden">
<div className="flex w-full h-[48px] px-[20px] py-[16px] items-center border-b border-[#D1D5DB] justify-between">
<h2 className="text-[16px] font-semibold text-[#111827] leading-none">
My Tasks
</h2>
<button
onClick={() => navigate("/tenant/workflows/tasks")}
className="text-[11px] font-bold hover:underline"
style={{ color: primaryColor }}
>
@ -315,20 +229,21 @@ const Dashboard = (): ReactElement => {
</button>
</div>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-3 p-4 w-full">
{tasksLoading ? (
<div className="text-center py-4 text-gray-400 text-sm">Loading tasks...</div>
<div className="text-center py-4 text-gray-400 text-sm">
Loading tasks...
</div>
) : tasks.length > 0 ? (
tasks.map((task) => (
<TaskCard key={task.id} task={task} />
))
tasks.map((task) => <TaskCard key={task.id} task={task} />)
) : (
<div className="text-center py-4 text-gray-400 text-sm">No pending tasks</div>
<div className="text-center py-4 text-gray-400 text-sm">
No pending tasks
</div>
)}
</div>
</div>
{/* Quick Actions Card */}
<QuickActions />
</div>
</div>