dstpabuaran.com/resources/js/pages/admin/manage/invoice/print.tsx
Yoga Pangestu 2a0b99c599 feat: implement invoice management features including index, create, update, delete, and print functionalities
- Added InvoiceIndex component for displaying a list of invoices with pagination and search capabilities.
- Created InvoiceShareButton component for sharing invoice details via WhatsApp.
- Developed InvoicePrint component for printing invoice details.
- Defined routes for invoice management including share and print functionalities.
- Implemented InvoiceTest to cover authentication, authorization, and CRUD operations for invoices.
2026-08-22 15:13:28 +07:00

222 lines
9.3 KiB
TypeScript

import { Head, Link } from '@inertiajs/react';
import { ArrowLeft, Printer } from 'lucide-react';
import { AppLogoIcon } from '@/components/brand';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { formatCurrency } from '@/lib/utils';
import { index as invoiceIndex } from '@/routes/admin/manage/invoices';
import { InvoiceShareButton } from './invoice-share-button';
type InvoiceItem = {
id: number;
name: string;
quantity: number;
price: number;
subtotal: number;
};
type Company = {
name: string;
address: string | null;
email: string | null;
phone: string | null;
};
type PrintInvoice = {
id: number;
invoice_number: string;
customer_name: string;
customer_phone: string | null;
customer_address: string | null;
amount: number;
formatted_amount: string;
formatted_date: string;
formatted_due_date: string | null;
status: 'unpaid' | 'paid';
status_label: string;
description: string | null;
items: InvoiceItem[];
company: Company;
};
type Props = {
invoice: PrintInvoice;
};
const STATUS_BADGE_CLASSES: Record<string, string> = {
unpaid: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-100',
paid: 'bg-green-100 text-green-800 hover:bg-green-100',
};
export default function InvoicePrint({ invoice }: Props) {
const { company } = invoice;
return (
<>
<Head title={`Invoice ${invoice.invoice_number}`} />
<div className="mx-auto max-w-3xl px-4 py-8 sm:px-6 lg:px-8">
<div className="mb-6 flex items-center justify-between print:hidden">
<Button asChild variant="outline">
<Link href={invoiceIndex.url()}>
<ArrowLeft className="h-4 w-4" />
Kembali
</Link>
</Button>
<div className="flex items-center gap-2">
<InvoiceShareButton invoice={invoice} />
<Button onClick={() => window.print()}>
<Printer className="h-4 w-4" />
Cetak / Simpan PDF
</Button>
</div>
</div>
<div className="relative overflow-hidden rounded-lg border bg-card shadow-sm print:rounded-none print:border-none print:shadow-none">
<AppLogoIcon className="pointer-events-none absolute inset-0 m-auto h-80 w-80 object-contain opacity-20 print:opacity-25" />
<div className="relative z-10 border-b bg-muted/30 p-6 print:bg-white">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<h1 className="text-2xl font-bold tracking-tight">
INVOICE
</h1>
<p className="mt-1 font-medium">
{company.name}
</p>
{company.address && (
<p className="max-w-xs text-sm text-muted-foreground">
{company.address}
</p>
)}
{(company.phone || company.email) && (
<p className="text-sm text-muted-foreground">
{[company.phone, company.email]
.filter(Boolean)
.join(' · ')}
</p>
)}
</div>
<div className="text-right">
<p className="text-lg font-semibold">
{invoice.invoice_number}
</p>
<Badge
variant="secondary"
className={
STATUS_BADGE_CLASSES[invoice.status]
}
>
{invoice.status_label}
</Badge>
</div>
</div>
</div>
<div className="relative z-10 grid grid-cols-1 gap-6 p-6 sm:grid-cols-2">
<div>
<p className="text-sm text-muted-foreground">
Ditagihkan Kepada
</p>
<p className="font-medium">
{invoice.customer_name}
</p>
{invoice.customer_phone && (
<p className="text-sm text-muted-foreground">
{invoice.customer_phone}
</p>
)}
{invoice.customer_address && (
<p className="max-w-xs text-sm text-muted-foreground">
{invoice.customer_address}
</p>
)}
</div>
<div className="sm:text-right">
<p className="text-sm text-muted-foreground">
Tanggal Invoice
</p>
<p className="font-medium">
{invoice.formatted_date}
</p>
{invoice.formatted_due_date && (
<>
<p className="mt-2 text-sm text-muted-foreground">
Jatuh Tempo
</p>
<p className="font-medium">
{invoice.formatted_due_date}
</p>
</>
)}
</div>
</div>
<div className="relative z-10 px-6 pb-6">
<Table>
<TableHeader>
<TableRow>
<TableHead>Item</TableHead>
<TableHead className="text-center">
Jumlah
</TableHead>
<TableHead className="text-right">
Harga Satuan
</TableHead>
<TableHead className="text-right">
Subtotal
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{invoice.items.map((item) => (
<TableRow key={item.id}>
<TableCell>{item.name}</TableCell>
<TableCell className="text-center">
{item.quantity}
</TableCell>
<TableCell className="text-right">
{formatCurrency(item.price)}
</TableCell>
<TableCell className="text-right">
{formatCurrency(item.subtotal)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<div className="mt-4 flex justify-end">
<div className="w-full max-w-xs space-y-2">
<div className="flex items-center justify-between border-t pt-2 text-base font-semibold">
<span>Total</span>
<span>{invoice.formatted_amount}</span>
</div>
</div>
</div>
{invoice.description && (
<div className="mt-6 border-t pt-4">
<p className="text-sm text-muted-foreground">
Keterangan
</p>
<p className="mt-1 text-sm">
{invoice.description}
</p>
</div>
)}
</div>
</div>
</div>
</>
);
}