- Add a custom hook `useCardTableExpand` for managing expandable card tables. - Refactor product columns to include variant edit and delete handlers. - Integrate `CardTable` component for displaying products with expandable variant details. - Create `ProductCardRow` component for rendering product information in a card format. - Implement variant editing functionality with a dedicated `ProductVariantEdit` component. - Add `VariantSubRow` component to display variant details in a table format. - Update routes to handle product variant editing and deletion. - Enhance tests to cover variant editing and deletion scenarios.
198 lines
7.9 KiB
TypeScript
198 lines
7.9 KiB
TypeScript
import { router } from '@inertiajs/react';
|
|
import { ChevronDown, Pencil, Trash2 } from 'lucide-react';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { Switch } from '@/components/ui/switch';
|
|
import {
|
|
Tooltip,
|
|
TooltipContent,
|
|
TooltipProvider,
|
|
TooltipTrigger,
|
|
} from '@/components/ui/tooltip';
|
|
import type { Product } from './columns';
|
|
|
|
function formatNumber(num: number): string {
|
|
return new Intl.NumberFormat('id-ID').format(num);
|
|
}
|
|
|
|
function getStatusLabel(status: string): string {
|
|
const labels: Record<string, string> = {
|
|
active: 'Aktif',
|
|
inactive: 'Non Aktif',
|
|
draft: 'Draft',
|
|
};
|
|
return labels[status] ?? status;
|
|
}
|
|
|
|
function getStatusVariant(status: string): string {
|
|
const variants: Record<string, string> = {
|
|
active: 'bg-green-100 text-green-800',
|
|
inactive: 'bg-red-100 text-red-800',
|
|
draft: 'bg-yellow-100 text-yellow-800',
|
|
};
|
|
return variants[status] ?? 'bg-gray-100 text-gray-800';
|
|
}
|
|
|
|
export type ProductCardRowParams = {
|
|
product: Product;
|
|
index: number;
|
|
isExpanded: boolean;
|
|
onToggleExpand: () => void;
|
|
onEdit: (product: Product) => void;
|
|
onDelete: (product: Product) => void;
|
|
toggleStatusUrl: (id: number) => string;
|
|
};
|
|
|
|
export function ProductCardRow({
|
|
product,
|
|
index,
|
|
isExpanded,
|
|
onToggleExpand,
|
|
onEdit,
|
|
onDelete,
|
|
toggleStatusUrl,
|
|
}: ProductCardRowParams) {
|
|
const variants = product.product_variants ?? [];
|
|
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 totalAll = totalStock + totalReject + totalRetail;
|
|
|
|
const isToggleable =
|
|
product.status === 'active' || product.status === 'inactive';
|
|
const isChecked = product.status === 'active';
|
|
|
|
function handleToggle() {
|
|
router.post(toggleStatusUrl(product.id), {}, { preserveScroll: true });
|
|
}
|
|
|
|
return (
|
|
<Card className="overflow-hidden">
|
|
<CardContent className="p-0">
|
|
<div className="flex items-start gap-3 p-4">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className="mt-0.5 h-6 w-6 shrink-0"
|
|
onClick={onToggleExpand}
|
|
>
|
|
<ChevronDown
|
|
className={`h-4 w-4 transition-transform ${isExpanded ? 'rotate-180' : ''}`}
|
|
/>
|
|
</Button>
|
|
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-xs text-muted-foreground">
|
|
{index}.
|
|
</span>
|
|
<h3 className="truncate font-medium">
|
|
{product.name}
|
|
</h3>
|
|
{product.categories?.length > 0 && (
|
|
<span className="text-xs text-muted-foreground">
|
|
(
|
|
{product.categories
|
|
.map((c) => c.name)
|
|
.join(', ')}
|
|
)
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="mt-2 flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
|
|
<span className="inline-flex items-center rounded-md bg-muted px-2 py-1 font-medium text-foreground">
|
|
{variants.length} varian
|
|
</span>
|
|
<span>
|
|
Bagus:{' '}
|
|
<span className="font-medium text-foreground">
|
|
{formatNumber(totalStock)}
|
|
</span>
|
|
</span>
|
|
<span>
|
|
Reject:{' '}
|
|
<span className="font-medium text-foreground">
|
|
{formatNumber(totalReject)}
|
|
</span>
|
|
</span>
|
|
<span>
|
|
Ecer:{' '}
|
|
<span className="font-medium text-foreground">
|
|
{formatNumber(totalRetail)}
|
|
</span>
|
|
</span>
|
|
<span>
|
|
Total:{' '}
|
|
<span className="font-medium text-foreground">
|
|
{formatNumber(totalAll)}
|
|
</span>
|
|
</span>
|
|
</div>
|
|
|
|
<div className="mt-2">
|
|
{isToggleable ? (
|
|
<div className="flex items-center gap-2">
|
|
<Switch
|
|
size="sm"
|
|
checked={isChecked}
|
|
onCheckedChange={handleToggle}
|
|
/>
|
|
<span
|
|
className={`text-xs font-medium ${isChecked ? 'text-green-700' : 'text-red-700'}`}
|
|
>
|
|
{getStatusLabel(product.status)}
|
|
</span>
|
|
</div>
|
|
) : (
|
|
<span
|
|
className={`inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${getStatusVariant(product.status)}`}
|
|
>
|
|
{getStatusLabel(product.status)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<TooltipProvider>
|
|
<div className="flex shrink-0 items-center gap-1">
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => onEdit(product)}
|
|
>
|
|
<Pencil className="h-4 w-4" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top">Edit</TooltipContent>
|
|
</Tooltip>
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => onDelete(product)}
|
|
>
|
|
<Trash2 className="h-4 w-4 text-destructive" />
|
|
</Button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="top">
|
|
Hapus
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
</div>
|
|
</TooltipProvider>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|