- 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.
174 lines
5.3 KiB
TypeScript
174 lines
5.3 KiB
TypeScript
import type { ColumnDef } from '@tanstack/react-table';
|
|
import { Pencil, Printer, Trash2 } from 'lucide-react';
|
|
import { RowActions } from '@/components/data-display';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { edit, print } from '@/routes/admin/manage/invoices';
|
|
import { InvoiceShareButton } from './invoice-share-button';
|
|
|
|
export type Invoice = {
|
|
id: number;
|
|
invoice_number: string;
|
|
customer_name: string;
|
|
amount: number;
|
|
formatted_amount: string;
|
|
date: string;
|
|
formatted_date: string;
|
|
due_date: string | null;
|
|
formatted_due_date: string | null;
|
|
status: 'unpaid' | 'paid';
|
|
status_label: string;
|
|
description: string | null;
|
|
items_count: number;
|
|
};
|
|
|
|
export type InvoiceItem = {
|
|
id?: number;
|
|
name: string;
|
|
quantity: number;
|
|
price: number;
|
|
};
|
|
|
|
export type InvoiceForEdit = {
|
|
id: number;
|
|
invoice_number: string;
|
|
customer_name: string;
|
|
customer_phone: string | null;
|
|
customer_address: string | null;
|
|
date: string;
|
|
due_date: string | null;
|
|
status: 'unpaid' | 'paid';
|
|
description: string | null;
|
|
items: InvoiceItem[];
|
|
};
|
|
|
|
function getStatusBadge(status: string) {
|
|
const statusConfig: Record<string, { label: string; className: string }> = {
|
|
unpaid: {
|
|
label: 'Belum Lunas',
|
|
className: 'bg-yellow-100 text-yellow-800 hover:bg-yellow-100',
|
|
},
|
|
paid: {
|
|
label: 'Lunas',
|
|
className: 'bg-green-100 text-green-800 hover:bg-green-100',
|
|
},
|
|
};
|
|
|
|
const config = statusConfig[status] ?? statusConfig.unpaid;
|
|
|
|
return (
|
|
<Badge variant="secondary" className={config.className}>
|
|
{config.label}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
type CreateColumnsParams = {
|
|
handleDeleteClick: (invoice: Invoice) => void;
|
|
can: (permission: string) => boolean;
|
|
};
|
|
|
|
export function createInvoiceColumns(
|
|
params: CreateColumnsParams,
|
|
): ColumnDef<Invoice>[] {
|
|
const { handleDeleteClick, can } = params;
|
|
|
|
const columns: ColumnDef<Invoice>[] = [
|
|
{
|
|
accessorKey: 'invoice_number',
|
|
header: () => <span>Nomor Invoice</span>,
|
|
cell: ({ row }) => (
|
|
<span className="font-medium">
|
|
{row.getValue('invoice_number') as string}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'customer_name',
|
|
header: () => <span>Pelanggan</span>,
|
|
cell: ({ row }) => (
|
|
<span>{row.getValue('customer_name') as string}</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'items_count',
|
|
header: () => <span>Item</span>,
|
|
cell: ({ row }) => (
|
|
<span>{row.getValue('items_count') as number}</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'formatted_amount',
|
|
header: () => <span>Jumlah</span>,
|
|
cell: ({ row }) => (
|
|
<span>{row.getValue('formatted_amount') as string}</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'formatted_date',
|
|
header: () => <span>Tanggal</span>,
|
|
cell: ({ row }) => (
|
|
<span>{row.getValue('formatted_date') as string}</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'formatted_due_date',
|
|
header: () => <span>Jatuh Tempo</span>,
|
|
cell: ({ row }) => (
|
|
<span>
|
|
{(row.getValue('formatted_due_date') as string) ?? '-'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: 'status',
|
|
header: () => <span>Status</span>,
|
|
cell: ({ row }) => (
|
|
<span>{getStatusBadge(row.getValue('status') as string)}</span>
|
|
),
|
|
},
|
|
];
|
|
|
|
columns.push({
|
|
id: 'actions',
|
|
header: () => <span className="block text-center">Aksi</span>,
|
|
meta: {
|
|
className: 'w-[170px] text-center',
|
|
headerClassName: 'w-[170px] text-center',
|
|
},
|
|
cell: ({ row }) => {
|
|
const invoice = row.original;
|
|
|
|
return (
|
|
<div className="flex items-center justify-center gap-1">
|
|
<InvoiceShareButton invoice={invoice} />
|
|
<RowActions
|
|
actions={[
|
|
{
|
|
label: 'Cetak PDF',
|
|
icon: <Printer className="h-4 w-4" />,
|
|
href: print.url(invoice.id),
|
|
},
|
|
{
|
|
label: 'Edit',
|
|
icon: <Pencil className="h-4 w-4" />,
|
|
show: can('invoices.update'),
|
|
href: edit.url(invoice.id),
|
|
},
|
|
{
|
|
label: 'Hapus',
|
|
icon: (
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
),
|
|
show: can('invoices.delete'),
|
|
onClick: () => handleDeleteClick(invoice),
|
|
},
|
|
]}
|
|
/>
|
|
</div>
|
|
);
|
|
},
|
|
});
|
|
|
|
return columns;
|
|
}
|