import { router } from '@inertiajs/react'; import type { ColumnDef } from '@tanstack/react-table'; import { CheckCircle, ChevronRight, Clock, Pencil, RotateCw, Trash2, XCircle } from 'lucide-react'; import { RowActions } from '@/components/data-display'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Switch } from '@/components/ui/switch'; import { formatNumber } from '@/lib/format'; export type ProductVariant = { id: number; name: string; stock: number; formatted_stock: string; reject_stock: number; formatted_reject_stock: string; retail_stock: number; formatted_retail_stock: string; photo_urls: string[]; photo_conversion_urls: string[]; product_prices: { id: number; type: string; type_label: string; price: number; }[]; }; export type Product = { id: number; name: string; slug: string; description: string | null; status: string; is_featured: boolean; rejection_reason: string | null; categories: { id: number; name: string; }[]; product_variants: ProductVariant[]; }; function getStatusLabel(status: string): string { const labels: Record = { active: 'Aktif', inactive: 'Non Aktif', draft: 'Draft', pending: 'Menunggu Verifikasi', rejected: 'Ditolak', }; return labels[status] ?? status; } function getStatusVariant(status: string): string { const variants: Record = { active: 'bg-green-100 text-green-800', inactive: 'bg-red-100 text-red-800', draft: 'bg-yellow-100 text-yellow-800', pending: 'bg-orange-100 text-orange-800', rejected: 'bg-red-100 text-red-800', }; return variants[status] ?? 'bg-gray-100 text-gray-800'; } function getFilteredVariants( allVariants: ProductVariant[], searchValue: string, ): ProductVariant[] { const query = searchValue.toLowerCase().trim(); return query ? allVariants.filter((v) => v.name.toLowerCase().includes(query)) : allVariants; } type CreateColumnsParams = { handleEdit: (product: Product) => void; handleDeleteClick: (product: Product) => void; handleReject: (product: Product) => void; toggleStatusUrl: (id: number) => string; approveUrl: (id: number) => string; resubmitUrl: (id: number) => string; can: (permission: string) => boolean; hasRole: (role: string) => boolean; }; export function createProductColumns( params: CreateColumnsParams, ): ColumnDef[] { const { handleEdit, handleDeleteClick, handleReject, toggleStatusUrl, approveUrl, resubmitUrl, can, hasRole, } = params; const isVerifier = hasRole('developer') || hasRole('owner'); const columns: ColumnDef[] = [ { id: 'expand', header: '', cell: ({ row }) => { const hasVariants = (row.original.product_variants?.length ?? 0) > 0; if (!hasVariants) { return null; } return ( ); }, meta: { className: 'w-[40px]', headerClassName: 'w-[40px]', }, }, { id: 'variant_names', accessorFn: (row) => row.product_variants?.map((v) => v.name).join(' ') ?? '', header: () => null, cell: () => null, meta: { className: 'hidden', headerClassName: 'hidden', }, }, { accessorKey: 'name', header: () => Nama Produk, cell: ({ row }) => { const product = row.original; return (
{product.name} {product.status === 'pending' && ( Menunggu Verifikasi )} {product.status === 'rejected' && ( Ditolak )}
{product.categories ?.map((c) => c.name) .join(', ') || '-'}
); }, }, { id: 'variants', header: () => Varian, cell: ({ row, table }) => { const searchValue = (table .getColumn('variant_names') ?.getFilterValue() as string) ?? ''; const variants = getFilteredVariants( row.original.product_variants ?? [], searchValue, ); return ( {variants.length} varian ); }, }, { id: 'stock', header: () => Stok Bagus, meta: { className: 'w-[80px] text-center', headerClassName: 'w-[80px] text-center', }, cell: ({ row, table }) => { const searchValue = (table .getColumn('variant_names') ?.getFilterValue() as string) ?? ''; const variants = getFilteredVariants( row.original.product_variants ?? [], searchValue, ); const totalStock = variants.reduce( (sum, v) => sum + (v.stock ?? 0), 0, ); return ( {formatNumber(totalStock)} ); }, }, { id: 'reject_stock', header: () => ( Stok Reject ), meta: { className: 'w-[80px] text-center', headerClassName: 'w-[80px] text-center', }, cell: ({ row, table }) => { const searchValue = (table .getColumn('variant_names') ?.getFilterValue() as string) ?? ''; const variants = getFilteredVariants( row.original.product_variants ?? [], searchValue, ); const totalReject = variants.reduce( (sum, v) => sum + (v.reject_stock ?? 0), 0, ); return ( {formatNumber(totalReject)} ); }, }, { id: 'retail_stock', header: () => Stok Ecer, meta: { className: 'w-[80px] text-center', headerClassName: 'w-[80px] text-center', }, cell: ({ row, table }) => { const searchValue = (table .getColumn('variant_names') ?.getFilterValue() as string) ?? ''; const variants = getFilteredVariants( row.original.product_variants ?? [], searchValue, ); const totalRetail = variants.reduce( (sum, v) => sum + (v.retail_stock ?? 0), 0, ); return ( {formatNumber(totalRetail)} ); }, }, { id: 'total_stock', header: () => Total Stok, meta: { className: 'w-[80px] text-center', headerClassName: 'w-[80px] text-center', }, cell: ({ row, table }) => { const searchValue = (table .getColumn('variant_names') ?.getFilterValue() as string) ?? ''; const variants = getFilteredVariants( row.original.product_variants ?? [], searchValue, ); const totalStock = variants.reduce( (sum, v) => sum + (v.stock ?? 0), 0, ); const totalReject = variants.reduce( (sum, v) => sum + (v.reject_stock ?? 0), 0, ); const totalRetail = variants.reduce( (sum, v) => sum + (v.retail_stock ?? 0), 0, ); const total = totalStock + totalReject + totalRetail; return ( {formatNumber(total)} ); }, }, { accessorKey: 'status', header: () => Status, cell: ({ row }) => { const product = row.original; const isChecked = product.status === 'active'; function handleToggle() { router.post( toggleStatusUrl(product.id), {}, { preserveScroll: true }, ); } if (product.status === 'active' || product.status === 'inactive') { return (
{getStatusLabel(product.status)}
); } if (product.status === 'rejected' && product.rejection_reason) { return (
{getStatusLabel(product.status)} Alasan: {product.rejection_reason}
); } return ( {getStatusLabel(product.status)} ); }, }, ]; // Actions column columns.push({ id: 'actions', header: () => Aksi, meta: { className: 'w-[100px] text-center', headerClassName: 'w-[100px] text-center', }, cell: ({ row }) => { const product = row.original; // Pending + verifier: Setujui / Tolak if (product.status === 'pending' && isVerifier) { return ( , onClick: () => router.post(approveUrl(product.id), {}, { preserveScroll: true }), }, { label: 'Tolak', icon: , onClick: () => handleReject(product), }, ]} /> ); } // Pending + non-verifier: no actions if (product.status === 'pending') { return null; } // Rejected + verifier: no actions if (product.status === 'rejected' && isVerifier) { return null; } // Rejected + non-verifier: Ajukan Ulang / Edit / Hapus if (product.status === 'rejected') { return ( , onClick: () => router.post(resubmitUrl(product.id), {}, { preserveScroll: true }), }, { label: 'Edit', icon: , show: can('products.update'), onClick: () => handleEdit(product), }, { label: 'Hapus', icon: , show: can('products.delete'), onClick: () => handleDeleteClick(product), }, ]} /> ); } // Active/Inactive/Draft: Edit / Hapus if (can('products.update') || can('products.delete')) { return ( , show: can('products.update'), onClick: () => handleEdit(product), }, { label: 'Hapus', icon: , show: can('products.delete'), onClick: () => handleDeleteClick(product), }, ]} /> ); } return null; }, }); return columns; }