This repository has been archived on 2026-08-11. You can view files and clone it, but cannot push or open issues or pull requests.
Gishen-B2B/src/app/[locale]/(portal)/page.tsx
kirukib 1f30f00da6 Finalize portal polish: profile, avatars, ID cards, and list UX.
Ship leftover Yimaru login/personas, sidebar sections, filter modal, chart/stat clip fixes, and matching feature docs so the polish work is fully committed.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-06 21:57:07 +03:00

223 lines
7.3 KiB
TypeScript

"use client";
import { useEffect, useMemo } from "react";
import { useTranslations } from "next-intl";
import { useQuery } from "@tanstack/react-query";
import { PageShell } from "@/components/layout/page-shell";
import { PageDashboardSkeleton } from "@/components/layout/page-loading";
import { StatCard, StatCardGrid } from "@/components/layout/stat-card";
import { StatusBadge } from "@/components/shared/status-badge";
import {
ChartCard,
CategoryPieChart,
DepartmentSpendChart,
MembersByPackageChart,
SpendTrendChart,
} from "@/components/charts/portal-charts";
import { useAuth } from "@/providers/auth-provider";
import { mockApi } from "@/lib/api/mock-client";
import { formatEtb } from "@/lib/format";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { ROLE_META } from "@/lib/auth/session";
import { Link, useRouter } from "@/i18n/routing";
import { Building2, Package, Users, Wallet } from "lucide-react";
export default function DashboardPage() {
const t = useTranslations();
const { user } = useAuth();
const router = useRouter();
const orgQ = useQuery({ queryKey: ["org"], queryFn: mockApi.getOrganisation });
const membersQ = useQuery({
queryKey: ["members", user?.activeRole],
queryFn: () => mockApi.getMembers(user!.activeRole),
enabled: !!user && user.activeRole !== "MEMBER",
});
const approvalsQ = useQuery({
queryKey: ["approvals"],
queryFn: mockApi.getApprovals,
enabled: !!user && user.activeRole !== "MEMBER",
});
const deptsQ = useQuery({
queryKey: ["departments"],
queryFn: mockApi.getDepartments,
enabled: !!user && user.activeRole !== "MEMBER",
});
const statementsQ = useQuery({
queryKey: ["statements"],
queryFn: mockApi.getStatements,
enabled: !!user && user.activeRole !== "MEMBER",
});
const spendQ = useQuery({
queryKey: ["spend"],
queryFn: mockApi.getSpend,
enabled: !!user && user.activeRole !== "MEMBER",
});
const packagesQ = useQuery({
queryKey: ["packages"],
queryFn: mockApi.getPackages,
enabled: !!user && user.activeRole !== "MEMBER",
});
useEffect(() => {
if (user?.activeRole === "MEMBER") router.replace("/me");
}, [user, router]);
const trendData = useMemo(
() =>
(statementsQ.data ?? [])
.slice()
.sort((a, b) => a.period.localeCompare(b.period))
.map((s) => ({
period: s.period.slice(5),
covered: s.coveredEtb,
total: s.totalSpendEtb,
})),
[statementsQ.data],
);
const deptChart = useMemo(
() =>
(deptsQ.data ?? []).map((d) => ({
name: d.name.split(" ")[0] ?? d.name,
spend: d.spendEtb,
limit: d.subLimitEtb ?? 0,
})),
[deptsQ.data],
);
const categoryChart = useMemo(() => {
const map = new Map<string, number>();
for (const row of spendQ.data ?? []) {
map.set(row.category, (map.get(row.category) ?? 0) + row.coveredEtb);
}
return Array.from(map.entries()).map(([name, value]) => ({ name, value }));
}, [spendQ.data]);
const packageChart = useMemo(
() =>
(packagesQ.data ?? []).map((p) => ({
name: p.name,
members: p.memberCount,
})),
[packagesQ.data],
);
if (!user || user.activeRole === "MEMBER") return null;
const dashLoading =
orgQ.isLoading ||
membersQ.isLoading ||
approvalsQ.isLoading ||
deptsQ.isLoading ||
statementsQ.isLoading ||
spendQ.isLoading ||
packagesQ.isLoading;
const activeMembers = membersQ.data?.filter((m) => m.status === "active").length ?? 0;
const pendingApprovals =
approvalsQ.data?.filter((a) => a.status === "pending").length ?? 0;
const creditUsed = orgQ.data?.creditUsedEtb ?? 0;
const creditLimit = orgQ.data?.creditLimitEtb ?? 0;
const creditPct = creditLimit > 0 ? Math.round((creditUsed / creditLimit) * 100) : 0;
return (
<PageShell title={t("dashboard.title")} description={t("dashboard.subtitle")}>
{dashLoading ? (
<PageDashboardSkeleton />
) : (
<>
<StatCardGrid cols={4}>
<StatCard
label={t("dashboard.members")}
value={activeMembers}
hint={t("dashboard.liveHint")}
icon={<Users className="size-4" />}
/>
<StatCard
label={t("dashboard.credit")}
value={`${creditPct}%`}
hint={
orgQ.data
? `${formatEtb(creditUsed)} / ${formatEtb(creditLimit)}`
: undefined
}
icon={<Wallet className="size-4" />}
/>
<StatCard
label={t("dashboard.departments")}
value={deptsQ.data?.length ?? "—"}
icon={<Building2 className="size-4" />}
/>
<StatCard
label={t("dashboard.pendingApprovals")}
value={pendingApprovals}
icon={<Package className="size-4" />}
/>
</StatCardGrid>
<div className="grid gap-4 xl:grid-cols-2">
<ChartCard
title={t("dashboard.spendTrend")}
description={t("dashboard.spendTrendHint")}
>
<SpendTrendChart data={trendData} />
</ChartCard>
<ChartCard
title={t("dashboard.deptSpend")}
description={t("dashboard.deptSpendHint")}
>
<DepartmentSpendChart data={deptChart} />
</ChartCard>
</div>
<div className="grid gap-4 xl:grid-cols-2">
<ChartCard title={t("details.spendByCategory")}>
<CategoryPieChart data={categoryChart} />
</ChartCard>
<ChartCard title={t("dashboard.membersByPackage")}>
<MembersByPackageChart data={packageChart} />
</ChartCard>
</div>
<div className="grid gap-4 lg:grid-cols-2">
<Card className="rounded-2xl shadow-none">
<CardHeader className="flex flex-row items-center justify-between">
<CardTitle className="text-base">{t("dashboard.orgStatus")}</CardTitle>
{orgQ.data ? <StatusBadge status={orgQ.data.status} /> : null}
</CardHeader>
<CardContent className="space-y-3 text-sm">
<p className="font-medium">{orgQ.data?.tradeName}</p>
<p className="text-muted-foreground">
{user.name} · {t(ROLE_META[user.activeRole].labelKey)}
</p>
<div className="flex flex-wrap gap-2 pt-1">
<Button size="sm" variant="outline" render={<Link href="/organisation" />}>
{t("nav.organisation")}
</Button>
<Button size="sm" variant="outline" render={<Link href="/finance" />}>
{t("nav.finance")}
</Button>
<Button size="sm" variant="outline" render={<Link href="/members" />}>
{t("nav.members")}
</Button>
</div>
</CardContent>
</Card>
<Card className="rounded-2xl border-l-4 border-l-brand shadow-none">
<CardHeader>
<CardTitle className="text-base">{t("dashboard.guidance")}</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground">
<p>{t("dashboard.guidanceHr")}</p>
<p>{t("dashboard.guidanceFinance")}</p>
<p>{t("dashboard.guidanceSuper")}</p>
</CardContent>
</Card>
</div>
</>
)}
</PageShell>
);
}