- Implemented StokOpnameEdit component for editing stock opname entries. - Created StokOpnameIndex component for listing and managing stock opnames. - Added StokOpnameCardRow and StokOpnameItemSubRow components for displaying stock opname details. - Introduced routes for stok opname CRUD operations and actions (submit, verify, reject, cancel). - Integrated UI components for better user interaction and data presentation.
172 lines
8.9 KiB
TypeScript
172 lines
8.9 KiB
TypeScript
import { Fragment } from 'react';
|
|
import { ImagePreviewButton } from '@/components/dialogs';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import { formatNumber } from '@/lib/format';
|
|
import type { StokOpname, StokOpnameItem } from './columns';
|
|
|
|
const STOCK_QUALITY_CONFIG: Record<string, { label: string; className: string }> = {
|
|
good: { label: 'Bagus', className: 'bg-green-100 text-green-800' },
|
|
reject: { label: 'Reject', className: 'bg-red-100 text-red-800' },
|
|
retail: { label: 'Ecer', className: 'bg-blue-100 text-blue-800' },
|
|
};
|
|
|
|
export function StokOpnameItemSubRow({
|
|
stokOpname,
|
|
items: loadedItems,
|
|
isLoading,
|
|
}: {
|
|
stokOpname: StokOpname;
|
|
items: StokOpnameItem[];
|
|
isLoading: boolean;
|
|
}) {
|
|
const items = loadedItems ?? [];
|
|
|
|
const groupedByProduct = items.reduce(
|
|
(acc, item) => {
|
|
const productName = item.product_name ?? 'Tanpa Produk';
|
|
|
|
if (!acc[productName]) {
|
|
acc[productName] = {};
|
|
}
|
|
|
|
const variantName = item.variant_name ?? '-';
|
|
if (!acc[productName][variantName]) {
|
|
acc[productName][variantName] = [];
|
|
}
|
|
|
|
acc[productName][variantName].push(item);
|
|
|
|
return acc;
|
|
},
|
|
{} as Record<string, Record<string, typeof items>>,
|
|
);
|
|
|
|
let counter = 0;
|
|
|
|
return (
|
|
<div className="overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead className="w-[50px] text-center">
|
|
No
|
|
</TableHead>
|
|
<TableHead className="w-[60px]">Foto</TableHead>
|
|
<TableHead>Varian</TableHead>
|
|
<TableHead className="text-center">Jenis</TableHead>
|
|
<TableHead className="text-right">Stok Sistem</TableHead>
|
|
<TableHead className="text-right">Stok Fisik</TableHead>
|
|
<TableHead className="text-right">Selisih</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{isLoading ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={7}
|
|
className="text-center text-muted-foreground"
|
|
>
|
|
Memuat item...
|
|
</TableCell>
|
|
</TableRow>
|
|
) : items.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={7}
|
|
className="text-center text-muted-foreground"
|
|
>
|
|
Tidak ada item.
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
Object.entries(groupedByProduct).map(
|
|
([productName, variants]) => {
|
|
const variantEntries = Object.entries(variants);
|
|
|
|
return (
|
|
<Fragment key={productName}>
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={7}
|
|
className="font-semibold text-muted-foreground bg-muted/30"
|
|
>
|
|
{productName}
|
|
</TableCell>
|
|
</TableRow>
|
|
{variantEntries.map(([variantName, variantItems]) => {
|
|
const firstItem = variantItems[0];
|
|
|
|
return (
|
|
<Fragment key={`${productName}-${variantName}`}>
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={7}
|
|
className="font-medium text-muted-foreground"
|
|
>
|
|
<div className="flex items-center gap-2">
|
|
{firstItem?.photo_url ? (
|
|
<ImagePreviewButton
|
|
srcs={[
|
|
firstItem.photo_conversion_url ?? firstItem.photo_url,
|
|
]}
|
|
modalSrc={firstItem.photo_url}
|
|
title={variantName}
|
|
/>
|
|
) : (
|
|
<div className="flex h-6 w-6 shrink-0 items-center justify-center rounded bg-muted text-[10px] text-muted-foreground">
|
|
N/A
|
|
</div>
|
|
)}
|
|
{variantName}
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
{variantItems.map((item) => {
|
|
counter++;
|
|
const qualityConfig = STOCK_QUALITY_CONFIG[item.stock_quality] ?? STOCK_QUALITY_CONFIG.good;
|
|
|
|
return (
|
|
<TableRow key={item.id}>
|
|
<TableCell className="text-center">
|
|
{counter}
|
|
</TableCell>
|
|
<TableCell />
|
|
<TableCell />
|
|
<TableCell className="text-center">
|
|
<span className={`inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium ${qualityConfig.className}`}>
|
|
{qualityConfig.label}
|
|
</span>
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{formatNumber(item.system_stock)}
|
|
</TableCell>
|
|
<TableCell className="text-right font-medium">
|
|
{formatNumber(item.physical_stock)}
|
|
</TableCell>
|
|
<TableCell className={`text-right font-medium ${item.difference > 0 ? 'text-green-600' : item.difference < 0 ? 'text-red-600' : ''}`}>
|
|
{item.difference > 0 ? '+' : ''}{formatNumber(item.difference)}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</Fragment>
|
|
);
|
|
})}
|
|
</Fragment>
|
|
);
|
|
},
|
|
)
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
);
|
|
}
|