feat(summary): add executive summary analytics page with modular components and metrics breakdown

This commit is contained in:
Chneemann 2026-08-14 08:31:52 +02:00
parent 3e7d943fe0
commit 4d8cef8605
No known key found for this signature in database
3 changed files with 223 additions and 3 deletions

View file

@ -1,6 +1,98 @@
/**
* Summary
* @file app/summary/page.tsx
* @description Server component rendering an advanced analytical summary page with comprehensive task metrics.
*/
export default function Summary() {
return <div className="space-y-6">Summary</div>;
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import { TaskService } from "@/services/task.service";
import { COLUMNS } from "@/types/task";
import SummaryHeader from "./sections/SummaryHeader";
import SummaryKpiGrid from "./sections/SummaryKpiGrid";
/**
* Calculates comprehensive analytical metrics from the user's active task collection.
*
* @param {Array<{ task: any; user: any }>} rawTasks - The collection of raw tasks and user relations.
* @returns {Object} An object containing total counts, completion rates, overdue totals, and status/priority breakdowns.
*/
function calculateTaskAnalytics(rawTasks: Array<{ task: any; user: any }>) {
const totalTasks = rawTasks.length;
const statusCounts: Record<string, number> = COLUMNS.reduce(
(acc, col) => {
acc[col.id] = 0;
return acc;
},
{} as Record<string, number>,
);
const priorityCounts: Record<string, number> = {
high: 0,
medium: 0,
low: 0,
};
let completedTasks = 0;
let overdueTasks = 0;
let upcomingDueTasks = 0;
const now = new Date();
rawTasks.forEach(({ task }) => {
if (statusCounts[task.status] !== undefined) statusCounts[task.status]++;
if (priorityCounts[task.priority] !== undefined)
priorityCounts[task.priority]++;
if (task.status === "done") completedTasks++;
if (task.dueDate) {
const dueDate = new Date(task.dueDate);
if (dueDate < now && task.status !== "done") {
overdueTasks++;
} else if (dueDate >= now && task.status !== "done") {
upcomingDueTasks++;
}
}
});
const completionRate =
totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
return {
totalTasks,
completedTasks,
completionRate,
overdueTasks,
upcomingDueTasks,
statusCounts,
priorityCounts,
};
}
/**
* Renders the summary analytics page with authentication validation, data retrieval via TaskService,
* KPI metrics calculation, and structural grid layouts.
*
* @async
* @returns {Promise<JSX.Element>} The rendered summary page component.
*/
export default async function SummaryPage() {
const session = await auth();
if (!session?.user?.id) redirect("/login");
const rawTasks = await TaskService.findActiveTasksForUser(session.user.id);
const analytics = calculateTaskAnalytics(rawTasks);
return (
<div className="space-y-8 max-w-7xl mx-auto pb-12">
<SummaryHeader />
<SummaryKpiGrid
totalTasks={analytics.totalTasks}
completionRate={analytics.completionRate}
overdueTasks={analytics.overdueTasks}
upcomingDueTasks={analytics.upcomingDueTasks}
/>
</div>
);
}

View file

@ -0,0 +1,34 @@
/**
* @file app/summary/sections/SummaryHeader.tsx
* @description Client component rendering the analytical page header.
*/
"use client";
import { BarChart3 } from "lucide-react";
/**
* Renders the analytical executive summary header featuring an analytics icon,
* workspace indicator, main title, and descriptive subtitle.
*
* @returns {JSX.Element} The rendered summary header component.
*/
export default function SummaryHeader() {
return (
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<div className="flex items-center gap-2 text-primary font-medium text-xs uppercase tracking-wider mb-1">
<BarChart3 size={14} />
Analytics & Insights
</div>
<h1 className="text-3xl font-extrabold tracking-tight">
Executive Summary
</h1>
<p className="text-sm text-foreground-muted mt-1">
Real-time overview of your workspace productivity and workflow
metrics.
</p>
</div>
</div>
);
}

View file

@ -0,0 +1,94 @@
/**
* @file app/summary/sections/SummaryKpiGrid.tsx
* @description Client component rendering the primary KPI metrics grid.
*/
"use client";
import { Layers, CheckCircle2, AlertTriangle, Clock } from "lucide-react";
/**
* Properties for the SummaryKpiGrid component.
*
* @interface SummaryKpiGridProps
* @property {number} totalTasks - The total count of active tasks.
* @property {number} completionRate - The calculated completion rate percentage.
* @property {number} overdueTasks - The number of tasks past their deadline.
* @property {number} upcomingDueTasks - The number of tasks with upcoming deadlines.
*/
interface SummaryKpiGridProps {
totalTasks: number;
completionRate: number;
overdueTasks: number;
upcomingDueTasks: number;
}
/**
* Renders a responsive grid of KPI cards summarizing key task metrics,
* including active totals, completion percentages, overdue items, and upcoming targets.
*
* @param {SummaryKpiGridProps} props - The component props.
* @returns {JSX.Element} The rendered summary KPI grid component.
*/
export default function SummaryKpiGrid({
totalTasks,
completionRate,
overdueTasks,
upcomingDueTasks,
}: SummaryKpiGridProps) {
const kpiCards = [
{
title: "Active Tasks",
value: totalTasks,
icon: Layers,
color: "text-primary",
borderColor: "hover:border-primary/40",
},
{
title: "Completion Rate",
value: `${completionRate}%`,
icon: CheckCircle2,
color: "text-emerald-500",
borderColor: "hover:border-emerald-500/40",
},
{
title: "Overdue Tasks",
value: overdueTasks,
icon: AlertTriangle,
color: "text-destructive",
borderColor: "hover:border-destructive/40",
},
{
title: "Upcoming Target",
value: upcomingDueTasks,
icon: Clock,
color: "text-amber-500",
borderColor: "hover:border-amber-500/40",
},
];
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
{kpiCards.map(({ title, value, icon: Icon, color, borderColor }) => (
<div
key={title}
className={`bg-card/40 border border-border/80 backdrop-blur-xl p-4 sm:p-6 rounded-2xl shadow-sm flex flex-col justify-between relative overflow-hidden group ${borderColor} transition-all duration-300`}
>
<div
className={`absolute bottom-0 right-0 p-4 opacity-20 group-hover:opacity-30 transition-opacity ${color}`}
>
<Icon size={48} />
</div>
<div>
<span className="text-xs font-semibold uppercase tracking-wider text-foreground-muted">
{title}
</span>
<div className={`text-4xl font-black tracking-tight mt-2 ${color}`}>
{value}
</div>
</div>
</div>
))}
</div>
);
}