feat: add formatted stock display and enhance product variant attributes

This commit is contained in:
Yoga Pangestu 2026-08-06 14:16:29 +07:00
parent 1fc058f974
commit 0a6a441ec4
4 changed files with 110 additions and 46 deletions

View File

@ -15,7 +15,7 @@
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
#[Appends(['formatted_name'])] #[Appends(['formatted_name', 'formatted_stock'])]
#[Guarded(['id'])] #[Guarded(['id'])]
#[ScopedBy([ProductVariantScope::class])] #[ScopedBy([ProductVariantScope::class])]
class ProductVariant extends Model implements HasMedia class ProductVariant extends Model implements HasMedia
@ -34,7 +34,14 @@ protected function casts(): array
protected function formattedName(): Attribute protected function formattedName(): Attribute
{ {
return Attribute::make( return Attribute::make(
get: fn () => ucfirst($this->name), get: fn() => ucfirst($this->name),
);
}
protected function formattedStock(): Attribute
{
return Attribute::make(
get: fn() => number_format($this->stock, 0, ',', '.'),
); );
} }

View File

@ -7,6 +7,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { Fragment } from 'react';
import { formatNumber } from '@/lib/format'; import { formatNumber } from '@/lib/format';
import { formatCurrency } from '@/lib/utils'; import { formatCurrency } from '@/lib/utils';
import type { Restock } from './columns'; import type { Restock } from './columns';
@ -14,8 +15,20 @@ import type { Restock } from './columns';
export function RestockItemSubRow({ restock }: { restock: Restock }) { export function RestockItemSubRow({ restock }: { restock: Restock }) {
const items = restock.restock_items ?? []; const items = restock.restock_items ?? [];
const groupedByProduct = items.reduce(
(acc, item) => {
const name = item.product_variant?.product?.name ?? 'Tanpa Produk';
if (!acc[name]) acc[name] = [];
acc[name].push(item);
return acc;
},
{} as Record<string, typeof items>,
);
let counter = 0;
return ( return (
<div className="space-y-4 overflow-x-auto"> <div className="overflow-x-auto">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@ -23,7 +36,6 @@ export function RestockItemSubRow({ restock }: { restock: Restock }) {
No No
</TableHead> </TableHead>
<TableHead className="w-[60px]">Foto</TableHead> <TableHead className="w-[60px]">Foto</TableHead>
<TableHead>Produk</TableHead>
<TableHead>Varian</TableHead> <TableHead>Varian</TableHead>
<TableHead className="text-right"> <TableHead className="text-right">
Harga Modal Harga Modal
@ -36,25 +48,61 @@ export function RestockItemSubRow({ restock }: { restock: Restock }) {
{items.length === 0 ? ( {items.length === 0 ? (
<TableRow> <TableRow>
<TableCell <TableCell
colSpan={7} colSpan={6}
className="text-center text-muted-foreground" className="text-center text-muted-foreground"
> >
Tidak ada item. Tidak ada item.
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
items.map((item, index) => ( Object.entries(groupedByProduct).map(
([productName, groupItems]) => {
const totalQty = groupItems.reduce(
(sum, item) => sum + (item.quantity ?? 0),
0,
);
const totalSubtotal = groupItems.reduce(
(sum, item) => sum + (item.subtotal ?? 0),
0,
);
return (
<Fragment key={productName}>
<TableRow>
<TableCell
colSpan={4}
className="font-medium text-muted-foreground bg-muted/20"
>
{productName}
</TableCell>
<TableCell className="text-center font-medium text-muted-foreground bg-muted/20">
{formatNumber(totalQty)}
</TableCell>
<TableCell className="text-right font-medium text-muted-foreground bg-muted/20">
{formatCurrency(totalSubtotal)}
</TableCell>
</TableRow>
{groupItems.map((item) => {
counter++;
return (
<TableRow key={item.id}> <TableRow key={item.id}>
<TableCell className="text-center"> <TableCell className="text-center">
{index + 1} {counter}
</TableCell> </TableCell>
<TableCell> <TableCell>
{item.product_variant?.photo_url ? ( {item.product_variant
?.photo_url ? (
<ImagePreviewButton <ImagePreviewButton
srcs={[ srcs={[
item.product_variant.photo_url, item
.product_variant
.photo_url,
]} ]}
title={item.product_variant.name} title={
item
.product_variant
.name
}
/> />
) : ( ) : (
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground"> <div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">
@ -63,22 +111,31 @@ export function RestockItemSubRow({ restock }: { restock: Restock }) {
)} )}
</TableCell> </TableCell>
<TableCell> <TableCell>
{item.product_variant?.product?.name ?? '-'} {item.product_variant
</TableCell> ?.name ?? '-'}
<TableCell>
{item.product_variant?.name ?? '-'}
</TableCell> </TableCell>
<TableCell className="text-right"> <TableCell className="text-right">
{formatCurrency(item.unit_price)} {formatCurrency(
item.unit_price,
)}
</TableCell> </TableCell>
<TableCell className="text-center"> <TableCell className="text-center">
{formatNumber(item.quantity)} {formatNumber(
item.quantity,
)}
</TableCell> </TableCell>
<TableCell className="text-right font-medium"> <TableCell className="text-right font-medium">
{formatCurrency(item.subtotal)} {formatCurrency(
item.subtotal,
)}
</TableCell> </TableCell>
</TableRow> </TableRow>
)) );
})}
</Fragment>
);
},
)
)} )}
</TableBody> </TableBody>
</Table> </Table>

View File

@ -1,16 +1,16 @@
import { router } from '@inertiajs/react';
import type { ColumnDef } from '@tanstack/react-table';
import { ChevronRight, Pencil, Trash2 } from 'lucide-react';
import { RowActions } from '@/components/row-actions'; import { RowActions } from '@/components/row-actions';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch'; import { Switch } from '@/components/ui/switch';
import { formatNumber } from '@/lib/format'; import { formatNumber } from '@/lib/format';
import { formatCurrency } from '@/lib/utils'; import { router } from '@inertiajs/react';
import type { ColumnDef } from '@tanstack/react-table';
import { ChevronRight, Pencil, Trash2 } from 'lucide-react';
export type ProductVariant = { export type ProductVariant = {
id: number; id: number;
name: string; name: string;
stock: number; stock: number;
formatted_stock: string
reject_stock: number; reject_stock: number;
retail_stock: number; retail_stock: number;
photo_urls: string[]; photo_urls: string[];

View File

@ -67,7 +67,7 @@ export function TransferStockDialog({
Stok Bagus Tersedia Stok Bagus Tersedia
</Label> </Label>
<p className="text-sm font-medium"> <p className="text-sm font-medium">
{variant.stock} {variant.formatted_stock}
</p> </p>
</div> </div>
<div className="grid gap-2"> <div className="grid gap-2">