From 8c91888da659684155a760ad0bb2ae2d3a06f028 Mon Sep 17 00:00:00 2001 From: adarsh-yadav1 Date: Thu, 14 May 2026 08:56:26 +0530 Subject: [PATCH] feat: add InvoiceCard component and InvoiceListPage with filter and search --- .../src/components/InvoiceCard.css | 146 ++++++++++++++ .../src/components/InvoiceCard.tsx | 170 +++++++++++++++++ .../src/pages/InvoiceListPage.css | 0 .../src/pages/InvoiceListPage.tsx | 178 ++++++++++++++++++ 4 files changed, 494 insertions(+) create mode 100644 Govt-Billing-React/src/components/InvoiceCard.css create mode 100644 Govt-Billing-React/src/components/InvoiceCard.tsx create mode 100644 Govt-Billing-React/src/pages/InvoiceListPage.css create mode 100644 Govt-Billing-React/src/pages/InvoiceListPage.tsx diff --git a/Govt-Billing-React/src/components/InvoiceCard.css b/Govt-Billing-React/src/components/InvoiceCard.css new file mode 100644 index 0000000..3e50bdb --- /dev/null +++ b/Govt-Billing-React/src/components/InvoiceCard.css @@ -0,0 +1,146 @@ +.invoice-card { + border-radius: 14px; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); + margin: 12px 16px; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.invoice-card:hover { + transform: translateY(-2px); + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.13); +} + +.invoice-card__header { + padding-bottom: 4px; +} + +.invoice-card__header-row { + display: flex; + align-items: center; + gap: 10px; +} + +.invoice-card__icon-wrap { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + background: var(--ion-color-primary-tint, #e8f0fe); + border-radius: 10px; + flex-shrink: 0; +} + +.invoice-card__doc-icon { + font-size: 20px; + color: var(--ion-color-primary, #3880ff); +} + +.invoice-card__title-wrap { + flex: 1; + min-width: 0; +} + +.invoice-card__number { + font-weight: 700; + font-size: 15px; + margin: 0; + color: var(--ion-text-color, #1a1a2e); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.invoice-card__org { + font-size: 12px; + color: var(--ion-color-medium, #92949c); + margin: 2px 0 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.invoice-card__badge { + font-size: 11px; + padding: 4px 10px; + border-radius: 20px; + flex-shrink: 0; +} + +.invoice-card__content { + padding-top: 8px; +} + +.invoice-card__row { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 0; + border-bottom: 1px solid var(--ion-color-light, #f4f5f8); +} + +.invoice-card__row:last-of-type { + border-bottom: none; +} + +.invoice-card__row-icon { + font-size: 16px; + color: var(--ion-color-medium, #92949c); + flex-shrink: 0; +} + +.invoice-card__row-label { + font-size: 12px; + color: var(--ion-color-medium, #92949c); + width: 52px; + flex-shrink: 0; +} + +.invoice-card__row-value { + font-size: 13px; + color: var(--ion-text-color, #1a1a2e); + font-weight: 500; +} + +.invoice-card__amount { + font-size: 15px; + font-weight: 700; + margin-left: auto; +} + +.invoice-card__amount--paid { + color: var(--ion-color-success, #2dd36f); +} + +.invoice-card__amount--pending { + color: var(--ion-color-warning, #ffc409); +} + +.invoice-card__amount--overdue { + color: var(--ion-color-danger, #eb445a); +} + +.invoice-card__amount--draft { + color: var(--ion-color-medium, #92949c); +} + +.invoice-card__overdue-text { + color: var(--ion-color-danger, #eb445a); +} + +/* IPFS chip */ +.invoice-card__ipfs-row { + margin-top: 8px; +} + +.invoice-card__ipfs-chip { + font-size: 11px; + height: 24px; +} + +.invoice-card__actions { + display: flex; + gap: 8px; + margin-top: 12px; + justify-content: flex-end; +} \ No newline at end of file diff --git a/Govt-Billing-React/src/components/InvoiceCard.tsx b/Govt-Billing-React/src/components/InvoiceCard.tsx new file mode 100644 index 0000000..383db68 --- /dev/null +++ b/Govt-Billing-React/src/components/InvoiceCard.tsx @@ -0,0 +1,170 @@ +import React from "react"; +import { + IonCard, + IonCardContent, + IonCardHeader, + IonBadge, + IonIcon, + IonButton, + IonChip, + IonLabel, +} from "@ionic/react"; +import { + documentTextOutline, + calendarOutline, + personOutline, + cashOutline, + downloadOutline, + eyeOutline, +} from "ionicons/icons"; +import "./InvoiceCard.css"; + +export type InvoiceStatus = "paid" | "pending" | "overdue" | "draft"; + +export interface Invoice { + id: string; + invoiceNumber: string; + recipientName: string; + recipientOrg: string; + amount: number; + currency?: string; + issueDate: string; + dueDate: string; + status: InvoiceStatus; + ipfsHash?: string; // Filecoin/IPFS CID if stored on-chain +} + +interface InvoiceCardProps { + invoice: Invoice; + onView?: (invoice: Invoice) => void; + onExport?: (invoice: Invoice) => void; +} + +const STATUS_CONFIG: Record< + InvoiceStatus, + { label: string; color: string } +> = { + paid: { label: "Paid", color: "success" }, + pending: { label: "Pending", color: "warning" }, + overdue: { label: "Overdue", color: "danger" }, + draft: { label: "Draft", color: "medium" }, +}; + +const InvoiceCard: React.FC = ({ + invoice, + onView, + onExport, +}) => { + const { + invoiceNumber, + recipientName, + recipientOrg, + amount, + currency = "INR", + issueDate, + dueDate, + status, + ipfsHash, + } = invoice; + + const { label, color } = STATUS_CONFIG[status]; + + const formattedAmount = new Intl.NumberFormat("en-IN", { + style: "currency", + currency, + maximumFractionDigits: 2, + }).format(amount); + + return ( + + {/* Header row: Invoice number + status badge */} + +
+
+ +
+
+

#{invoiceNumber}

+

{recipientOrg}

+
+ + {label} + +
+
+ + + {/* Recipient */} +
+ + Bill To + {recipientName} +
+ + {/* Amount */} +
+ + Amount + + {formattedAmount} + +
+ + {/* Issue Date */} +
+ + Issued + {issueDate} +
+ + {/* Due Date */} +
+ + Due + + {dueDate} + +
+ + {/* IPFS chip — shown only if stored on-chain */} + {ipfsHash && ( +
+ + + 🔗 IPFS: {ipfsHash.slice(0, 10)}…{ipfsHash.slice(-4)} + + +
+ )} + + {/* Action Buttons */} +
+ onView?.(invoice)} + > + + View + + onExport?.(invoice)} + > + + Export + +
+
+
+ ); +}; + +export default InvoiceCard; \ No newline at end of file diff --git a/Govt-Billing-React/src/pages/InvoiceListPage.css b/Govt-Billing-React/src/pages/InvoiceListPage.css new file mode 100644 index 0000000..e69de29 diff --git a/Govt-Billing-React/src/pages/InvoiceListPage.tsx b/Govt-Billing-React/src/pages/InvoiceListPage.tsx new file mode 100644 index 0000000..9c43f14 --- /dev/null +++ b/Govt-Billing-React/src/pages/InvoiceListPage.tsx @@ -0,0 +1,178 @@ +import React, { useState } from "react"; +import { + IonPage, + IonHeader, + IonToolbar, + IonTitle, + IonContent, + IonSearchbar, + IonSegment, + IonSegmentButton, + IonLabel, + IonFab, + IonFabButton, + IonIcon, + IonToast, +} from "@ionic/react"; +import { addOutline } from "ionicons/icons"; +import InvoiceCard, { Invoice, InvoiceStatus } from "../components/InvoiceCard"; +import "./InvoiceListPage.css"; + + +const SAMPLE_INVOICES: Invoice[] = [ + { + id: "1", + invoiceNumber: "GVT-2024-001", + recipientName: "Ramesh Kumar", + recipientOrg: "Delhi Public School", + amount: 85000, + currency: "INR", + issueDate: "01 May 2024", + dueDate: "31 May 2024", + status: "paid", + ipfsHash: "bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", + }, + { + id: "2", + invoiceNumber: "GVT-2024-002", + recipientName: "Priya Sharma", + recipientOrg: "NSUT Administration", + amount: 124500, + currency: "INR", + issueDate: "10 May 2024", + dueDate: "10 Jun 2024", + status: "pending", + }, + { + id: "3", + invoiceNumber: "GVT-2024-003", + recipientName: "Anjali Mehta", + recipientOrg: "MCD Ward Office", + amount: 47200, + currency: "INR", + issueDate: "15 Mar 2024", + dueDate: "15 Apr 2024", + status: "overdue", + ipfsHash: "bafkreihdwdcefgh4r5g6h7i8j9k0l1m2n3o4p5q6r7s8t9u0v1w2x3y4z", + }, + { + id: "4", + invoiceNumber: "GVT-2024-004", + recipientName: "Suresh Nair", + recipientOrg: "Kerala State Education Dept", + amount: 210000, + currency: "INR", + issueDate: "12 May 2024", + dueDate: "12 Jun 2024", + status: "draft", + }, +]; + +type FilterTab = "all" | InvoiceStatus; + +const InvoiceListPage: React.FC = () => { + const [search, setSearch] = useState(""); + const [activeTab, setActiveTab] = useState("all"); + const [toastMessage, setToastMessage] = useState(""); + const [showToast, setShowToast] = useState(false); + + + const filtered = SAMPLE_INVOICES.filter((inv) => { + const matchesTab = activeTab === "all" || inv.status === activeTab; + const q = search.toLowerCase(); + const matchesSearch = + !q || + inv.invoiceNumber.toLowerCase().includes(q) || + inv.recipientName.toLowerCase().includes(q) || + inv.recipientOrg.toLowerCase().includes(q); + return matchesTab && matchesSearch; + }); + + const handleView = (invoice: Invoice) => { + setToastMessage(`Opening invoice #${invoice.invoiceNumber}`); + setShowToast(true); + }; + + const handleExport = (invoice: Invoice) => { + setToastMessage(`Exporting #${invoice.invoiceNumber} as HTML/CSV…`); + setShowToast(true); + + }; + + return ( + + + + Invoices + + + + + {/* Search */} + setSearch(e.detail.value ?? "")} + placeholder="Search by name, org, or invoice #" + className="invoice-list__searchbar" + /> + + {/* Status filter tabs */} + setActiveTab(e.detail.value as FilterTab)} + className="invoice-list__segment" + > + + All + + + Paid + + + Pending + + + Overdue + + + Draft + + + + {/* Invoice cards */} + {filtered.length > 0 ? ( + filtered.map((invoice) => ( + + )) + ) : ( +
+

No invoices found.

+
+ )} + + {/* FAB — create new invoice */} + + + + + + + {/* Toast feedback */} + setShowToast(false)} + position="bottom" + /> +
+
+ ); +}; + +export default InvoiceListPage; \ No newline at end of file