- Updated ProductIndex component to improve category and product filtering with memoization. - Refactored Combobox components for better performance and usability. - Added PurchaseController routes for managing purchases with appropriate permissions. - Created comprehensive tests for purchase management, covering creation, updating, and deletion scenarios. - Ensured proper handling of raw materials and their variants during purchase operations. - Implemented validation for required fields in purchase creation and updates.
136 lines
4.8 KiB
TypeScript
136 lines
4.8 KiB
TypeScript
import { useState } from 'react';
|
|
import { ImagePreviewModal } from '@/components/image-preview-modal';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import type { Purchase } from './columns';
|
|
|
|
function formatCurrency(amount: number): string {
|
|
return new Intl.NumberFormat('id-ID', {
|
|
style: 'currency',
|
|
currency: 'IDR',
|
|
minimumFractionDigits: 0,
|
|
}).format(amount);
|
|
}
|
|
|
|
function formatNumber(num: number): string {
|
|
return new Intl.NumberFormat('id-ID', {
|
|
maximumFractionDigits: 4,
|
|
}).format(num);
|
|
}
|
|
|
|
function formatDateTime(dateString: string): string {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('id-ID', {
|
|
day: '2-digit',
|
|
month: 'short',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
});
|
|
}
|
|
|
|
function VariantPhotoPreview({ url, title }: { url: string; title: string }) {
|
|
const [open, setOpen] = useState(false);
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(true)}
|
|
className="block h-10 w-10 overflow-hidden rounded-md border transition-opacity hover:opacity-80"
|
|
>
|
|
<img
|
|
src={url}
|
|
alt={title}
|
|
className="h-full w-full object-cover"
|
|
/>
|
|
</button>
|
|
<ImagePreviewModal
|
|
open={open}
|
|
onOpenChange={setOpen}
|
|
src={url}
|
|
title={title}
|
|
/>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export function PurchaseItemSubRow({
|
|
purchase,
|
|
}: {
|
|
purchase: Purchase;
|
|
}) {
|
|
const items = purchase.purchase_items ?? [];
|
|
const unit =
|
|
items[0]?.raw_material_price?.raw_material?.unit ?? '';
|
|
|
|
return (
|
|
<div className="space-y-4 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-right">Harga</TableHead>
|
|
<TableHead className="text-center">Qty</TableHead>
|
|
<TableHead className="text-right">Subtotal</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{items.length === 0 ? (
|
|
<TableRow>
|
|
<TableCell
|
|
colSpan={6}
|
|
className="text-center text-muted-foreground"
|
|
>
|
|
Tidak ada item.
|
|
</TableCell>
|
|
</TableRow>
|
|
) : (
|
|
items.map((item, index) => (
|
|
<TableRow key={item.id}>
|
|
<TableCell className="text-center">
|
|
{index + 1}
|
|
</TableCell>
|
|
<TableCell>
|
|
{item.raw_material_price?.photo_url ? (
|
|
<VariantPhotoPreview
|
|
url={item.raw_material_price.photo_url}
|
|
title={item.raw_material_price.variant}
|
|
/>
|
|
) : (
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">
|
|
N/A
|
|
</div>
|
|
)}
|
|
</TableCell>
|
|
<TableCell>
|
|
{item.raw_material_price?.variant ?? '-'}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
{formatCurrency(item.unit_price)}
|
|
</TableCell>
|
|
<TableCell className="text-center">
|
|
{formatNumber(item.quantity)} {unit}
|
|
</TableCell>
|
|
<TableCell className="text-right font-medium">
|
|
{formatCurrency(item.subtotal)}
|
|
</TableCell>
|
|
</TableRow>
|
|
))
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
);
|
|
}
|