Compare commits
2 commits
3e7d943fe0
...
f18427c4ab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f18427c4ab | ||
|
|
4d8cef8605 |
5 changed files with 418 additions and 3 deletions
|
|
@ -1,6 +1,111 @@
|
|||
/**
|
||||
* 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";
|
||||
import SummaryStatusCard from "./sections/SummaryStatusCard";
|
||||
import SummaryPriorityCard from "./sections/SummaryPriorityCard";
|
||||
|
||||
/**
|
||||
* 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 className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<SummaryStatusCard
|
||||
totalTasks={analytics.totalTasks}
|
||||
statusCounts={analytics.statusCounts}
|
||||
/>
|
||||
<SummaryPriorityCard
|
||||
totalTasks={analytics.totalTasks}
|
||||
priorityCounts={analytics.priorityCounts}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
34
app/(app)/summary/sections/SummaryHeader.tsx
Normal file
34
app/(app)/summary/sections/SummaryHeader.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
94
app/(app)/summary/sections/SummaryKpiGrid.tsx
Normal file
94
app/(app)/summary/sections/SummaryKpiGrid.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
94
app/(app)/summary/sections/SummaryPriorityCard.tsx
Normal file
94
app/(app)/summary/sections/SummaryPriorityCard.tsx
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/**
|
||||
* @file app/summary/sections/SummaryPriorityCard.tsx
|
||||
* @description Client component rendering the priority severity analysis breakdown.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { Flame } from "lucide-react";
|
||||
import { PRIORITY_CONFIG } from "@/types/task";
|
||||
|
||||
/**
|
||||
* Properties for the SummaryPriorityCard component.
|
||||
*
|
||||
* @interface SummaryPriorityCardProps
|
||||
* @property {number} totalTasks - The total number of tasks to calculate percentages against.
|
||||
* @property {Record<string, number>} priorityCounts - An object containing counts for each priority level.
|
||||
*/
|
||||
interface SummaryPriorityCardProps {
|
||||
totalTasks: number;
|
||||
priorityCounts: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a priority breakdown card featuring progress bars, count badges,
|
||||
* percentage statistics, and severity configuration tags for high, medium, and low tasks.
|
||||
*
|
||||
* @param {SummaryPriorityCardProps} props - The component props.
|
||||
* @returns {JSX.Element} The rendered summary priority card component.
|
||||
*/
|
||||
export default function SummaryPriorityCard({
|
||||
totalTasks,
|
||||
priorityCounts,
|
||||
}: SummaryPriorityCardProps) {
|
||||
return (
|
||||
<div 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 space-y-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold tracking-tight">
|
||||
Priority Breakdown
|
||||
</h3>
|
||||
<p className="text-xs text-foreground-muted mt-0.5">
|
||||
Severity and importance weights assigned to active workloads.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-accent/10 text-accent p-2 rounded-xl shrink-0">
|
||||
<Flame size={18} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 my-auto">
|
||||
{(["high", "medium", "low"] as const).map((pKey) => {
|
||||
const count = priorityCounts[pKey] || 0;
|
||||
const percentage =
|
||||
totalTasks > 0 ? Math.round((count / totalTasks) * 100) : 0;
|
||||
const config = PRIORITY_CONFIG[pKey];
|
||||
|
||||
return (
|
||||
<div key={pKey} className="space-y-3">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span
|
||||
className={`px-2.5 py-0.5 rounded-lg text-xs font-semibold border ${config.className}`}
|
||||
>
|
||||
{config.label}
|
||||
</span>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-foreground-muted font-semibold w-10 text-right">
|
||||
{percentage}%
|
||||
</span>
|
||||
<span className="bg-background/80 py-0.5 rounded-lg border border-border/60 text-xs font-bold w-14 text-right inline-block">
|
||||
{count} {count === 1 ? "item" : "items"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-background/80 h-2.5 rounded-full overflow-hidden border border-border/40">
|
||||
<div
|
||||
className={`h-full transition-all duration-500 ease-out ${
|
||||
pKey === "high"
|
||||
? "bg-destructive"
|
||||
: pKey === "medium"
|
||||
? "bg-amber-500"
|
||||
: "bg-primary"
|
||||
}`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
88
app/(app)/summary/sections/SummaryStatusCard.tsx
Normal file
88
app/(app)/summary/sections/SummaryStatusCard.tsx
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
/**
|
||||
* @file app/summary/sections/SummaryStatusCard.tsx
|
||||
* @description Client component rendering the workflow status distribution progress breakdown.
|
||||
*/
|
||||
|
||||
"use client";
|
||||
|
||||
import { LayoutDashboard } from "lucide-react";
|
||||
import { COLUMNS } from "@/types/task";
|
||||
|
||||
/**
|
||||
* Properties for the SummaryStatusCard component.
|
||||
*
|
||||
* @interface SummaryStatusCardProps
|
||||
* @property {number} totalTasks - The total number of tasks across all statuses.
|
||||
* @property {Record<string, number>} statusCounts - A record mapping each status identifier to its respective task count.
|
||||
*/
|
||||
interface SummaryStatusCardProps {
|
||||
totalTasks: number;
|
||||
statusCounts: Record<string, number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the workflow status distribution card showing progress bars, percentages,
|
||||
* and task counts for each individual pipeline stage.
|
||||
*
|
||||
* @param {SummaryStatusCardProps} props - The component props.
|
||||
* @returns {JSX.Element} The rendered summary status card component.
|
||||
*/
|
||||
export default function SummaryStatusCard({
|
||||
totalTasks,
|
||||
statusCounts,
|
||||
}: SummaryStatusCardProps) {
|
||||
return (
|
||||
<div className="lg:col-span-2 bg-card/40 border border-border/80 backdrop-blur-xl p-4 sm:p-6 rounded-2xl shadow-sm space-y-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-bold tracking-tight">
|
||||
Workflow Status Distribution
|
||||
</h3>
|
||||
<p className="text-xs text-foreground-muted mt-0.5">
|
||||
Task concentration across individual pipeline stages.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-primary/10 text-primary p-2 rounded-xl shrink-0">
|
||||
<LayoutDashboard size={18} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{COLUMNS.map((col) => {
|
||||
const count = statusCounts[col.id] || 0;
|
||||
const percentage =
|
||||
totalTasks > 0 ? Math.round((count / totalTasks) * 100) : 0;
|
||||
|
||||
return (
|
||||
<div key={col.id} className="space-y-2">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<div className="flex items-center gap-2.5 font-medium">
|
||||
<span
|
||||
className={`w-3 h-3 rounded-full shadow-sm ${col.color}`}
|
||||
/>
|
||||
<span>{col.title}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xs text-foreground-muted font-semibold w-10 text-right">
|
||||
{percentage}%
|
||||
</span>
|
||||
<span className="bg-background/80 py-0.5 rounded-lg border border-border/60 text-xs font-bold w-14 text-right inline-block">
|
||||
{count} {count === 1 ? "task" : "tasks"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full bg-background/80 h-2.5 rounded-full overflow-hidden border border-border/40">
|
||||
<div
|
||||
className={`h-full transition-all duration-500 ease-out ${col.color}`}
|
||||
style={{ width: `${percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue