feat: add formatted stock display and enhance product variant attributes
This commit is contained in:
parent
1fc058f974
commit
0a6a441ec4
@ -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, ',', '.'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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[];
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user