250 lines
11 KiB
TypeScript
250 lines
11 KiB
TypeScript
import { router } from '@inertiajs/react';
|
|
import { CheckCircle, ChevronDown, Clock, Pencil, RotateCw, Trash2, XCircle } from 'lucide-react';
|
|
import { RowActions } from '@/components/data-display';
|
|
import { ToggleStatus } from '@/components/data-display';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Card, CardContent } from '@/components/ui/card';
|
|
import { useCan } from '@/hooks/use-can';
|
|
import { formatNumber } from '@/lib/format';
|
|
import type { Product } from './columns';
|
|
|
|
function getStatusLabel(status: string): string {
|
|
const labels: Record<string, string> = {
|
|
active: 'Aktif',
|
|
inactive: 'Non Aktif',
|
|
draft: 'Draft',
|
|
pending: 'Menunggu Verifikasi',
|
|
rejected: 'Ditolak',
|
|
};
|
|
|
|
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',
|
|
pending: 'bg-orange-100 text-orange-800',
|
|
rejected: 'bg-red-100 text-red-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;
|
|
onReject: (product: Product) => void;
|
|
toggleStatusUrl: (id: number) => string;
|
|
toggleFeaturedUrl: (id: number) => string;
|
|
approveUrl: (id: number) => string;
|
|
resubmitUrl: (id: number) => string;
|
|
};
|
|
|
|
export function ProductCardRow({
|
|
product,
|
|
index,
|
|
isExpanded,
|
|
onToggleExpand,
|
|
onEdit,
|
|
onDelete,
|
|
onReject,
|
|
toggleStatusUrl,
|
|
toggleFeaturedUrl,
|
|
approveUrl,
|
|
resubmitUrl,
|
|
}: ProductCardRowParams) {
|
|
const { can, hasRole } = useCan();
|
|
const isVerifier = hasRole('developer') || hasRole('owner');
|
|
const variantCount = product.variants_count ?? 0;
|
|
const totalStock = Number(product.total_stock ?? 0);
|
|
const totalReject = Number(product.total_reject_stock ?? 0);
|
|
const totalRetail = Number(product.total_retail_stock ?? 0);
|
|
const totalAll = totalStock + totalReject + totalRetail;
|
|
|
|
const isPending = product.status === 'pending';
|
|
const isRejected = product.status === 'rejected';
|
|
const isChecked = product.status === 'active';
|
|
|
|
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>
|
|
{product.name}
|
|
</h3>
|
|
{isPending && (
|
|
<Badge variant="secondary" className="bg-orange-100 text-orange-800 hover:bg-orange-100">
|
|
<Clock className="mr-1 h-3 w-3" />
|
|
Menunggu Verifikasi
|
|
</Badge>
|
|
)}
|
|
{isRejected && (
|
|
<Badge variant="secondary" className="bg-red-100 text-red-800 hover:bg-red-100">
|
|
<XCircle className="mr-1 h-3 w-3" />
|
|
Ditolak
|
|
</Badge>
|
|
)}
|
|
{product.categories?.length > 0 && (
|
|
<span className="text-xs text-muted-foreground">
|
|
(
|
|
{product.categories
|
|
.map((c) => c.name)
|
|
.join(', ')}
|
|
)
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{isRejected && product.rejection_reason && (
|
|
<div className="mt-1 text-[0.65rem] text-muted-foreground max-w-[300px]">
|
|
Alasan penolakan: {product.rejection_reason}
|
|
</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">
|
|
{variantCount} 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">
|
|
{(product.status === 'active' || product.status === 'inactive') ? (
|
|
<ToggleStatus
|
|
url={toggleStatusUrl(product.id)}
|
|
checked={isChecked}
|
|
label={getStatusLabel(product.status)}
|
|
disabled={!can('products.toggle_status')}
|
|
/>
|
|
) : (
|
|
<span
|
|
className={`inline-flex items-center rounded-md px-2 py-1 text-xs font-medium ${getStatusVariant(product.status)}`}
|
|
>
|
|
{getStatusLabel(product.status)}
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{(product.status === 'active' || product.status === 'inactive') && (
|
|
<div className="mt-2">
|
|
<ToggleStatus
|
|
url={toggleFeaturedUrl(product.id)}
|
|
checked={product.is_featured}
|
|
label={product.is_featured ? 'Ditampilkan di Halaman Depan' : 'Tidak Ditampilkan'}
|
|
disabled={!can('products.toggle_featured')}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
{isPending && isVerifier ? (
|
|
<RowActions
|
|
actions={[
|
|
{
|
|
label: 'Setujui',
|
|
icon: <CheckCircle className="h-4 w-4 text-green-600" />,
|
|
onClick: () => router.post(approveUrl(product.id), {}, { preserveScroll: true }),
|
|
},
|
|
{
|
|
label: 'Tolak',
|
|
icon: <XCircle className="h-4 w-4 text-destructive" />,
|
|
onClick: () => onReject(product),
|
|
},
|
|
]}
|
|
wrapperClassName="flex shrink-0 items-center gap-1"
|
|
/>
|
|
) : isRejected && !isVerifier ? (
|
|
<RowActions
|
|
actions={[
|
|
{
|
|
label: 'Ajukan Ulang',
|
|
icon: <RotateCw className="h-4 w-4 text-blue-600" />,
|
|
onClick: () => router.post(resubmitUrl(product.id), {}, { preserveScroll: true }),
|
|
},
|
|
{
|
|
label: 'Edit',
|
|
icon: <Pencil className="h-4 w-4" />,
|
|
show: can('products.update'),
|
|
onClick: () => onEdit(product),
|
|
},
|
|
{
|
|
label: 'Hapus',
|
|
icon: <Trash2 className="h-4 w-4 text-destructive" />,
|
|
show: can('products.delete'),
|
|
onClick: () => onDelete(product),
|
|
},
|
|
]}
|
|
wrapperClassName="flex shrink-0 items-center gap-1"
|
|
/>
|
|
) : !isPending && !(isRejected && isVerifier) && (can('products.update') || can('products.delete')) ? (
|
|
<RowActions
|
|
actions={[
|
|
{
|
|
label: 'Edit',
|
|
icon: <Pencil className="h-4 w-4" />,
|
|
show: can('products.update'),
|
|
onClick: () => onEdit(product),
|
|
},
|
|
{
|
|
label: 'Hapus',
|
|
icon: <Trash2 className="h-4 w-4 text-destructive" />,
|
|
show: can('products.delete'),
|
|
onClick: () => onDelete(product),
|
|
},
|
|
]}
|
|
wrapperClassName="flex shrink-0 items-center gap-1"
|
|
/>
|
|
) : null}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|