dstpabuaran.com/resources/js/pages/admin/manage/cutting/edit.tsx
Yoga Pangestu 166419134f Refactor component imports for consistency and organization
- Updated import paths for various components to align with new directory structure.
- Changed imports from 'row-actions', 'confirm-dialog', 'image-preview-button', and 'file-upload' to their respective new locations in 'data-display', 'dialogs', and 'inputs'.
- Adjusted imports in multiple pages including purchase, restock, transaction, category, customer, product, raw-material, supplier, roles, and settings.
- Ensured all relevant components are imported from their new locations to maintain functionality.
2026-08-07 13:48:46 +07:00

748 lines
44 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use no memo';
import { Form, Head, Link, usePage } from '@inertiajs/react';
import { ArrowLeft, Check, Layers, Plus, ShoppingCart, Trash2 } from 'lucide-react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { toast } from 'sonner';
import { ConfirmDialog } from '@/components/dialogs';
import { FileUpload } from '@/components/inputs';
import { ImagePreviewModal } from '@/components/dialogs';
import { InputError } from '@/components/ui';
import { NumberInput } from '@/components/inputs';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from '@/components/ui/combobox';
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';
import { Textarea } from '@/components/ui/textarea';
import { formatNumber } from '@/lib/format';
import { getTemporaryUrl } from '@/lib/upload';
import { formatCurrency } from '@/lib/utils';
import { index as cuttingIndex, update } from '@/routes/admin/manage/cuttings';
import type { CuttingCreateData, CuttingForEdit } from './columns';
type MaterialState = {
id?: number;
raw_material_price_id: number;
material_usage: number;
material_result: number;
combination_id: number | null;
variant: string;
material_name: string;
unit: string;
photo_url: string | null;
};
type CombinationState = {
id?: number;
material_result: number;
};
type Props = {
cutting: CuttingForEdit;
data: CuttingCreateData;
};
export default function CuttingEdit({ cutting, data }: Props) {
const { rawMaterials } = data;
const { errors } = usePage().props as { errors: Record<string, string> };
const [materials, setMaterials] = useState<MaterialState[]>(() => {
const comboIdToIndex = new Map<number, number>();
cutting.combinations.forEach((c, i) => comboIdToIndex.set(c.id, i));
return cutting.materials.map((m) => {
const rawMaterial = rawMaterials.find((rm) =>
rm.raw_material_prices.some((p) => p.id === m.raw_material_price_id),
);
const price = rawMaterial?.raw_material_prices.find(
(p) => p.id === m.raw_material_price_id,
);
return {
id: m.id,
raw_material_price_id: m.raw_material_price_id,
material_usage: m.material_usage,
material_result: m.material_result ?? 0,
combination_id:
m.combination_id !== null
? (comboIdToIndex.get(m.combination_id) ?? null)
: null,
variant: price?.variant ?? m.variant ?? '',
material_name: rawMaterial?.name ?? '',
unit: rawMaterial?.unit ?? '',
photo_url: price?.photo_url ?? m.photo_url ?? null,
};
});
});
const [combinations, setCombinations] = useState<CombinationState[]>(
() =>
cutting.combinations.map((c) => ({
id: c.id,
material_result: c.material_result ?? 0,
})),
);
const [selectedMaterialName, setSelectedMaterialName] = useState('');
const [productName, setProductName] = useState(cutting.product_name);
const [sample, setSample] = useState(cutting.sample);
const [originalOutsideSample, setOriginalOutsideSample] = useState(cutting.original_outside_sample);
const cuttingResult = sample + originalOutsideSample;
const [notes, setNotes] = useState(cutting.description ?? '');
const [photo, setPhoto] = useState<string | null>(cutting.photo_key);
const [photoUrl, setPhotoUrl] = useState<string | null>(cutting.photo_url);
const [uploading, setUploading] = useState(false);
const submittingRef = useRef(false);
const [cartOpen, setCartOpen] = useState(false);
const [previewKey, setPreviewKey] = useState<string | null>(null);
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [deleteMaterialIndex, setDeleteMaterialIndex] = useState<number | null>(null);
const [cartDeleteConfirmOpen, setCartDeleteConfirmOpen] = useState(false);
const [cartDeleteIndex, setCartDeleteIndex] = useState<number | null>(null);
const [comboDeleteConfirmOpen, setComboDeleteConfirmOpen] = useState(false);
const [comboDeleteIndex, setComboDeleteIndex] = useState<number | null>(null);
const [comboDialogOpen, setComboDialogOpen] = useState(false);
const [comboMaterialName, setComboMaterialName] = useState('');
const [comboSelectedPriceIds, setComboSelectedPriceIds] = useState<number[]>([]);
const [comboResult, setComboResult] = useState(0);
const comboMaterial = useMemo(
() => rawMaterials.find((m) => m.name === comboMaterialName) ?? null,
[rawMaterials, comboMaterialName],
);
const materialsRef = useRef(materials);
materialsRef.current = materials;
const priceMap = useMemo(
() => new Map(rawMaterials.flatMap((m) => m.raw_material_prices.map((p) => [p.id, p]))),
[rawMaterials],
);
const selectedMaterial = useMemo(
() => rawMaterials.find((m) => m.name === selectedMaterialName) ?? null,
[rawMaterials, selectedMaterialName],
);
const addVariant = useCallback(
(priceId: number) => {
if (!selectedMaterial) {
return;
}
const price = selectedMaterial.raw_material_prices.find((p) => p.id === priceId);
if (!price) {
return;
}
setMaterials((prev) => {
if (prev.some((m) => m.raw_material_price_id === priceId && m.combination_id === null)) {
return prev;
}
return [
...prev,
{
raw_material_price_id: price.id,
material_usage: 0,
material_result: 0,
combination_id: null,
variant: price.variant,
material_name: selectedMaterial.name,
unit: selectedMaterial.unit,
photo_url: price.photo_url,
},
];
});
},
[selectedMaterial],
);
const openComboDialog = useCallback((materialName: string, preSelectPriceId?: number) => {
setComboMaterialName(materialName);
setComboSelectedPriceIds(preSelectPriceId ? [preSelectPriceId] : []);
setComboResult(0);
setComboDialogOpen(true);
}, []);
const toggleComboPrice = useCallback((priceId: number) => {
setComboSelectedPriceIds((prev) =>
prev.includes(priceId) ? prev.filter((id) => id !== priceId) : [...prev, priceId],
);
}, []);
const confirmCombo = useCallback(() => {
if (comboSelectedPriceIds.length < 2) {
return;
}
const comboIndex = combinations.length;
const newMaterials: MaterialState[] = comboSelectedPriceIds.map((priceId) => {
let foundMaterial: (typeof rawMaterials)[number] | undefined;
let foundPrice: (typeof rawMaterials)[number]['raw_material_prices'][number] | undefined;
for (const rm of rawMaterials) {
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
if (p) {
foundMaterial = rm;
foundPrice = p;
break;
}
}
return {
raw_material_price_id: priceId,
material_usage: 0,
material_result: 0,
combination_id: comboIndex,
variant: foundPrice?.variant ?? '',
material_name: foundMaterial?.name ?? '',
unit: foundMaterial?.unit ?? '',
photo_url: foundPrice?.photo_url ?? null,
};
});
setMaterials((prev) => [...prev, ...newMaterials]);
setCombinations((prev) => [
...prev,
{
material_result: comboResult,
},
]);
setComboDialogOpen(false);
setComboMaterialName('');
setComboSelectedPriceIds([]);
setComboResult(0);
}, [comboSelectedPriceIds, comboResult, rawMaterials, combinations.length]);
const updateMaterial = useCallback(
(index: number, field: keyof MaterialState, value: unknown) => {
setMaterials((prev) => {
const updated = [...prev];
(updated[index] as Record<string, unknown>)[field] = value;
return updated;
});
},
[],
);
const updateCombinationResult = useCallback((comboIndex: number, value: number) => {
setCombinations((prev) => prev.map((c, i) => (i === comboIndex ? { ...c, material_result: value } : c)));
}, []);
const totalMaterialCost = useMemo(() => {
return materials.reduce((sum, m) => {
const price = priceMap.get(m.raw_material_price_id);
return sum + (price ? price.price * m.material_usage : 0);
}, 0);
}, [materials, priceMap]);
const totalCost = totalMaterialCost;
const costPerUnit = cuttingResult > 0 ? Math.floor(totalCost / cuttingResult) : 0;
function formatQuantity(value: number): string {
return formatNumber(value, { maximumFractionDigits: 4 });
}
function getPayload() {
return {
description: notes || null,
product_name: productName || null,
sample: sample,
original_outside_sample: originalOutsideSample,
cutting_result: cuttingResult,
materials: materialsRef.current.map((m) => ({
raw_material_price_id: m.raw_material_price_id,
material_usage: m.material_usage,
material_result: m.material_result,
combination_index: m.combination_id,
})),
combinations: combinations.map((c) => ({
material_result: c.material_result,
})),
photo_key: photo,
};
}
return (
<>
<Head title="Edit Cutting" />
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-semibold tracking-tight">Edit Cutting</h2>
<Button asChild variant="outline">
<Link href={cuttingIndex.url()}>
<ArrowLeft className="h-4 w-4" />
Kembali
</Link>
</Button>
</div>
<Form action={update(cutting.id)} transform={(data) => ({ ...data, ...getPayload() })} onSubmit={() => {
submittingRef.current = true;
}} onError={() => {
toast.error('Terjadi kesalahan saat menyimpan data. Silakan periksa kembali input Anda.');
}}>
{({ errors, processing }) => (
<div className="grid gap-6 md:grid-cols-3">
<div className="space-y-6 md:col-span-2">
<Card>
<CardHeader>
<CardTitle>Pilih Bahan Baku</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-2">
<Label>
Nama Bahan Baku <span className="text-destructive">*</span>
</Label>
<Combobox
items={rawMaterials}
itemToStringLabel={(m) => m.name}
value={selectedMaterial}
onValueChange={(value) => setSelectedMaterialName(value?.name ?? '')}
>
<ComboboxInput placeholder="Cari bahan baku..." className="w-full" />
<ComboboxContent>
<ComboboxEmpty>Tidak ada bahan baku ditemukan.</ComboboxEmpty>
<ComboboxList>
{(m) => (
<ComboboxItem key={m.id} value={m}>
{m.name} ({m.unit})
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{selectedMaterial && selectedMaterial.raw_material_prices.length > 0 && (
<div className="space-y-2">
<Label className="text-sm font-medium">Pilih Varian</Label>
<div className="space-y-2">
{selectedMaterial.raw_material_prices.map((price) => {
const isAdded = materials.some((m) => m.raw_material_price_id === price.id);
const addedCount = materials.filter((m) => m.raw_material_price_id === price.id).length;
return (
<div key={price.id} className={isAdded ? 'flex items-center justify-between gap-3 rounded-lg border border-primary p-3' : 'flex items-center justify-between gap-3 rounded-lg border p-3'}>
<div className="flex min-w-0 items-center gap-3">
{price.photo_url ? (
<img src={price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
) : (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
)}
<div className="min-w-0">
<p className="truncate font-medium">{price.variant}</p>
<p className="text-xs text-muted-foreground">
Stok: {formatNumber(Number(price.stock))} {selectedMaterial.unit} · {formatCurrency(price.price)}
{addedCount > 0 && ` · ×${addedCount}`}
</p>
</div>
</div>
<div className="flex shrink-0 items-center gap-1">
<Button type="button" variant="outline" size="sm" onClick={() => addVariant(price.id)}>
<Plus className="h-4 w-4" />
Tambah
</Button>
<Button type="button" variant="outline" size="sm" onClick={() => openComboDialog(selectedMaterial.name, price.id)}>
<Layers className="h-4 w-4" />
Kombinasi
</Button>
</div>
</div>
);
})}
</div>
</div>
)}
</CardContent>
</Card>
</div>
<div className="space-y-6 md:col-span-1">
<Card className="sticky top-6">
<CardHeader>
<CardTitle>Ringkasan</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid gap-2">
<Label htmlFor="product_name">
Nama Produk <span className="text-destructive">*</span>
</Label>
<Input id="product_name" name="product_name" value={productName} onChange={(e) => setProductName(e.target.value)} placeholder="Masukkan nama produk" />
<InputError message={errors.product_name} />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="grid gap-2">
<Label htmlFor="sample">
Sample <span className="text-destructive">*</span>
</Label>
<NumberInput id="sample" value={sample} onValueChange={setSample} />
<InputError message={errors.sample} />
</div>
<div className="grid gap-2">
<Label htmlFor="original_outside_sample">
Diluar Sample <span className="text-destructive">*</span>
</Label>
<NumberInput id="original_outside_sample" value={originalOutsideSample} onValueChange={setOriginalOutsideSample} />
<InputError message={errors.original_outside_sample} />
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="cutting_result">Hasil</Label>
<NumberInput id="cutting_result" value={cuttingResult} disabled />
<InputError message={errors.cutting_result} />
</div>
<div className="space-y-2">
<div className="border-t pt-2">
<div className="flex items-center justify-between text-sm font-semibold">
<span>Total</span>
<span>{formatCurrency(totalCost)}</span>
</div>
</div>
{cuttingResult > 0 && (
<div className="flex items-center justify-between text-sm">
<span className="text-muted-foreground">Biaya Per Produk</span>
<span className="font-medium">{formatCurrency(costPerUnit)}</span>
</div>
)}
</div>
<div className="grid gap-2">
<Label htmlFor="notes">Keterangan</Label>
<Textarea id="notes" value={notes} onChange={(e) => setNotes(e.target.value)} placeholder="Masukkan keterangan" maxLength={100} />
<InputError message={errors.description} />
</div>
<div className="grid gap-2">
<Label>Foto</Label>
<FileUpload value={photo} onChange={(key) => {
setPhoto(key); setPhotoUrl(key ? getTemporaryUrl(key) : null);
}} folder="cutting" existingUrl={photoUrl} onUploadingChange={setUploading} />
<InputError message={errors.photo_key} />
</div>
<Button type="submit" className="w-full" disabled={processing}>
{processing ? 'Menyimpan...' : 'Simpan'}
</Button>
</CardContent>
</Card>
</div>
</div>
)}
</Form>
<Button type="button" onClick={() => setCartOpen(true)} className="fixed top-1/2 right-4 z-50 h-14 w-14 -translate-y-1/2 rounded-full shadow-lg" size="icon" aria-label="Buka keranjang cutting">
<ShoppingCart className="h-5 w-5" />
{materials.length > 0 && (
<span className="absolute -top-1 -right-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-destructive px-1 text-xs font-semibold text-white">
{materials.length}
</span>
)}
</Button>
<Sheet open={cartOpen} onOpenChange={setCartOpen}>
<SheetContent side="right" className="w-full sm:max-w-md">
<SheetHeader>
<SheetTitle>Keranjang Cutting</SheetTitle>
</SheetHeader>
<div className="flex-1 space-y-3 overflow-y-auto px-6 pb-6">
{materials.length === 0 ? (
<p className="text-sm text-muted-foreground">Keranjang kosong.</p>
) : (
(() => {
const groups: { comboIndex: number | null; items: { m: MaterialState; index: number }[] }[] = [];
const comboMap = new Map<number, { m: MaterialState; index: number }[]>();
const singleItems: { m: MaterialState; index: number }[] = [];
materials.forEach((m, i) => {
if (m.combination_id !== null) {
if (!comboMap.has(m.combination_id)) {
comboMap.set(m.combination_id, []);
}
comboMap.get(m.combination_id)!.push({ m, index: i });
} else {
singleItems.push({ m, index: i });
}
});
comboMap.forEach((items, comboIdx) => groups.push({ comboIndex: comboIdx, items }));
singleItems.forEach((item) => groups.push({ comboIndex: null, items: [item] }));
return groups.map((group, gi) => (
<div key={gi} className="space-y-2 rounded-lg border p-3">
{group.comboIndex !== null && (
<>
<div className="flex items-center justify-between border-b pb-2">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-muted-foreground">Kombinasi {group.comboIndex + 1}</span>
<span className="text-xs text-muted-foreground">·</span>
<Label className="text-xs whitespace-nowrap">Hasil <span className="text-destructive">*</span></Label>
<NumberInput
className="w-20"
value={combinations[group.comboIndex]?.material_result ?? 0}
onValueChange={(val) => updateCombinationResult(group.comboIndex!, val)}
/>
</div>
<Button type="button" variant="ghost" size="icon-sm" onClick={() => {
setComboDeleteIndex(group.comboIndex);
setComboDeleteConfirmOpen(true);
}}>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
</div>
<InputError message={errors[`combinations.${group.comboIndex}.material_result` as keyof typeof errors]} />
</>
)}
{group.items.map(({ m, index }) => {
const price = priceMap.get(m.raw_material_price_id);
const cartKey = `material-${index}`;
return (
<div key={cartKey} className="space-y-2">
<div className="flex items-start justify-between gap-2">
<div className="flex items-center gap-3">
{m.photo_url ? (
<button type="button" onClick={() => setPreviewKey(cartKey)} className="block h-8 w-8 shrink-0 overflow-hidden rounded-md border transition-opacity hover:opacity-80">
<img src={m.photo_url} alt={m.variant} className="h-full w-full object-cover" />
</button>
) : (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
)}
<div>
<p className="text-sm font-medium">{m.material_name}</p>
<p className="text-xs text-muted-foreground">{m.variant}{price ? ` · ${formatNumber(Number(price.stock))} ${m.unit}` : ''}</p>
</div>
</div>
{group.comboIndex === null && (
<Button type="button" variant="ghost" size="icon-sm" onClick={() => {
setCartDeleteIndex(index);
setCartDeleteConfirmOpen(true);
}}>
<Trash2 className="h-4 w-4 text-destructive" />
</Button>
)}
</div>
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground whitespace-nowrap">Pemakaian <span className="text-destructive">*</span></span>
<NumberInput
className="flex-1"
value={m.material_usage}
onValueChange={(val) => updateMaterial(index, 'material_usage', val)}
/>
</div>
<InputError message={errors[`materials.${index}.material_usage` as keyof typeof errors]} />
{group.comboIndex === null && (
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground whitespace-nowrap">Hasil <span className="text-destructive">*</span></span>
<NumberInput
className="flex-1"
value={m.material_result}
onValueChange={(val) => updateMaterial(index, 'material_result', val)}
/>
</div>
)}
{group.comboIndex === null && <InputError message={errors[`materials.${index}.material_result` as keyof typeof errors]} />}
</div>
);
})}
</div>
));
})()
)}
</div>
<SheetFooter>
<div className="flex items-center justify-between border-t pt-4">
<span className="text-sm">Total Pemakaian</span>
<span className="text-sm font-semibold">{formatQuantity(materials.reduce((sum, m) => sum + m.material_usage, 0))}</span>
</div>
</SheetFooter>
</SheetContent>
</Sheet>
<ImagePreviewModal
open={previewKey !== null}
onOpenChange={(open) => {
if (!open) {
setPreviewKey(null);
}
}}
src={previewKey && previewKey.startsWith('material-') ? (materials[Number(previewKey.replace('material-', ''))]?.photo_url ?? null) : null}
title={previewKey && previewKey.startsWith('material-') ? `${materials[Number(previewKey.replace('material-', ''))]?.material_name}${materials[Number(previewKey.replace('material-', ''))]?.variant}` : undefined}
sources={materials.filter((m) => m.photo_url).map((m) => m.photo_url!)}
/>
{comboDialogOpen && (
<div className="fixed inset-0 z-50 bg-black/50" onClick={() => setComboDialogOpen(false)} />
)}
<Dialog open={comboDialogOpen} onOpenChange={setComboDialogOpen} modal={false}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Tambah Kombinasi</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="grid gap-2">
<Label>
Nama Bahan Baku <span className="text-destructive">*</span>
</Label>
<Combobox
items={rawMaterials}
itemToStringLabel={(m) => m.name}
value={comboMaterial}
onValueChange={(value) => setComboMaterialName(value?.name ?? '')}
>
<ComboboxInput placeholder="Cari bahan baku..." className="w-full" />
<ComboboxContent>
<ComboboxEmpty>Tidak ada bahan baku ditemukan.</ComboboxEmpty>
<ComboboxList>
{(m) => (
<ComboboxItem key={m.id} value={m}>
{m.name} ({m.unit})
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>
{comboSelectedPriceIds.length > 0 && (
<div className="space-y-2">
<Label className="text-sm font-medium">Varian Dipilih</Label>
<div className="space-y-1">
{comboSelectedPriceIds.map((priceId) => {
let variantName = '';
let materialName = '';
let photoUrl: string | null = null;
for (const rm of rawMaterials) {
const p = rm.raw_material_prices.find((pp) => pp.id === priceId);
if (p) {
variantName = p.variant;
materialName = rm.name;
photoUrl = p.photo_url;
break;
}
}
return (
<div key={priceId} className="flex items-center justify-between gap-2 rounded-md border border-primary bg-primary/5 px-3 py-2">
<div className="flex min-w-0 items-center gap-3">
{photoUrl ? (
<img src={photoUrl} alt={variantName} className="h-8 w-8 shrink-0 rounded-md object-cover" />
) : (
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
)}
<div className="min-w-0">
<p className="text-sm font-medium truncate">{variantName}</p>
<p className="text-xs text-muted-foreground">{materialName}</p>
</div>
</div>
<Button type="button" variant="ghost" size="icon-sm" onClick={() => toggleComboPrice(priceId)}>
<Trash2 className="h-3 w-3 text-destructive" />
</Button>
</div>
);
})}
</div>
</div>
)}
{comboMaterial && comboMaterial.raw_material_prices.length > 0 && (
<div className="space-y-2">
<Label className="text-sm font-medium">Pilih Varian</Label>
<div className="space-y-2">
{comboMaterial.raw_material_prices.map((price) => {
const isSelected = comboSelectedPriceIds.includes(price.id);
return (
<div key={price.id} className={`flex items-center justify-between gap-3 rounded-lg border p-3 ${isSelected ? 'border-primary' : ''}`}>
<div className="flex min-w-0 items-center gap-3">
{price.photo_url ? (
<img src={price.photo_url} alt={price.variant} className="h-10 w-10 shrink-0 rounded-md object-cover" />
) : (
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-md bg-muted text-xs text-muted-foreground">N/A</div>
)}
<div className="min-w-0">
<p className="truncate font-medium">{price.variant}</p>
<p className="text-xs text-muted-foreground">Stok: {formatNumber(Number(price.stock))} {comboMaterial.unit}</p>
</div>
</div>
<Button type="button" variant={isSelected ? 'default' : 'outline'} size="sm" onClick={() => toggleComboPrice(price.id)}>
{isSelected ? <><Check className="h-4 w-4" /> Dipilih</> : 'Pilih'}
</Button>
</div>
);
})}
</div>
</div>
)}
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setComboDialogOpen(false)}>Batal</Button>
<Button type="button" onClick={confirmCombo} disabled={comboSelectedPriceIds.length < 2}>
Konfirmasi
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<ConfirmDialog open={deleteConfirmOpen} onOpenChange={(open) => {
if (!open) {
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
}
}} title="Hapus Bahan Baku" description="Apakah Anda yakin ingin menghapus bahan baku ini?" confirmLabel="Hapus" onConfirm={() => {
if (deleteMaterialIndex !== null) {
setMaterials((prev) => prev.filter((_, i) => i !== deleteMaterialIndex));
}
setDeleteConfirmOpen(false); setDeleteMaterialIndex(null);
}} />
<ConfirmDialog open={cartDeleteConfirmOpen} onOpenChange={(open) => {
if (!open) {
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
}
}} title="Hapus dari Keranjang" description="Apakah Anda yakin ingin menghapus item ini dari keranjang?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
if (cartDeleteIndex !== null) {
setMaterials((prev) => prev.filter((_, i) => i !== cartDeleteIndex));
}
setCartDeleteConfirmOpen(false); setCartDeleteIndex(null);
}} />
<ConfirmDialog open={comboDeleteConfirmOpen} onOpenChange={(open) => {
if (!open) {
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
}
}} title="Hapus Kombinasi" description="Apakah Anda yakin ingin menghapus kombinasi ini beserta semua bahannya?" confirmLabel="Hapus" variant="destructive" onConfirm={() => {
if (comboDeleteIndex !== null) {
setCombinations((prev) => prev.filter((_, i) => i !== comboDeleteIndex)); setMaterials((prev) => prev.filter((m) => m.combination_id !== comboDeleteIndex));
}
setComboDeleteConfirmOpen(false); setComboDeleteIndex(null);
}} />
</div>
</>
);
}