Billing Invoice Details
Billing Invoice Details is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.
HextaUI
MIT
other
What it is
Billing Invoice Details is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
No packages beyond the project baseline
Licence
This component comes from HextaUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Billing Invoice Details
"use client";
import { Download, Loader2, Printer } from "lucide-react";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { Badge } from "@/registry/new-york/ui/badge";
import { Button } from "@/registry/new-york/ui/button";
import { Separator } from "@/registry/new-york/ui/separator";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/registry/new-york/ui/sheet";
export interface InvoiceLineItem {
description: string;
quantity: number;
unitPrice: number;
subtotal: number;
}
export interface InvoiceDetails {
id: string;
invoiceNumber: string;
date: Date;
dueDate?: Date;
amount: number;
currency?: string;
status: "paid" | "pending" | "failed" | "refunded" | "void";
description?: string;
lineItems: InvoiceLineItem[];
subtotal: number;
tax?: {
amount: number;
rate?: number;
label?: string;
};
discount?: {
amount: number;
code?: string;
label?: string;
};
total: number;
paymentMethod?: {
type: string;
last4?: string;
brand?: string;
};
billingAddress?: {
name: string;
line1: string;
line2?: string;
city: string;
state: string;
zip: string;
country: string;
};
downloadUrl?: string;
}
export interface BillingInvoiceDetailsProps {
invoice: InvoiceDetails | null;
open: boolean;
onOpenChange: (open: boolean) => void;
onDownload?: (invoiceId: string) => void;
onPrint?: (invoiceId: string) => void;
className?: string;
currency?: string;
}
function formatDate(date: Date): string {
const year = date.getFullYear();
const month = date.toLocaleString("en-US", { month: "short" });
const day = date.getDate();
return `${month} ${day}, ${year}`;
}
function formatPrice(amount: number, currency = "USD"): string {
return new Intl.NumberFormat("en-US", {
style: "currency",
currency,
minimumFractionDigits: 2,
maximumFractionDigits: 2,
}).format(amount);
}
function getStatusConfig(status: InvoiceDetails["status"]) {
switch (status) {
case "paid":
return {
label: "Paid",
variant: "default" as const,
className: "bg-green-500/10 text-green-600 border-green-500/20",
};
case "pending":
return {
label: "Pending",
variant: "secondary" as const,
className: "bg-yellow-500/10 text-yellow-600 border-yellow-500/20",
};
case "failed":
return {
label: "Failed",
variant: "destructive" as const,
className: "bg-destructive/10 text-destructive border-destructive/20",
};
case "refunded":
return {
label: "Refunded",
variant: "secondary" as const,
className: "bg-blue-500/10 text-blue-600 border-blue-500/20",
};
case "void":
return {
label: "Void",
variant: "secondary" as const,
className: "",
};
default:
return {
label: "Unknown",
variant: "secondary" as const,
className: "",
};
}
}
export default function BillingInvoiceDetails({
invoice,
open,
onOpenChange,
onDownload,
onPrint,
className,
currency = "USD",
}: BillingInvoiceDetailsProps) {
const [isDownloading, setIsDownloading] = useState(false);
const [isPrinting, setIsPrinting] = useState(false);
if (!invoice) return null;
const statusConfig = getStatusConfig(invoice.status);
const invoiceCurrency = invoice.currency || currency;
const handleDownload = async () => {
setIsDownloading(true);
try {
await onDownload?.(invoice.id);
} finally {
setIsDownloading(false);
}
};
const handlePrint = async () => {
setIsPrinting(true);
try {
await onPrint?.(invoice.id);
window.print();
} finally {
setIsPrinting(false);
}
};
return (
<Sheet onOpenChange={onOpenChange} open={open}>
<SheetContent
className={cn(
"flex flex-col gap-0 overflow-y-auto sm:max-w-2xl",
className
)}
>
<SheetHeader className="shrink-0">
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex min-w-0 flex-1 flex-col gap-2">
<SheetTitle className="wrap-break-word">
Invoice {invoice.invoiceNumber}
</SheetTitle>
<SheetDescription className="wrap-break-word">
{invoice.description ||
`Invoice dated ${formatDate(invoice.date)}`}
</SheetDescription>
</div>
<Badge
className={cn("shrink-0 text-xs", statusConfig.className)}
variant={statusConfig.variant}
>
{statusConfig.label}
</Badge>
</div>
</SheetHeader>
<div className="flex flex-1 flex-col gap-6 overflow-y-auto p-4">
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4 sm:flex-row sm:justify-between">
<div className="flex flex-col gap-2">
<h3 className="font-medium text-sm">Invoice Details</h3>
<div className="flex flex-col gap-1 text-muted-foreground text-sm">
<div className="flex flex-wrap items-center gap-2">
<span>Date:</span>
<span className="text-foreground">
{formatDate(invoice.date)}
</span>
</div>
{invoice.dueDate && (
<div className="flex flex-wrap items-center gap-2">
<span>Due Date:</span>
<span className="text-foreground">
{formatDate(invoice.dueDate)}
</span>
</div>
)}
<div className="flex flex-wrap items-center gap-2">
<span>Status:</span>
<Badge
className={cn("text-xs", statusConfig.className)}
variant={statusConfig.variant}
>
{statusConfig.label}
</Badge>
</div>
</div>
</div>
{invoice.billingAddress && (
<div className="flex flex-col gap-2">
<h3 className="font-medium text-sm">Billing Address</h3>
<div className="flex flex-col gap-1 text-muted-foreground text-sm">
<div className="wrap-break-word text-foreground">
{invoice.billingAddress.name}
</div>
<div className="wrap-break-word">
{invoice.billingAddress.line1}
</div>
{invoice.billingAddress.line2 && (
<div className="wrap-break-word">
{invoice.billingAddress.line2}
</div>
)}
<div className="wrap-break-word">
{invoice.billingAddress.city},{" "}
{invoice.billingAddress.state}{" "}
{invoice.billingAddress.zip}
</div>
<div className="wrap-break-word">
{invoice.billingAddress.country}
</div>
</div>
</div>
)}
</div>
<Separator />
<div className="flex flex-col gap-4">
<h3 className="font-medium text-sm">Line Items</h3>
<div className="flex flex-col gap-0 overflow-hidden rounded-lg border">
<div className="hidden grid-cols-[2fr_1fr_1fr_1fr] gap-4 border-b bg-muted/50 p-3 font-medium text-muted-foreground text-xs sm:grid">
<div>Description</div>
<div className="text-right">Quantity</div>
<div className="text-right">Unit Price</div>
<div className="text-right">Subtotal</div>
</div>
{invoice.lineItems.map((item, idx) => (
<div
className="flex flex-col gap-2 border-b p-3 last:border-b-0 sm:grid sm:grid-cols-[2fr_1fr_1fr_1fr] sm:gap-4 sm:gap-y-0"
key={idx}
>
<div className="wrap-break-word font-medium text-sm">
{item.description}
</div>
<div className="flex items-center justify-between text-muted-foreground text-sm sm:justify-end">
<span className="sm:hidden">Quantity:</span>
<span>{item.quantity}</span>
</div>
<div className="flex items-center justify-between text-muted-foreground text-sm sm:justify-end">
<span className="sm:hidden">Unit Price:</span>
<span>
{formatPrice(item.unitPrice, invoiceCurrency)}
</span>
</div>
<div className="flex items-center justify-between font-medium text-sm sm:justify-end">
<span className="sm:hidden">Subtotal:</span>
<span>{formatPrice(item.subtotal, invoiceCurrency)}</span>
</div>
</div>
))}
</div>
</div>
<Separator />
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Subtotal</span>
<span>{formatPrice(invoice.subtotal, invoiceCurrency)}</span>
</div>
{invoice.discount && (
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
Discount
{invoice.discount.label && ` (${invoice.discount.label})`}
{invoice.discount.code && ` - ${invoice.discount.code}`}
</span>
<span className="text-green-600">
-{formatPrice(invoice.discount.amount, invoiceCurrency)}
</span>
</div>
)}
{invoice.tax && (
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">
Tax{invoice.tax.label && ` (${invoice.tax.label})`}
{invoice.tax.rate !== undefined &&
` ${(invoice.tax.rate * 100).toFixed(1)}%`}
</span>
<span>
{formatPrice(invoice.tax.amount, invoiceCurrency)}
</span>
</div>
)}
<Separator />
<div className="flex items-center justify-between font-semibold text-base">
<span>Total</span>
<span>{formatPrice(invoice.total, invoiceCurrency)}</span>
</div>
</div>
{invoice.paymentMethod && (
<>
<Separator />
<div className="flex flex-col gap-2">
<h3 className="font-medium text-sm">Payment Method</h3>
<div className="flex flex-wrap items-center gap-2 text-muted-foreground text-sm">
<span className="capitalize">
{invoice.paymentMethod.type}
</span>
{invoice.paymentMethod.brand && (
<>
<span aria-hidden="true">•</span>
<span className="capitalize">
{invoice.paymentMethod.brand}
</span>
</>
)}
{invoice.paymentMethod.last4 && (
<>
<span aria-hidden="true">•</span>
<span>•••• {invoice.paymentMethod.last4}</span>
</>
)}
</div>
</div>
</>
)}
</div>
</div>
<div className="flex shrink-0 flex-col gap-2 border-t p-4 sm:flex-row sm:justify-end">
{onPrint && (
<Button
aria-label="Print invoice"
className="w-full sm:w-auto"
onClick={handlePrint}
type="button"
variant="outline"
>
{isPrinting ? (
<>
<Loader2 className="size-4 animate-spin" />
Printing…
</>
) : (
<>
<Printer className="size-4" />
Print
</>
)}
</Button>
)}
{onDownload && (
<Button
aria-label={`Download invoice ${invoice.invoiceNumber}`}
className="w-full sm:w-auto"
onClick={handleDownload}
type="button"
>
{isDownloading ? (
<>
<Loader2 className="size-4 animate-spin" />
Downloading…
</>
) : (
<>
<Download className="size-4" />
Download PDF
</>
)}
</Button>
)}
</div>
</SheetContent>
</Sheet>
);
}