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", 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)} + /> {
+ + );