From 93bebffa96312078d79dccbf482a35dc602d7427 Mon Sep 17 00:00:00 2001 From: cherry-git999 Date: Mon, 22 Jun 2026 12:46:15 +0000 Subject: [PATCH] feat(dashboard): replace bar charts with mandal-level pie charts --- .../dashboard/components/mandal-pie-chart.tsx | 120 ++++++++++++++ .../app/(authenticated)/dashboard/page.tsx | 73 ++++++--- .../src/app/api/dashboard/summary/route.ts | 147 +++++++++++++++++- frontend/src/i18n/translations/en.ts | 6 + frontend/src/i18n/translations/te.ts | 8 + 5 files changed, 331 insertions(+), 23 deletions(-) create mode 100644 frontend/src/app/(authenticated)/dashboard/components/mandal-pie-chart.tsx diff --git a/frontend/src/app/(authenticated)/dashboard/components/mandal-pie-chart.tsx b/frontend/src/app/(authenticated)/dashboard/components/mandal-pie-chart.tsx new file mode 100644 index 0000000..a6c7dc0 --- /dev/null +++ b/frontend/src/app/(authenticated)/dashboard/components/mandal-pie-chart.tsx @@ -0,0 +1,120 @@ +"use client"; + +import { + ChartContainer, + ChartTooltip, + ChartTooltipContent, + type ChartConfig, +} from "@/components/ui/chart"; +import { Pie, PieChart, Cell } from "recharts"; +import { useLanguage } from "@/i18n/LanguageContext"; + +export type MandalPieItem = { + mandalId: string; + mandalName: string; + mandalNameTelugu: string; + value: number; +}; + +interface MandalPieChartProps { + title: string; + data: MandalPieItem[]; + labelPrefix: string; +} + +const PIE_COLORS = [ + "#22d3ee", // cyan + "#3b82f6", // blue + "#f59e0b", // amber + "#ef4444", // red + "#a855f7", // purple + "#ec4899", // pink + "#10b981", // emerald + "#f97316", // orange + "#6366f1", // indigo + "#14b8a6", // teal +]; + +export function MandalPieChart({ title, data, labelPrefix }: MandalPieChartProps) { + const { lang } = useLanguage(); + + // Build config for ChartContainer + const config: ChartConfig = {}; + data.forEach((item, idx) => { + config[item.mandalName] = { + label: lang === "te" ? item.mandalNameTelugu : item.mandalName, + color: PIE_COLORS[idx % PIE_COLORS.length], + }; + }); + + // Build pie data + const pieData = data.map((item, idx) => ({ + name: lang === "te" ? item.mandalNameTelugu : item.mandalName, + value: item.value, + fill: PIE_COLORS[idx % PIE_COLORS.length], + })); + + const hasData = pieData.some((d) => d.value > 0); + + return ( +
+

+ {title} +

+ + {hasData ? ( + + + ( + ₹{Number(value).toLocaleString("en-IN")} + )} + /> + } + /> + + {pieData.map((entry, index) => ( + + ))} + + + + ) : ( +
+ — +
+ )} + + {/* Custom Legend */} +
+ {data.map((item, idx) => ( +
+ + + {lang === "te" ? item.mandalNameTelugu : item.mandalName} + + + {labelPrefix} ₹{item.value.toLocaleString("en-IN")} + +
+ ))} +
+
+ ); +} diff --git a/frontend/src/app/(authenticated)/dashboard/page.tsx b/frontend/src/app/(authenticated)/dashboard/page.tsx index cdf12db..52ba06c 100644 --- a/frontend/src/app/(authenticated)/dashboard/page.tsx +++ b/frontend/src/app/(authenticated)/dashboard/page.tsx @@ -2,7 +2,7 @@ import { useState, useEffect } from "react"; import { ContentLayout } from "../../../components/admin-panel/content-layout"; -import { VillageTransactionsChart } from "./components/village-transactions-chart"; +import { MandalPieChart, MandalPieItem } from "./components/mandal-pie-chart"; import { Loader2 } from "lucide-react"; import { useLanguage } from "@/i18n/LanguageContext"; @@ -14,8 +14,18 @@ type VillageSummary = { totalLoans: number; }; +type MandalSummary = { + mandalId: string; + mandalName: string; + mandalNameTelugu: string; + totalDeposits: number; + totalLoans: number; + totalLaagodi: number; +}; + type DashboardSummaryResponse = { villages: VillageSummary[]; + mandals: MandalSummary[]; totals: { villageCount: number; memberCount: number; @@ -26,7 +36,7 @@ type DashboardSummaryResponse = { export default function DashboardPage() { const { t } = useLanguage(); - const [data, setData] = useState([]); + const [mandals, setMandals] = useState([]); const [totals, setTotals] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -49,7 +59,7 @@ export default function DashboardPage() { throw new Error(json.error?.message || "Failed to load dashboard summary"); } - setData(json.data.villages); + setMandals(json.data.mandals); setTotals(json.data.totals); } catch (err) { setError(err instanceof Error ? err.message : "Unknown error"); @@ -61,10 +71,26 @@ export default function DashboardPage() { fetchSummary(); }, []); - const chartData = data.map((village) => ({ - village: village.villageName, - deposits: village.totalDeposits, - loans: village.totalLoans, + // Prepare pie chart data per category + const depositsData: MandalPieItem[] = mandals.map((m) => ({ + mandalId: m.mandalId, + mandalName: m.mandalName, + mandalNameTelugu: m.mandalNameTelugu, + value: m.totalDeposits, + })); + + const loansData: MandalPieItem[] = mandals.map((m) => ({ + mandalId: m.mandalId, + mandalName: m.mandalName, + mandalNameTelugu: m.mandalNameTelugu, + value: m.totalLoans, + })); + + const laagodiData: MandalPieItem[] = mandals.map((m) => ({ + mandalId: m.mandalId, + mandalName: m.mandalName, + mandalNameTelugu: m.mandalNameTelugu, + value: m.totalLaagodi, })); return ( @@ -108,21 +134,24 @@ export default function DashboardPage() { )} -
-
-
-
-

- {t.dashboard.chartTitle} -

-

- {t.dashboard.chartSubtitle} -

-
-
- -
-
+ {/* Pie Charts Grid */} +
+ + + +
)} diff --git a/frontend/src/app/api/dashboard/summary/route.ts b/frontend/src/app/api/dashboard/summary/route.ts index cb74c4c..2bac42c 100644 --- a/frontend/src/app/api/dashboard/summary/route.ts +++ b/frontend/src/app/api/dashboard/summary/route.ts @@ -13,8 +13,18 @@ type VillageSummary = { totalLoans: number; }; +type MandalPieSummary = { + mandalId: string; + mandalName: string; + mandalNameTelugu: string; + totalDeposits: number; + totalLoans: number; + totalLaagodi: number; +}; + type DashboardSummaryResponse = { villages: VillageSummary[]; + mandals: MandalPieSummary[]; totals: { villageCount: number; memberCount: number; @@ -75,8 +85,144 @@ export async function GET(): Promise { }), ); + // Fetch mandals with their Telugu labels for pie charts + const mandals = await prisma.mandals.findMany({ + select: { + id: true, + label_english: true, + villages: { + select: { + id: true, + }, + }, + }, + orderBy: { + label_english: "asc", + }, + }); + + // Fetch Telugu labels for mandals + const mandalTeluguLabels = await prisma.i18n_labels.findMany({ + where: { + entity_table: "mandals", + field: "label_telugu", + language_code: "te", + }, + }); + + const teluguLabelMap = new Map(); + for (const label of mandalTeluguLabels) { + teluguLabelMap.set(label.entity_id, label.text); + } + + // Fetch account types to identify SAVINGS, WITHDRAW, LAAGODI + const accountTypes = await prisma.account_types.findMany({ + where: { + name: { in: ["SAVINGS", "WITHDRAW", "LAAGODI"] }, + is_active: true, + }, + select: { + id: true, + name: true, + }, + }); + + const accountTypeMap = new Map(); + for (const at of accountTypes) { + accountTypeMap.set(at.name, at.id); + } + + const savingsTypeId = accountTypeMap.get("SAVINGS"); + const withdrawTypeId = accountTypeMap.get("WITHDRAW"); + const laagodiTypeId = accountTypeMap.get("LAAGODI"); + + // Get accounts by type for efficient filtering + const accountsByType = await prisma.accounts.findMany({ + where: { + account_type_id: { + in: [savingsTypeId, withdrawTypeId, laagodiTypeId].filter(Boolean) as string[], + }, + }, + select: { + id: true, + account_type_id: true, + }, + }); + + const savingsAccountIds = accountsByType + .filter((a) => a.account_type_id === savingsTypeId) + .map((a) => a.id); + const withdrawAccountIds = accountsByType + .filter((a) => a.account_type_id === withdrawTypeId) + .map((a) => a.id); + const laagodiAccountIds = accountsByType + .filter((a) => a.account_type_id === laagodiTypeId) + .map((a) => a.id); + + // Aggregate transactions per mandal per account type + const mandalSummaries: MandalPieSummary[] = await Promise.all( + mandals.map(async (mandal) => { + const villageIds = mandal.villages.map((v) => v.id); + + const [depositsAgg, loansAgg, laagodiAgg] = await Promise.all([ + // Deposits = credits to SAVINGS accounts in this mandal's villages + savingsAccountIds.length > 0 + ? prisma.transactions.aggregate({ + _sum: { amount: true }, + where: { + is_archived: false, + is_deleted: false, + account_id: { in: savingsAccountIds }, + members: { + village_id: { in: villageIds }, + }, + }, + }) + : Promise.resolve({ _sum: { amount: null } }), + // Loans = transactions on WITHDRAW accounts in this mandal's villages + withdrawAccountIds.length > 0 + ? prisma.transactions.aggregate({ + _sum: { amount: true }, + where: { + is_archived: false, + is_deleted: false, + account_id: { in: withdrawAccountIds }, + members: { + village_id: { in: villageIds }, + }, + }, + }) + : Promise.resolve({ _sum: { amount: null } }), + // Laagodi = transactions on LAAGODI accounts in this mandal's villages + laagodiAccountIds.length > 0 + ? prisma.transactions.aggregate({ + _sum: { amount: true }, + where: { + is_archived: false, + is_deleted: false, + account_id: { in: laagodiAccountIds }, + members: { + village_id: { in: villageIds }, + }, + }, + }) + : Promise.resolve({ _sum: { amount: null } }), + ]); + + return { + mandalId: mandal.id, + mandalName: mandal.label_english, + mandalNameTelugu: teluguLabelMap.get(mandal.id) || mandal.label_english, + totalDeposits: depositsAgg._sum.amount ?? 0, + totalLoans: loansAgg._sum.amount ?? 0, + totalLaagodi: laagodiAgg._sum.amount ?? 0, + }; + }), + ); + const response: DashboardSummaryResponse = { villages: summaries, + mandals: mandalSummaries, totals: { villageCount: villages.length, memberCount: summaries.reduce( @@ -105,4 +251,3 @@ export async function GET(): Promise { ); } } - diff --git a/frontend/src/i18n/translations/en.ts b/frontend/src/i18n/translations/en.ts index 932aee9..3a9c66b 100644 --- a/frontend/src/i18n/translations/en.ts +++ b/frontend/src/i18n/translations/en.ts @@ -48,6 +48,12 @@ const en: Translations = { chartSubtitle: "Total amount of loans and deposits from each village.", deposits: "Deposits", loans: "Loans", + pieDeposits: "Sangham Total Deposits", + pieLoans: "Sangham Total Loans", + pieLaagodi: "Sangham Total Laagodi", + totalDepositsLabel: "Total Deposits", + totalLoansLabel: "Total Loans", + totalLaagodiLabel: "Total Laagodi", }, membersBrowse: { diff --git a/frontend/src/i18n/translations/te.ts b/frontend/src/i18n/translations/te.ts index ca296d7..c590895 100644 --- a/frontend/src/i18n/translations/te.ts +++ b/frontend/src/i18n/translations/te.ts @@ -13,6 +13,8 @@ export type Translations = { dashboard: { title: string; villages: string; members: string; totalDeposits: string; totalLoans: string; chartTitle: string; chartSubtitle: string; deposits: string; loans: string; + pieDeposits: string; pieLoans: string; pieLaagodi: string; + totalDepositsLabel: string; totalLoansLabel: string; totalLaagodiLabel: string; }; membersBrowse: { title: string; mandal: string; village: string; filterBtn: string; resetBtn: string; @@ -150,6 +152,12 @@ const te: Translations = { chartSubtitle: "ప్రతి గ్రామం నుండి మొత్తం రుణాలు మరియు డిపాజిట్లు.", deposits: "డిపాజిట్లు", loans: "రుణాలు", + pieDeposits: "సంఘం మొత్తం జమలు", + pieLoans: "సంఘం మొత్తం అప్పులు", + pieLaagodi: "సంఘం మొత్తం లోన్లు", + totalDepositsLabel: "మొత్తం జమలు", + totalLoansLabel: "మొత్తం అప్పులు", + totalLaagodiLabel: "మొత్తం లోన్లు", }, // Members – Browse