feat: enhance product and raw material models with additional stock formatting attributes and update image preview components to include descriptions

This commit is contained in:
Yoga Pangestu 2026-08-12 22:14:08 +07:00
parent 1b48f5c24f
commit fbdad6923c
11 changed files with 44 additions and 8 deletions

View File

@ -17,7 +17,7 @@
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media; use Spatie\MediaLibrary\MediaCollections\Models\Media;
#[Appends(['formatted_name', 'formatted_stock'])] #[Appends(['formatted_name', 'formatted_stock', 'formatted_reject_stock', 'formatted_retail_stock'])]
#[Guarded(['id'])] #[Guarded(['id'])]
#[ScopedBy([ProductVariantScope::class])] #[ScopedBy([ProductVariantScope::class])]
class ProductVariant extends Model implements HasMedia class ProductVariant extends Model implements HasMedia
@ -47,6 +47,20 @@ protected function formattedStock(): Attribute
); );
} }
protected function formattedRejectStock(): Attribute
{
return Attribute::make(
get: fn () => number_format($this->reject_stock, 0, ',', '.'),
);
}
protected function formattedRetailStock(): Attribute
{
return Attribute::make(
get: fn () => number_format($this->retail_stock, 0, ',', '.'),
);
}
public function orderItems(): HasMany public function orderItems(): HasMany
{ {
return $this->hasMany(OrderItem::class); return $this->hasMany(OrderItem::class);

View File

@ -18,7 +18,7 @@
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media; use Spatie\MediaLibrary\MediaCollections\Models\Media;
#[Appends(['formatted_price'])] #[Appends(['formatted_price', 'formatted_stock'])]
#[Guarded(['id'])] #[Guarded(['id'])]
#[ScopedBy([RawMaterialPriceScope::class])] #[ScopedBy([RawMaterialPriceScope::class])]
class RawMaterialPrice extends Model implements HasMedia class RawMaterialPrice extends Model implements HasMedia
@ -40,6 +40,13 @@ protected function formattedPrice(): Attribute
); );
} }
protected function formattedStock(): Attribute
{
return Attribute::make(
get: fn () => number_format($this->stock, 0, ',', '.'),
);
}
public function cuttingMaterials(): HasMany public function cuttingMaterials(): HasMany
{ {
return $this->hasMany(CuttingMaterial::class); return $this->hasMany(CuttingMaterial::class);

View File

@ -6,6 +6,7 @@ type ImagePreviewButtonProps = {
modalSrc?: string; modalSrc?: string;
modalSrcs?: string[]; modalSrcs?: string[];
title?: string; title?: string;
description?: string;
alt?: string; alt?: string;
className?: string; className?: string;
}; };
@ -15,6 +16,7 @@ export function ImagePreviewButton({
modalSrc, modalSrc,
modalSrcs, modalSrcs,
title, title,
description,
alt, alt,
className = 'h-10 w-10', className = 'h-10 w-10',
}: ImagePreviewButtonProps) { }: ImagePreviewButtonProps) {
@ -46,6 +48,7 @@ export function ImagePreviewButton({
src={modalSources?.[0] ?? srcs[0]} src={modalSources?.[0] ?? srcs[0]}
sources={modalSources ?? srcs} sources={modalSources ?? srcs}
title={title} title={title}
description={description}
/> />
</> </>
); );

View File

@ -4,6 +4,7 @@ import { Button } from '@/components/ui/button';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogDescription,
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
@ -13,6 +14,7 @@ type ImagePreviewModalProps = {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
src: string | null; src: string | null;
title?: string; title?: string;
description?: string;
alt?: string; alt?: string;
sources?: string[]; sources?: string[];
}; };
@ -22,6 +24,7 @@ export function ImagePreviewModal({
onOpenChange, onOpenChange,
src, src,
title, title,
description,
alt = 'Preview', alt = 'Preview',
sources, sources,
}: ImagePreviewModalProps) { }: ImagePreviewModalProps) {
@ -55,9 +58,10 @@ setCurrentIndex(0);
}} }}
> >
<DialogContent showCloseButton> <DialogContent showCloseButton>
{title && ( {(title || description) && (
<DialogHeader> <DialogHeader>
<DialogTitle>{title}</DialogTitle> {title && <DialogTitle>{title}</DialogTitle>}
{description && <DialogDescription>{description}</DialogDescription>}
</DialogHeader> </DialogHeader>
)} )}
{currentSrc && ( {currentSrc && (

View File

@ -133,7 +133,8 @@ export function CuttingCardRow({
<ImagePreviewButton <ImagePreviewButton
srcs={[cutting.photo_conversion_url ?? cutting.photo_url]} srcs={[cutting.photo_conversion_url ?? cutting.photo_url]}
modalSrc={cutting.photo_url} modalSrc={cutting.photo_url}
title="Foto Cutting" title={productName}
description={cutting.description ?? formatDateTime(cutting.created_at)}
className="h-16 w-16" className="h-16 w-16"
/> />
</div> </div>

View File

@ -120,7 +120,8 @@ export function PurchaseCardRow({
<ImagePreviewButton <ImagePreviewButton
srcs={[purchase.photo_conversion_url ?? purchase.photo_url]} srcs={[purchase.photo_conversion_url ?? purchase.photo_url]}
modalSrc={purchase.photo_url} modalSrc={purchase.photo_url}
title="Foto Belanja" title={purchase.supplier.name}
description={formatDateTime(purchase.created_at)}
className="h-16 w-16" className="h-16 w-16"
/> />
</div> </div>

View File

@ -203,7 +203,8 @@ export function TransactionCardRow({
<ImagePreviewButton <ImagePreviewButton
srcs={[transaction.photo_conversion_url ?? transaction.photo_url]} srcs={[transaction.photo_conversion_url ?? transaction.photo_url]}
modalSrc={transaction.photo_url} modalSrc={transaction.photo_url}
title="Foto Pesanan" title={transaction.customer?.name ?? transaction.order_number}
description={transaction.notes ?? formatDateTime(transaction.created_at)}
className="h-16 w-16" className="h-16 w-16"
/> />
</div> </div>

View File

@ -11,9 +11,11 @@ export type ProductVariant = {
id: number; id: number;
name: string; name: string;
stock: number; stock: number;
formatted_stock: string formatted_stock: string;
reject_stock: number; reject_stock: number;
formatted_reject_stock: string;
retail_stock: number; retail_stock: number;
formatted_retail_stock: string;
photo_urls: string[]; photo_urls: string[];
photo_conversion_urls: string[]; photo_conversion_urls: string[];
product_prices: { product_prices: {

View File

@ -93,6 +93,7 @@ export function VariantSubRow({
srcs={variant.photo_conversion_urls?.length > 0 ? variant.photo_conversion_urls : variant.photo_urls} srcs={variant.photo_conversion_urls?.length > 0 ? variant.photo_conversion_urls : variant.photo_urls}
modalSrcs={variant.photo_urls} modalSrcs={variant.photo_urls}
title={variant.name} title={variant.name}
description={`Stok Bagus: ${variant.formatted_stock} | Stok Reject: ${variant.formatted_reject_stock} | Stok Ecer: ${variant.formatted_retail_stock}`}
/> />
) : ( ) : (
<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">

View File

@ -3,6 +3,7 @@ export type RawMaterialVariant = {
variant: string; variant: string;
price: number; price: number;
stock: number; stock: number;
formatted_stock: string;
photo_url: string | null; photo_url: string | null;
photo_conversion_url: string | null; photo_conversion_url: string | null;
}; };

View File

@ -90,6 +90,7 @@ export function RawMaterialVariantSubRow({
srcs={[variant.photo_conversion_url ?? variant.photo_url]} srcs={[variant.photo_conversion_url ?? variant.photo_url]}
modalSrc={variant.photo_url} modalSrc={variant.photo_url}
title={variant.variant} title={variant.variant}
description={`Stok: ${variant.formatted_stock}`}
/> />
) : ( ) : (
<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">