Qassure-frontend/src/pages/tenant/Dashboard.tsx

367 lines
13 KiB
TypeScript

import { Layout } from "@/components/layout/Layout";
import type { ReactElement } from "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 { workflowService } from "@/services/workflow-service";
import { dashboardService, type TenantDashboardStats } from "@/services/dashboard-service";
import type { WorkflowTask } from "@/types/workflow";
import { useNavigate } from "react-router-dom";
interface StatCardProps {
icon: React.ComponentType<{ className?: string; strokeWidth?: number }>;
value: string | number;
label: string;
badge?: {
text: string;
variant: 'success' | 'warning' | 'info' | 'error';
};
}
const StatCard = ({
icon: Icon,
value,
label,
badge
}: StatCardProps): ReactElement => {
return (
<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">
<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>
<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-[11px] font-semibold text-[#6b7280] uppercase tracking-wider mt-1">
{label}
</div>
</div>
</div>
</div>
);
};
const TaskCard = ({ task }: { task: WorkflowTask }) => {
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';
return `In ${diffDays} Days`;
};
const handleView = () => {
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>
<div className="text-[13px] font-semibold text-[#111827] leading-tight">{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} {task.assignment?.assigned_role || 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 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 navigate = useNavigate();
const [tasks, setTasks] = useState<WorkflowTask[]>([]);
const [stats, setStats] = useState<TenantDashboardStats | null>(null);
const [loading, setLoading] = useState(true);
const [tasksLoading, setTasksLoading] = useState(true);
useEffect(() => {
fetchDashboardData();
}, []);
const fetchDashboardData = async () => {
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);
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("Critical error in fetchDashboardData:", error);
setLoading(false);
setTasksLoading(false);
}
};
const statCards: StatCardProps[] = [
stats?.documentsCount !== undefined && {
icon: FileText,
value: stats.documentsCount,
label: "Total Documents",
badge: { text: "Controlled", variant: "info" }
},
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 (
<Layout
currentPage="Dashboard"
breadcrumbs={[{ label: "QAssure - Tenant" }, { label: "Dashboard" }]}
pageHeader={{
title: "Tenant Overview",
description: "Key quality metrics and performance indicators.",
}}
>
<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">
{loading ? (
<div className="col-span-full text-center py-8 text-gray-400 text-sm">Loading statistics...</div>
) : statCards.length > 0 ? (
statCards.map((card, index) => (
<StatCard key={index} {...card} />
))
) : (
<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">
{/* 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')}
className="text-[11px] font-bold text-[#084cc8] hover:underline"
>
View all
</button>
</div>
<div className="flex flex-col gap-3">
{tasksLoading ? (
<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} />
))
) : (
<div className="text-center py-4 text-gray-400 text-sm">No pending tasks</div>
)}
</div>
</div>
{/* Quick Actions Card */}
<QuickActions />
</div>
</div>
</Layout>
);
};
export default Dashboard;