255 lines
10 KiB
TypeScript
255 lines
10 KiB
TypeScript
import { Head, router, Link } from '@inertiajs/react';
|
|
import type { Product } from '@/types';
|
|
import { ColumnDef } from '@tanstack/react-table';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Trash2, Pencil, Plus } from 'lucide-react';
|
|
import { DataTable } from '@/components/data-table';
|
|
import { DataTableColumnHeader } from '@/components/data-table-column-header';
|
|
import { useState } from 'react';
|
|
import { toast } from 'sonner';
|
|
import { TooltipContent, TooltipTrigger } from '@radix-ui/react-tooltip';
|
|
import { Tooltip } from '@/components/ui/tooltip';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import productRoutes from '@/routes/product';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogMedia,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog"
|
|
|
|
export default function ProductIndex({ products }: { products: Product[] }) {
|
|
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
|
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
|
|
const [productToDelete, setProductToDelete] = useState<Product | null>(null);
|
|
const [rowsToDelete, setRowsToDelete] = useState<any[]>([]);
|
|
const [rowSelection, setRowSelection] = useState({});
|
|
|
|
const onDelete = (product: Product) => {
|
|
setProductToDelete(product);
|
|
setIsDeleteDialogOpen(true);
|
|
};
|
|
|
|
const confirmDelete = () => {
|
|
if (productToDelete) {
|
|
router.delete(productRoutes.destroy(productToDelete.id).url, {
|
|
onSuccess: (response: any) => {
|
|
toast.success(response.props.flash.success);
|
|
setIsDeleteDialogOpen(false);
|
|
setProductToDelete(null);
|
|
setRowSelection({});
|
|
},
|
|
});
|
|
}
|
|
};
|
|
|
|
const confirmBulkDelete = () => {
|
|
router.post(productRoutes.bulkDestroy().url, {
|
|
ids: rowsToDelete.map((row: any) => row.id),
|
|
_method: 'DELETE'
|
|
}, {
|
|
onSuccess: (response: any) => {
|
|
toast.success(response.props.flash.success);
|
|
setIsBulkDeleteDialogOpen(false);
|
|
setRowsToDelete([]);
|
|
setRowSelection({});
|
|
},
|
|
});
|
|
};
|
|
|
|
const onToggleStatus = (id: number) => {
|
|
router.patch(productRoutes.toggleStatus(id).url, {}, {
|
|
onSuccess: (response: any) => toast.success(response.props.flash.success),
|
|
});
|
|
};
|
|
|
|
const columns: ColumnDef<Product>[] = [
|
|
{
|
|
accessorKey: "name",
|
|
header: ({ column }) => {
|
|
return (
|
|
<DataTableColumnHeader column={column} title="Nama" />
|
|
)
|
|
},
|
|
meta: { title: "Nama" },
|
|
},
|
|
{
|
|
accessorKey: "categories",
|
|
header: ({ column }) => {
|
|
return (
|
|
<DataTableColumnHeader column={column} title="Kategori" />
|
|
)
|
|
},
|
|
meta: { title: "Kategori" },
|
|
cell: ({ row }) => {
|
|
const product = row.original;
|
|
return (
|
|
<div className="flex flex-wrap gap-1">
|
|
{product.categories?.map((category) => (
|
|
<Badge key={category.id} variant="secondary">
|
|
{category.title}
|
|
</Badge>
|
|
))}
|
|
{(!product.categories || product.categories.length === 0) && (
|
|
<span className="text-muted-foreground text-xs italic">Tanpa Kategori</span>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
accessorKey: "is_active",
|
|
header: "Status",
|
|
meta: { title: "Status" },
|
|
cell: ({ row }) => {
|
|
const product = row.original;
|
|
return (
|
|
<Switch
|
|
checked={product.is_active}
|
|
onCheckedChange={() => onToggleStatus(product.id)}
|
|
/>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
id: "actions",
|
|
header: "Aksi",
|
|
cell: ({ row }) => {
|
|
const product = row.original;
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Link href={productRoutes.edit(product.id).url}>
|
|
<Button variant="ghost" size="icon" className='text-yellow-600'>
|
|
<Pencil className="size-4" />
|
|
</Button>
|
|
</Link>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Ubah</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button variant="ghost" size="icon" className='text-red-600' onClick={() => onDelete(product)}>
|
|
<Trash2 className="size-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent>
|
|
<p>Hapus</p>
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
);
|
|
},
|
|
meta: { title: "Aksi" },
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6 p-6">
|
|
<Head title="Produk" />
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Produk</h1>
|
|
</div>
|
|
<Link href={productRoutes.create().url}>
|
|
<Button>
|
|
Tambah
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
|
|
<Card className="overflow-hidden border-none shadow-lg bg-card/50 backdrop-blur-sm p-5">
|
|
<CardContent className="p-0">
|
|
<DataTable
|
|
columns={columns}
|
|
data={products}
|
|
rowSelection={rowSelection}
|
|
onRowSelectionChange={setRowSelection}
|
|
filters={[
|
|
{
|
|
columnId: 'is_active',
|
|
title: 'Status',
|
|
options: [
|
|
{ label: 'Aktif', value: 'true' },
|
|
{ label: 'Tidak Aktif', value: 'false' },
|
|
]
|
|
}
|
|
]}
|
|
bulkActions={[
|
|
{
|
|
label: 'Hapus Terpilih',
|
|
onClick: (rows) => {
|
|
setRowsToDelete(rows);
|
|
setIsBulkDeleteDialogOpen(true);
|
|
},
|
|
icon: Trash2,
|
|
variant: 'destructive'
|
|
},
|
|
]}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<AlertDialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
|
<AlertDialogContent size="default">
|
|
<AlertDialogHeader>
|
|
<AlertDialogMedia className="bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive">
|
|
<Trash2 className="size-5" />
|
|
</AlertDialogMedia>
|
|
<AlertDialogTitle>Hapus produk?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Tindakan ini tidak dapat dibatalkan. Produk <strong>{productToDelete?.name}</strong> akan dihapus secara permanen.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel variant="outline">Batal</AlertDialogCancel>
|
|
<AlertDialogAction onClick={confirmDelete} variant="destructive">Hapus</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
|
|
<AlertDialog open={isBulkDeleteDialogOpen} onOpenChange={setIsBulkDeleteDialogOpen}>
|
|
<AlertDialogContent size="default">
|
|
<AlertDialogHeader>
|
|
<AlertDialogMedia className="bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive">
|
|
<Trash2 className="size-5" />
|
|
</AlertDialogMedia>
|
|
<AlertDialogTitle>Hapus {rowsToDelete.length} produk?</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Tindakan ini tidak dapat dibatalkan. <strong>{rowsToDelete.length}</strong> item yang terpilih akan dihapus secara permanen.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel variant="outline">Batal</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={confirmBulkDelete}
|
|
variant="destructive"
|
|
>
|
|
Hapus
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
ProductIndex.layout = {
|
|
breadcrumbs: [
|
|
{
|
|
title: 'Master',
|
|
},
|
|
],
|
|
};
|