Invoice Page
Full Proforma Invoices page block with KPIs, activity chart, and filterable table.
import { AppShell, AppShellMain, AppShellScrollBody,} from "@/registry/new-york/items/manpowerhub-app-shell/components/app-shell";import { Topbar, TopbarLeft, TopbarRight, TopbarSeparator, TopbarSubtitle, TopbarTitle,} from "@/registry/new-york/items/manpowerhub-topbar/components/topbar";import { InvoicePage } from "@/registry/new-york/items/manpowerhub-invoice/components/invoice";import { Badge } from "@/components/ui/badge";
export function ManpowerhubInvoiceBasic() { return ( <AppShell className="h-[700px] rounded-lg border border-border"> <AppShellMain> <Topbar> <TopbarLeft> <TopbarTitle>Invoices</TopbarTitle> <TopbarSeparator>/</TopbarSeparator> <TopbarSubtitle>Proforma invoices</TopbarSubtitle> </TopbarLeft> <TopbarRight> <Badge variant="outline">22 records</Badge> </TopbarRight> </Topbar> <AppShellScrollBody> <InvoicePage className="p-0" /> </AppShellScrollBody> </AppShellMain> </AppShell> );}Installation
Section titled “Installation”This installation provides support for all official Shadcn icon libraries. The component is otherwise identical to the non-experimental installation.
Icon support is experimental and may not be fully stable since it uses internal Shadcn APIs.
This component relies on other items which must be installed first.
Install the following dependencies.
Copy and paste the following code into your project.
"use client";
import * as React from "react";import { Download, Eye, FileText, MoreHorizontal, Pencil, Plus, RefreshCw, Search, Trash2, ThumbsUp,} from "lucide-react";
import { InvoiceKpiCard } from "@/registry/new-york/items/manpowerhub-invoice-kpi/components/invoice-kpi";import { InvoiceChart } from "@/registry/new-york/items/manpowerhub-invoice-chart/components/invoice-chart";import { PageHeader, PageHeaderActions, PageHeaderContent, PageHeaderDescription, PageHeaderTitle,} from "@/registry/new-york/items/manpowerhub-page-header/components/page-header";import { DataTable, DataTableBody, DataTableCell, DataTableCellPrimary, DataTableHead, DataTableHeaderCell, DataTableRoot, DataTableRow,} from "@/registry/new-york/items/manpowerhub-data-table/components/data-table";import { FilterBar } from "@/registry/new-york/items/manpowerhub-filter-bar/components/filter-bar";import { Badge } from "@/components/ui/badge";import { Button } from "@/components/ui/button";import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger,} from "@/components/ui/dropdown-menu";import { Input } from "@/components/ui/input";import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";import { cn } from "@/lib/utils";
type InvoiceStatus = "draft" | "pending" | "sent" | "approved" | "expired";type FilterTab = "all" | InvoiceStatus;
interface ApprovalEntry { name: string; dept: "Operation Department" | "Account Department"; signed: boolean;}
interface Invoice { id: string; piNumber: string; billTo: string; date: string; status: InvoiceStatus; approvals: ApprovalEntry[]; grantTotal: number | null;}
const INVOICES: Invoice[] = [ { id: "1", piNumber: "KAT/2026/05/024", billTo: "Al Naboodah Contracting", date: "20/05/2026", status: "pending", approvals: [ { name: "Ahmad", dept: "Operation Department", signed: false }, { name: "Khalid", dept: "Account Department", signed: false }, ], grantTotal: 14200, }, { id: "2", piNumber: "KAT/2026/05/023", billTo: "Emaar Properties PJSC", date: "19/05/2026", status: "sent", approvals: [ { name: "Mariam", dept: "Operation Department", signed: true }, { name: "Faisal", dept: "Account Department", signed: true }, ], grantTotal: 8750, }, { id: "3", piNumber: "KAT/2026/05/022", billTo: "Sobha Realty Corporate Office", date: "18/05/2026", status: "draft", approvals: [ { name: "Tariq", dept: "Operation Department", signed: false }, { name: "Salma", dept: "Account Department", signed: false }, ], grantTotal: null, }, { id: "4", piNumber: "KAT/2026/05/021", billTo: "Al Futtaim Carillion", date: "17/05/2026", status: "approved", approvals: [ { name: "Nasser", dept: "Operation Department", signed: true }, { name: "Hind", dept: "Account Department", signed: true }, ], grantTotal: 3990, }, { id: "5", piNumber: "KAT/2026/05/020", billTo: "Shapoorji Pallonji M.E.", date: "16/05/2026", status: "expired", approvals: [ { name: "Ravi", dept: "Operation Department", signed: false }, { name: "Priya", dept: "Account Department", signed: false }, ], grantTotal: 2900, }, { id: "6", piNumber: "KAT/2026/05/019", billTo: "Arabtec Construction LLC", date: "15/05/2026", status: "draft", approvals: [ { name: "Omar", dept: "Operation Department", signed: false }, { name: "Layla", dept: "Account Department", signed: false }, ], grantTotal: null, }, { id: "7", piNumber: "KAT/2026/05/018", billTo: "M/S. Dimentions Landscapes LLC", date: "14/05/2026", status: "pending", approvals: [ { name: "Hassan", dept: "Operation Department", signed: false }, { name: "Mona", dept: "Account Department", signed: false }, ], grantTotal: 6480, }, { id: "8", piNumber: "KAT/2026/05/017", billTo: "Drake & Scull International", date: "13/05/2026", status: "sent", approvals: [ { name: "Yusuf", dept: "Operation Department", signed: true }, { name: "Aisha", dept: "Account Department", signed: true }, ], grantTotal: 11300, }, { id: "9", piNumber: "KAT/2026/05/016", billTo: "Carrefour UAE Facilities", date: "12/05/2026", status: "approved", approvals: [ { name: "Dina", dept: "Operation Department", signed: true }, { name: "Ziad", dept: "Account Department", signed: true }, ], grantTotal: 5120, }, { id: "10", piNumber: "KAT/2026/05/015", billTo: "Aldar Properties LLC", date: "11/05/2026", status: "pending", approvals: [ { name: "Sara", dept: "Operation Department", signed: false }, { name: "Bilal", dept: "Account Department", signed: false }, ], grantTotal: 18500, }, { id: "11", piNumber: "KAT/2026/05/014", billTo: "Dubai Municipality Works", date: "10/05/2026", status: "draft", approvals: [ { name: "Walid", dept: "Operation Department", signed: false }, { name: "Nadia", dept: "Account Department", signed: false }, ], grantTotal: null, }, { id: "12", piNumber: "KAT/2026/05/013", billTo: "AECOM Middle East Ltd", date: "09/05/2026", status: "expired", approvals: [ { name: "James", dept: "Operation Department", signed: false }, { name: "Fatima", dept: "Account Department", signed: false }, ], grantTotal: 1800, },];
const KPI_TREND = { total: [18, 19, 19, 20, 20, 21, 22], pending: [2, 3, 4, 3, 5, 5, 6], billed: [42000, 44000, 48000, 51000, 55000, 58000, 60481], vat: [900, 980, 1050, 1100, 1180, 1280, 1380],};
const statusConfig: Record< InvoiceStatus, { label: string; className: string }> = { draft: { label: "Draft", className: "border-[var(--color-border)] bg-transparent text-[var(--text-3)]", }, pending: { label: "Pending approval", className: "border-[var(--amber-border)] bg-[var(--amber-bg)] text-[var(--amber)]", }, sent: { label: "Sent to client", className: "border-[var(--green-border)] bg-[var(--green-bg)] text-[var(--green)]", }, approved: { label: "Approved", className: "border-[var(--brand-border)] bg-[var(--brand-subtle)] text-[var(--brand)]", }, expired: { label: "Expired", className: "border-[var(--red-border)] bg-[var(--red-bg)] text-[var(--red)]", },};
function StatusBadge({ status }: { status: InvoiceStatus }) { const cfg = statusConfig[status]; return ( <span className={cn( "inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-[11.5px] font-medium", cfg.className, )} > {status === "pending" && ( <span className="size-1.5 rounded-full bg-[var(--amber)]" /> )} {cfg.label} </span> );}
function ApprovalCell({ approvals }: { approvals: ApprovalEntry[] }) { if (approvals.every((a) => !a.signed)) { return <span className="text-[12px] text-[var(--text-3)]">—</span>; } return ( <div className="flex flex-col gap-1"> {approvals.map((a) => ( <div key={a.dept} className="flex items-center gap-2"> <span className={cn( "flex size-3.5 items-center justify-center rounded-full border", a.signed ? "border-[var(--brand)] bg-[var(--brand)]" : "border-[var(--color-border-strong)] bg-transparent", )} > {a.signed && <span className="size-1.5 rounded-full bg-white" />} </span> <span className="text-[12px] text-[var(--text-2)]"> <span className="font-medium text-foreground">{a.name}</span> {" · "} <span className="text-[var(--text-3)]">{a.dept}</span> </span> </div> ))} </div> );}
function ActionsMenu({ invoice }: { invoice: Invoice }) { return ( <DropdownMenu> <DropdownMenuTrigger asChild> <Button variant="ghost" size="icon" className="size-7"> <MoreHorizontal className="size-4" /> </Button> </DropdownMenuTrigger> <DropdownMenuContent align="end" className="w-52"> <div className="px-2 py-1.5 text-[11px] font-medium text-[var(--text-3)]"> {invoice.piNumber} </div> <DropdownMenuSeparator /> <DropdownMenuItem className="gap-2 text-[13px]"> <Eye className="size-3.5" /> View </DropdownMenuItem> <DropdownMenuItem className="gap-2 text-[13px]"> <Pencil className="size-3.5" /> Edit draft </DropdownMenuItem> <DropdownMenuItem className="gap-2 text-[13px]"> <Download className="size-3.5" /> Download PDF </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem className="gap-2 text-[13px]"> <ThumbsUp className="size-3.5" /> Approve step 1 (Operation) </DropdownMenuItem> <DropdownMenuItem className="gap-2 text-[13px]"> <RefreshCw className="size-3.5" /> Recall to draft </DropdownMenuItem> <DropdownMenuSeparator /> <DropdownMenuItem className="gap-2 text-[13px] text-[var(--red)] focus:text-[var(--red)]"> <Trash2 className="size-3.5" /> Delete draft </DropdownMenuItem> </DropdownMenuContent> </DropdownMenu> );}
export interface InvoicePageProps extends React.ComponentProps<"div"> {}
function InvoicePage({ className, ...props }: InvoicePageProps) { const [tab, setTab] = React.useState<FilterTab>("all"); const [search, setSearch] = React.useState("");
const filtered = INVOICES.filter((inv) => { const matchTab = tab === "all" || inv.status === tab; const q = search.toLowerCase(); const matchSearch = !q || inv.piNumber.toLowerCase().includes(q) || inv.billTo.toLowerCase().includes(q); return matchTab && matchSearch; });
const pendingCount = INVOICES.filter((i) => i.status === "pending").length; const totalBilled = INVOICES.reduce((sum, i) => sum + (i.grantTotal ?? 0), 0); const vatCollected = Math.round(totalBilled * 0.05 * 0.45);
return ( <div className={cn("p-6", className)} {...props}> <PageHeader> <PageHeaderContent> <PageHeaderTitle>Proforma invoices</PageHeaderTitle> <PageHeaderDescription>Wednesday, May 20, 2026</PageHeaderDescription> </PageHeaderContent> <PageHeaderActions> <Button size="sm" className="gap-1.5"> <Plus className="size-3.5" /> New draft </Button> </PageHeaderActions> </PageHeader>
<div className="mb-4 grid grid-cols-2 gap-3 lg:grid-cols-4"> <InvoiceKpiCard label="Total invoices" value={INVOICES.length} sublabel="All PI records in the system" trendData={KPI_TREND.total} accent="neutral" /> <InvoiceKpiCard label="Pending approval" value={pendingCount} sublabel="Awaiting Operation & Account sign-off" trendData={KPI_TREND.pending} accent="amber" /> <InvoiceKpiCard label="Total billed" value={ <span className="text-[var(--brand)]"> AED {totalBilled.toLocaleString()}.00 </span> } sublabel="Sum of grant totals (AED)" trendData={KPI_TREND.billed} accent="brand" /> <InvoiceKpiCard label="VAT collected" value={`AED ${vatCollected.toLocaleString()}.00`} sublabel="Total VAT across all invoices" trendData={KPI_TREND.vat} accent="neutral" /> </div>
<div className="mb-4"> <InvoiceChart /> </div>
<div className="rounded-[var(--r-lg)] border border-border bg-card"> <div className="border-b border-[var(--color-border-subtle)] px-4 py-3.5"> <p className="text-[13px] font-semibold text-foreground"> Recent invoices </p> <p className="text-[11.5px] text-[var(--text-3)]"> {INVOICES.length} invoices · Draft, approval, and send workflow </p> </div>
<div className="px-4 pt-3"> <FilterBar className="mb-3 justify-between"> <Tabs value={tab} onValueChange={(v) => setTab(v as FilterTab)} className="w-auto" > <TabsList className="h-8 gap-0.5 bg-transparent p-0"> {( [ { value: "all", label: "All" }, { value: "draft", label: "Draft" }, { value: "pending", label: "Pending" }, { value: "approved", label: "Approved" }, { value: "sent", label: "Sent" }, ] as { value: FilterTab; label: string }[] ).map(({ value, label }) => ( <TabsTrigger key={value} value={value} className="h-8 rounded-[var(--r-md)] px-3 text-[12.5px]" > {label} </TabsTrigger> ))} </TabsList> </Tabs> <div className="relative"> <Search className="absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-[var(--text-3)]" /> <Input placeholder="Search PI number or bill to…" value={search} onChange={(e) => setSearch(e.target.value)} className="h-8 w-56 pl-8 text-[12.5px]" /> </div> </FilterBar> </div>
<DataTable> <DataTableRoot> <DataTableHead> <DataTableRow> <DataTableHeaderCell>PI Number</DataTableHeaderCell> <DataTableHeaderCell>Bill To</DataTableHeaderCell> <DataTableHeaderCell>Date</DataTableHeaderCell> <DataTableHeaderCell>Status</DataTableHeaderCell> <DataTableHeaderCell>Approval</DataTableHeaderCell> <DataTableHeaderCell className="text-right"> Grant Total </DataTableHeaderCell> <DataTableHeaderCell className="w-10" /> </DataTableRow> </DataTableHead> <DataTableBody> {filtered.length === 0 ? ( <DataTableRow> <DataTableCell colSpan={7} className="py-10 text-center text-[12.5px] text-[var(--text-3)]" > No invoices match your filter. </DataTableCell> </DataTableRow> ) : ( filtered.map((inv) => ( <DataTableRow key={inv.id}> <DataTableCellPrimary className="font-mono text-[12.5px]"> <span className="flex items-center gap-1.5"> <FileText className="size-3.5 shrink-0 text-[var(--text-3)]" /> {inv.piNumber} </span> </DataTableCellPrimary> <DataTableCell>{inv.billTo}</DataTableCell> <DataTableCell className="text-[var(--text-3)]"> {inv.date} </DataTableCell> <DataTableCell> <StatusBadge status={inv.status} /> </DataTableCell> <DataTableCell> <ApprovalCell approvals={inv.approvals} /> </DataTableCell> <DataTableCell className="text-right font-medium text-foreground"> {inv.grantTotal != null ? `AED ${inv.grantTotal.toLocaleString()}.00` : "—"} </DataTableCell> <DataTableCell className="text-right"> <ActionsMenu invoice={inv} /> </DataTableCell> </DataTableRow> )) )} </DataTableBody> </DataTableRoot> </DataTable> </div> </div> );}
export { InvoicePage };Update the import paths to match your project setup.
Install the ManpowerHub theme first, then add the block:
npx shadcn@latest add https://woxcn-registry.woxware.io/r/manpowerhub-invoice.jsonThis installs the full block at components/blocks/manpowerhub-invoice.tsx along with its component dependencies. Drop <InvoicePage /> inside an AppShellScrollBody for the full shell layout.