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\InteractsWithMedia;
#[Appends(['formatted_name'])]
#[Appends(['formatted_name', 'formatted_stock'])]
#[Guarded(['id'])]
#[ScopedBy([ProductVariantScope::class])]
class ProductVariant extends Model implements HasMedia
@ -34,7 +34,14 @@ protected function casts(): array
protected function formattedName(): Attribute
{
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,
TableRow,
} from '@/components/ui/table';
import { Fragment } from 'react';
import { formatNumber } from '@/lib/format';
import { formatCurrency } from '@/lib/utils';
import type { Restock } from './columns';
@ -14,8 +15,20 @@ import type { Restock } from './columns';
export function RestockItemSubRow({ restock }: { restock: Restock }) {
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 (
<div className="space-y-4 overflow-x-auto">
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
@ -23,7 +36,6 @@ export function RestockItemSubRow({ restock }: { restock: Restock }) {
No
</TableHead>
<TableHead className="w-[60px]">Foto</TableHead>
<TableHead>Produk</TableHead>
<TableHead>Varian</TableHead>
<TableHead className="text-right">
Harga Modal
@ -36,49 +48,94 @@ export function RestockItemSubRow({ restock }: { restock: Restock }) {
{items.length === 0 ? (
<TableRow>
<TableCell
colSpan={7}
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.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?.product?.name ?? '-'}
</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>
))
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}>
<TableCell className="text-center">
{counter}
</TableCell>
<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>
</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 { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
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 = {
id: number;
name: string;
stock: number;
formatted_stock: string
reject_stock: number;
retail_stock: number;
photo_urls: string[];

View File

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