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,49 +48,94 @@ 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(
<TableRow key={item.id}> ([productName, groupItems]) => {
<TableCell className="text-center"> const totalQty = groupItems.reduce(
{index + 1} (sum, item) => sum + (item.quantity ?? 0),
</TableCell> 0,
<TableCell> );
{item.product_variant?.photo_url ? ( const totalSubtotal = groupItems.reduce(
<ImagePreviewButton (sum, item) => sum + (item.subtotal ?? 0),
srcs={[ 0,
item.product_variant.photo_url, );
]}
title={item.product_variant.name} return (
/> <Fragment key={productName}>
) : ( <TableRow>
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground"> <TableCell
N/A colSpan={4}
</div> className="font-medium text-muted-foreground bg-muted/20"
)} >
</TableCell> {productName}
<TableCell> </TableCell>
{item.product_variant?.product?.name ?? '-'} <TableCell className="text-center font-medium text-muted-foreground bg-muted/20">
</TableCell> {formatNumber(totalQty)}
<TableCell> </TableCell>
{item.product_variant?.name ?? '-'} <TableCell className="text-right font-medium text-muted-foreground bg-muted/20">
</TableCell> {formatCurrency(totalSubtotal)}
<TableCell className="text-right"> </TableCell>
{formatCurrency(item.unit_price)} </TableRow>
</TableCell> {groupItems.map((item) => {
<TableCell className="text-center"> counter++;
{formatNumber(item.quantity)} return (
</TableCell> <TableRow key={item.id}>
<TableCell className="text-right font-medium"> <TableCell className="text-center">
{formatCurrency(item.subtotal)} {counter}
</TableCell> </TableCell>
</TableRow> <TableCell>
)) {item.product_variant
?.photo_url ? (
<ImagePreviewButton
srcs={[
item
.product_variant
.photo_url,
]}
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">
N/A
</div>
)}
</TableCell>
<TableCell>
{item.product_variant
?.name ?? '-'}
</TableCell>
<TableCell className="text-right">
{formatCurrency(
item.unit_price,
)}
</TableCell>
<TableCell className="text-center">
{formatNumber(
item.quantity,
)}
</TableCell>
<TableCell className="text-right font-medium">
{formatCurrency(
item.subtotal,
)}
</TableCell>
</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">