From d66284282f17befdb8bef46fc4e7f222a05c426b Mon Sep 17 00:00:00 2001 From: adarsh-yadav1 Date: Fri, 22 May 2026 13:08:33 +0530 Subject: [PATCH 1/3] feat: add Invoice Analytics Dashboard with Chart.js --- Govt-Billing-React/.env.example | 12 +- .../src/components/Dashboard/Dashboard.tsx | 197 ++++++++++++++++++ .../Dashboard/InvoiceSummaryCards.tsx | 68 ++++++ Govt-Billing-React/src/pages/Home.tsx | 15 ++ 4 files changed, 286 insertions(+), 6 deletions(-) create mode 100644 Govt-Billing-React/src/components/Dashboard/Dashboard.tsx create mode 100644 Govt-Billing-React/src/components/Dashboard/InvoiceSummaryCards.tsx diff --git a/Govt-Billing-React/.env.example b/Govt-Billing-React/.env.example index 05373f2..c6ab527 100644 --- a/Govt-Billing-React/.env.example +++ b/Govt-Billing-React/.env.example @@ -1,7 +1,7 @@ VITE_FIREBASE_VITE_APP_TITLE="firebase app title" -VITE_FIREBASE_API_KEY="firebase_key" -VITE_FIREBASE_AUTH_DOMAIN="domain_name.firebaseapp.com" -VITE_FIREBASE_PROJECT_ID="project_id" -VITE_FIREBASE_STORAGE_BUCKET="storage_bucket" -VITE_FIREBASE_MESSAGING_SENDER_ID="sender_id" -VITE_FIREBASE_APP_ID="firebase_app_id" \ No newline at end of file +VITE_FIREBASE_API_KEY=dummy +VITE_FIREBASE_AUTH_DOMAIN=dummy.firebaseapp.com +VITE_FIREBASE_PROJECT_ID=dummy +VITE_FIREBASE_STORAGE_BUCKET=dummy.appspot.com +VITE_FIREBASE_MESSAGING_SENDER_ID=123456789 +VITE_FIREBASE_APP_ID=1:123:web:abc \ No newline at end of file diff --git a/Govt-Billing-React/src/components/Dashboard/Dashboard.tsx b/Govt-Billing-React/src/components/Dashboard/Dashboard.tsx new file mode 100644 index 0000000..1a7cb18 --- /dev/null +++ b/Govt-Billing-React/src/components/Dashboard/Dashboard.tsx @@ -0,0 +1,197 @@ +import { + IonButton, + IonButtons, + IonCard, + IonCardContent, + IonCardHeader, + IonCardTitle, + IonCol, + IonContent, + IonGrid, + IonHeader, + IonModal, + IonRow, + IonTitle, + IonToolbar, +} from "@ionic/react"; +import { Chart, registerables } from "chart.js"; +import { useEffect, useRef } from "react"; +import InvoiceSummaryCards from "./InvoiceSummaryCards"; + +Chart.register(...registerables); + +// Sample data — replace with real store reads once invoice data model is finalized +const SAMPLE_INVOICES = [ + { status: "paid", amount: 85000, month: "Jan" }, + { status: "pending", amount: 34580, month: "Jan" }, + { status: "overdue", amount: 47200, month: "Feb" }, + { status: "paid", amount: 120000, month: "Feb" }, + { status: "draft", amount: 15000, month: "Mar" }, + { status: "paid", amount: 62000, month: "Mar" }, + { status: "overdue", amount: 29000, month: "Apr" }, + { status: "pending", amount: 53000, month: "Apr" }, + { status: "paid", amount: 78000, month: "May" }, +]; + +const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May"]; + +interface Props { + showDashboard: boolean; + setShowDashboard: (val: boolean) => void; +} + +const Dashboard: React.FC = ({ showDashboard, setShowDashboard }) => { + const donutRef = useRef(null); + const barRef = useRef(null); + const donutChart = useRef(null); + const barChart = useRef(null); + + const summaryData = { + total: SAMPLE_INVOICES.length, + paid: SAMPLE_INVOICES.filter((i) => i.status === "paid").length, + pending: SAMPLE_INVOICES.filter((i) => i.status === "pending").length, + overdue: SAMPLE_INVOICES.filter((i) => i.status === "overdue").length, + totalAmount: SAMPLE_INVOICES.reduce((s, i) => s + i.amount, 0), + collectedAmount: SAMPLE_INVOICES.filter((i) => i.status === "paid").reduce( + (s, i) => s + i.amount, + 0 + ), + }; + + useEffect(() => { + if (!showDashboard) return; + + // small delay so modal finishes rendering before canvas is available + const timer = setTimeout(() => { + if (donutRef.current) { + donutChart.current?.destroy(); + donutChart.current = new Chart(donutRef.current, { + type: "doughnut", + data: { + labels: ["Paid", "Pending", "Overdue", "Draft"], + datasets: [ + { + data: [ + summaryData.paid, + summaryData.pending, + summaryData.overdue, + SAMPLE_INVOICES.filter((i) => i.status === "draft").length, + ], + backgroundColor: ["#2dd36f", "#ffc409", "#eb445a", "#92949c"], + borderWidth: 0, + }, + ], + }, + options: { + responsive: true, + cutout: "65%", + plugins: { legend: { position: "bottom" } }, + }, + }); + } + + if (barRef.current) { + barChart.current?.destroy(); + barChart.current = new Chart(barRef.current, { + type: "bar", + data: { + labels: MONTHS, + datasets: [ + { + label: "Total (₹)", + data: MONTHS.map((m) => + SAMPLE_INVOICES.filter((i) => i.month === m).reduce( + (s, i) => s + i.amount, + 0 + ) + ), + backgroundColor: "rgba(56,128,255,0.3)", + borderColor: "#3880ff", + borderWidth: 2, + borderRadius: 4, + }, + { + label: "Collected (₹)", + data: MONTHS.map((m) => + SAMPLE_INVOICES.filter( + (i) => i.month === m && i.status === "paid" + ).reduce((s, i) => s + i.amount, 0) + ), + backgroundColor: "rgba(45,211,111,0.4)", + borderColor: "#2dd36f", + borderWidth: 2, + borderRadius: 4, + }, + ], + }, + options: { + responsive: true, + plugins: { legend: { position: "top" } }, + scales: { + y: { + beginAtZero: true, + ticks: { + callback: (v) => `₹${Number(v).toLocaleString("en-IN")}`, + }, + }, + }, + }, + }); + } + }, 300); + + return () => { + clearTimeout(timer); + donutChart.current?.destroy(); + barChart.current?.destroy(); + }; + }, [showDashboard]); + + return ( + setShowDashboard(false)}> + + + Invoice Analytics + + setShowDashboard(false)}>Close + + + + + + + + + + + + + Status Breakdown + + + + + + + + + + + + + Monthly Revenue + + + + + + + + + + + + ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/Govt-Billing-React/src/components/Dashboard/InvoiceSummaryCards.tsx b/Govt-Billing-React/src/components/Dashboard/InvoiceSummaryCards.tsx new file mode 100644 index 0000000..3973a1f --- /dev/null +++ b/Govt-Billing-React/src/components/Dashboard/InvoiceSummaryCards.tsx @@ -0,0 +1,68 @@ +import { IonCard, IonCardContent, IonCol, IonGrid, IonRow } from "@ionic/react"; + +interface SummaryData { + total: number; + paid: number; + pending: number; + overdue: number; + totalAmount: number; + collectedAmount: number; +} + +interface Props { + data: SummaryData; +} + +const InvoiceSummaryCards: React.FC = ({ data }) => { + const cards = [ + { label: "Total Invoices", value: data.total, color: "#3880ff" }, + { label: "Paid", value: data.paid, color: "#2dd36f" }, + { label: "Pending", value: data.pending, color: "#ffc409" }, + { label: "Overdue", value: data.overdue, color: "#eb445a" }, + { + label: "Total (₹)", + value: `₹${data.totalAmount.toLocaleString("en-IN")}`, + color: "#3880ff", + }, + { + label: "Collected (₹)", + value: `₹${data.collectedAmount.toLocaleString("en-IN")}`, + color: "#2dd36f", + }, + ]; + + return ( + + + {cards.map((c) => ( + + + +
+ {c.value} +
+
+ {c.label} +
+
+
+
+ ))} +
+
+ ); +}; + +export default InvoiceSummaryCards; \ No newline at end of file diff --git a/Govt-Billing-React/src/pages/Home.tsx b/Govt-Billing-React/src/pages/Home.tsx index 7c4e143..9c86c2f 100644 --- a/Govt-Billing-React/src/pages/Home.tsx +++ b/Govt-Billing-React/src/pages/Home.tsx @@ -1,3 +1,5 @@ +import Dashboard from "../components/Dashboard/Dashboard"; + import { IonButton, IonContent, @@ -32,6 +34,7 @@ const Home: React.FC = () => { const [selectedFile, updateSelectedFile] = useState("default"); const [billType, updateBillType] = useState(1); const [device] = useState("default"); + const [showDashboard, setShowDashboard] = useState(false); initFirebase(); @@ -94,6 +97,13 @@ const Home: React.FC = () => { console.log("Popover clicked"); }} /> + setShowDashboard(true)} + /> {
+ + ); From 3bf7557b71a848e6f26ffc3bb78cc2f2bf952ffd Mon Sep 17 00:00:00 2001 From: adarsh-yadav1 Date: Fri, 22 May 2026 13:20:40 +0530 Subject: [PATCH 2/3] chore: add chart.js dependency --- Govt-Billing-React/package-lock.json | 17 +++++++++++++++++ Govt-Billing-React/package.json | 1 + 2 files changed, 18 insertions(+) diff --git a/Govt-Billing-React/package-lock.json b/Govt-Billing-React/package-lock.json index 35a1752..641ace4 100644 --- a/Govt-Billing-React/package-lock.json +++ b/Govt-Billing-React/package-lock.json @@ -22,6 +22,7 @@ "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", "capacitor-email-composer": "^5.0.0", + "chart.js": "^4.5.1", "firebase": "^10.8.1", "ionicons": "^7.0.0", "react": "^18.2.0", @@ -3568,6 +3569,11 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5276,6 +5282,17 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/check-error": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", diff --git a/Govt-Billing-React/package.json b/Govt-Billing-React/package.json index d008d55..156f46e 100644 --- a/Govt-Billing-React/package.json +++ b/Govt-Billing-React/package.json @@ -27,6 +27,7 @@ "@types/react-router": "^5.1.20", "@types/react-router-dom": "^5.3.3", "capacitor-email-composer": "^5.0.0", + "chart.js": "^4.5.1", "firebase": "^10.8.1", "ionicons": "^7.0.0", "react": "^18.2.0", From 428d8ecafb8c6e012e5a7ab6f6c5f314f6804ae2 Mon Sep 17 00:00:00 2001 From: adarsh-yadav1 Date: Fri, 22 May 2026 13:23:29 +0530 Subject: [PATCH 3/3] fix: restore .env.example to original --- Govt-Billing-React/.env.example | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Govt-Billing-React/.env.example b/Govt-Billing-React/.env.example index c6ab527..05373f2 100644 --- a/Govt-Billing-React/.env.example +++ b/Govt-Billing-React/.env.example @@ -1,7 +1,7 @@ VITE_FIREBASE_VITE_APP_TITLE="firebase app title" -VITE_FIREBASE_API_KEY=dummy -VITE_FIREBASE_AUTH_DOMAIN=dummy.firebaseapp.com -VITE_FIREBASE_PROJECT_ID=dummy -VITE_FIREBASE_STORAGE_BUCKET=dummy.appspot.com -VITE_FIREBASE_MESSAGING_SENDER_ID=123456789 -VITE_FIREBASE_APP_ID=1:123:web:abc \ No newline at end of file +VITE_FIREBASE_API_KEY="firebase_key" +VITE_FIREBASE_AUTH_DOMAIN="domain_name.firebaseapp.com" +VITE_FIREBASE_PROJECT_ID="project_id" +VITE_FIREBASE_STORAGE_BUCKET="storage_bucket" +VITE_FIREBASE_MESSAGING_SENDER_ID="sender_id" +VITE_FIREBASE_APP_ID="firebase_app_id" \ No newline at end of file