Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions components/dashboard/approval-trend.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
"use client";

import { useId } from "react";
import { useTranslations } from "next-intl";
import {
AreaChart,
Area,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
} from "recharts";
import type { ApprovalTrendDataExport } from "@/hooks/use-dashboard-stats";
import {
CARD_BASE_STYLES,
TITLE_STYLES,
SUBTITLE_STYLES,
VALUE_STYLES,
TOOLTIP_STYLES,
} from "./styles";

interface ApprovalTrendProps {
data: ApprovalTrendDataExport[];
isLoading?: boolean;
}

export function ApprovalTrend({ data, isLoading = false }: ApprovalTrendProps) {
const gradientId = useId();
const t = useTranslations("client.dashboard.APPROVAL_TREND");

if (isLoading) {
return (
<div className={CARD_BASE_STYLES}>
<div className="animate-pulse">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded-sm mb-4 w-1/3"></div>
<div className="h-64 bg-gray-200 dark:bg-gray-700 rounded-sm"></div>
</div>
</div>
);
}

if (!data || data.length === 0) {
return (
<div className={CARD_BASE_STYLES}>
<h3 className={TITLE_STYLES}>{t("TITLE")}</h3>
<p className="text-gray-500 dark:text-gray-400 text-center py-8">
{t("NO_DATA")}
</p>
<p className={`${SUBTITLE_STYLES} text-center`}>
{t("NO_DATA_DESC")}
</p>
</div>
);
}

const latestRate = data[data.length - 1]?.rate || 0;
const firstRate = data[0]?.rate || 0;
const rateChange = latestRate - firstRate;
const totalSubmissions = data.reduce((sum, item) => sum + item.total, 0);
const totalApproved = data.reduce((sum, item) => sum + item.approved, 0);

return (
<section className={CARD_BASE_STYLES} aria-labelledby="approval-trend-title">
<div className="flex items-center justify-between mb-4">
<div>
<h3 id="approval-trend-title" className={TITLE_STYLES}>{t("TITLE")}</h3>
<p className={SUBTITLE_STYLES}>{t("SUBTITLE")}</p>
</div>
<div className="text-right">
<div className={VALUE_STYLES}>{latestRate.toFixed(0)}%</div>
<div
className={`text-xs ${
rateChange >= 0
? "text-green-600 dark:text-green-400"
: "text-red-600 dark:text-red-400"
}`}
>
{rateChange >= 0 ? "+" : ""}
{rateChange.toFixed(0)}%
</div>
</div>
</div>
<ResponsiveContainer width="100%" height={200}>
<AreaChart
data={data}
margin={{ top: 10, right: 10, left: 0, bottom: 0 }}
>
<defs>
<linearGradient
id={`approvalGradient-${gradientId}`}
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="5%"
stopColor="#10B981"
stopOpacity={0.3}
/>
<stop
offset="95%"
stopColor="#10B981"
stopOpacity={0}
/>
</linearGradient>
</defs>
<CartesianGrid
strokeDasharray="3 3"
stroke="var(--tw-prose-hr, #e5e7eb)"
vertical={false}
/>
<XAxis dataKey="month" stroke="#6B7280" fontSize={12} />
<YAxis
stroke="#6B7280"
fontSize={12}
domain={[0, 100]}
tickFormatter={(value: number) => `${value}%`}
/>
<Tooltip
contentStyle={TOOLTIP_STYLES}
formatter={(value) => [
`${Number(value).toFixed(1)}%`,
t("APPROVAL_RATE"),
]}
/>
<Area
type="monotone"
dataKey="rate"
stroke="#10B981"
strokeWidth={2}
fill={`url(#approvalGradient-${gradientId})`}
/>
</AreaChart>
</ResponsiveContainer>
<div className="mt-4 flex justify-between text-xs text-gray-500 dark:text-gray-400">
<span>
{t("TOTAL")}: {totalSubmissions}
</span>
<span>
{t("APPROVED")}: {totalApproved}
</span>
</div>
</section>
);
}
140 changes: 140 additions & 0 deletions components/dashboard/category-performance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
"use client";

import { useTranslations } from "next-intl";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
Cell,
} from "recharts";
import type { CategoryPerformanceDataExport } from "@/hooks/use-dashboard-stats";
import {
CARD_BASE_STYLES,
TITLE_STYLES,
SUBTITLE_STYLES,
TOOLTIP_STYLES,
CHART_COLORS,
} from "./styles";

interface CategoryPerformanceProps {
data: CategoryPerformanceDataExport[];
isLoading?: boolean;
}

export function CategoryPerformance({ data, isLoading = false }: CategoryPerformanceProps) {
const t = useTranslations("client.dashboard.CATEGORY_PERFORMANCE");

if (isLoading) {
return (
<div className={CARD_BASE_STYLES}>
<div className="animate-pulse">
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded-sm mb-4 w-1/3"></div>
<div className="h-64 bg-gray-200 dark:bg-gray-700 rounded-sm"></div>
</div>
</div>
);
}

if (!data || data.length === 0) {
return (
<div className={CARD_BASE_STYLES}>
<h3 className={TITLE_STYLES}>{t("TITLE")}</h3>
<p className="text-gray-500 dark:text-gray-400 text-center py-8">
{t("NO_DATA")}
</p>
<p className={`${SUBTITLE_STYLES} text-center`}>
{t("NO_DATA_DESC")}
</p>
</div>
);
}

const chartData = data.map((item) => ({
...item,
displayCategory:
item.category.length > 15
? `${item.category.substring(0, 15)}...`
: item.category,
}));

return (
<section className={CARD_BASE_STYLES} aria-labelledby="category-performance-title">
<div className="flex items-center justify-between mb-4">
<h3 id="category-performance-title" className={TITLE_STYLES}>{t("TITLE")}</h3>
<span className={SUBTITLE_STYLES}>{t("SUBTITLE")}</span>
</div>
<ResponsiveContainer width="100%" height={250}>
<BarChart
data={chartData}
layout="vertical"
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
>
<CartesianGrid
strokeDasharray="3 3"
stroke="var(--tw-prose-hr, #e5e7eb)"
horizontal={true}
vertical={false}
/>
<XAxis
type="number"
stroke="#6B7280"
fontSize={12}
tickFormatter={(value: number) => value.toFixed(1)}
/>
<YAxis
type="category"
dataKey="displayCategory"
stroke="#6B7280"
fontSize={12}
width={100}
/>
<Tooltip
contentStyle={TOOLTIP_STYLES}
formatter={(value, name) => {
if (name === "avgEngagement") {
return [Number(value).toFixed(2), t("AVG_ENGAGEMENT")];
}
return [value, String(name)];
}}
labelFormatter={(label) => {
const item = chartData.find(
(d) => d.displayCategory === String(label)
);
return item ? item.category : String(label);
}}
/>
<Bar dataKey="avgEngagement" radius={[0, 4, 4, 0]}>
{chartData.map((_, index) => (
<Cell
key={`cell-${index}`}
fill={CHART_COLORS[index % CHART_COLORS.length]}
/>
))}
</Bar>
</BarChart>
</ResponsiveContainer>
<div className="mt-4 grid grid-cols-2 gap-2 text-xs">
{data.slice(0, 4).map((item, index) => (
<div key={item.category} className="flex items-center gap-2">
<div
className="h-2 w-2 rounded-full shrink-0"
style={{
backgroundColor: CHART_COLORS[index % CHART_COLORS.length],
}}
/>
<span className="text-gray-600 dark:text-gray-400 truncate">
{item.category}
</span>
<span className="text-gray-900 dark:text-gray-100 font-medium shrink-0">
{item.itemCount} {t("ITEMS").toLowerCase()}
</span>
</div>
))}
</div>
</section>
);
}
57 changes: 54 additions & 3 deletions components/dashboard/dashboard-content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ import { SubmissionTimeline } from "./submission-timeline";
import { EngagementOverview } from "./engagement-overview";
import { StatusBreakdown } from "./status-breakdown";
import { TopItems } from "./top-items";
import { PeriodComparison } from "./period-comparison";
import { CategoryPerformance } from "./category-performance";
import { ApprovalTrend } from "./approval-trend";
import { SubmissionCalendar } from "./submission-calendar";
import { EngagementDistribution } from "./engagement-distribution";
import { EngagementRateChart } from "./engagement-rate-chart";
import { useDashboardStats } from "@/hooks/use-dashboard-stats";
import { Button } from "@/components/ui/button";

Expand Down Expand Up @@ -99,7 +105,15 @@ export function DashboardContent({ session }: DashboardContentProps) {
/>
</div>

{/* New Charts Section */}
{/* Period Comparison */}
<div className="mb-8">
<PeriodComparison
data={stats?.periodComparison}
isLoading={!stats}
/>
</div>

{/* Submission Timeline & Status */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<SubmissionTimeline
data={stats?.submissionTimeline || []}
Expand All @@ -113,8 +127,20 @@ export function DashboardContent({ session }: DashboardContentProps) {

{/* Engagement Overview */}
<div className="mb-8">
<EngagementOverview
data={stats?.engagementOverview || []}
<EngagementOverview
data={stats?.engagementOverview || []}
isLoading={!stats}
/>
</div>

{/* Category Performance & Approval Trend */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
<CategoryPerformance
data={stats?.categoryPerformance || []}
isLoading={!stats}
/>
<ApprovalTrend
data={stats?.approvalTrend || []}
isLoading={!stats}
/>
</div>
Expand All @@ -135,6 +161,31 @@ export function DashboardContent({ session }: DashboardContentProps) {
</div>
</div>

{/* Submission Calendar - Full Width */}
<div className="mb-8">
<SubmissionCalendar
data={stats?.submissionCalendar || []}
isLoading={!stats}
/>
</div>

{/* Engagement Distribution */}
<div className="mb-8">
<EngagementDistribution
data={stats?.engagementDistribution || []}
isLoading={!stats}
/>
</div>

{/* Engagement Rate Chart */}
<div className="mb-8">
<EngagementRateChart
engagementOverview={stats?.engagementOverview || []}
totalSubmissions={stats?.totalSubmissions || 0}
isLoading={!stats}
/>
</div>

{/* Weekly Activity Chart */}
<div className="mb-8">
<ActivityChart
Expand Down
6 changes: 4 additions & 2 deletions components/dashboard/engagement-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ export function EngagementChart({ data, isLoading = false }: EngagementChartProp
cx="50%"
cy="50%"
labelLine={false}
label={({ name, percent }: { name?: string; percent?: number }) =>
`${name ?? 'Unknown'} ${((percent ?? 0) * 100).toFixed(0)}%`}
label={({ name, percent }: { name?: string; percent?: number }) => {
if ((percent ?? 0) === 0) return '';
return `${name ?? 'Unknown'} ${((percent ?? 0) * 100).toFixed(0)}%`;
}}
outerRadius={80}
fill="#8884d8"
dataKey="value"
Expand Down
Loading
Loading