'use no memo'; import { Form, Head, Link, usePage } from '@inertiajs/react'; import { ArrowLeft, Check, Layers, Plus, Search, ShoppingCart, Trash2 } from 'lucide-react'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { toast } from 'sonner'; import { ConfirmDialog } from '@/components/dialogs'; import { FileUploadMultiple } 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, RawMaterialVariant } from './columns'; type MaterialState = { id?: number; raw_material_price_id: number; material_usage: string; material_result: number; combination_id: number | null; variant: string; material_name: string; unit: string; photo_url: string | null; photo_conversion_url: string | null; }; type CombinationState = { id?: number; material_result: number; }; type Props = { cutting: CuttingForEdit; rawMaterials: CuttingCreateData['rawMaterials']; }; export default function CuttingEdit({ cutting, rawMaterials }: Props) { const { errors } = usePage().props as { errors: Record }; const [materials, setMaterials] = useState(() => { const comboIdToIndex = new Map(); cutting.combinations.forEach((c, i) => comboIdToIndex.set(c.id, i)); return cutting.materials.map((m) => { return { id: m.id, raw_material_price_id: m.raw_material_price_id, material_usage: String(m.material_usage), material_result: m.material_result ?? 0, combination_id: m.combination_id !== null ? (comboIdToIndex.get(m.combination_id) ?? null) : null, variant: m.variant ?? '', material_name: '', unit: '', photo_url: m.photo_url ?? null, photo_conversion_url: null, }; }); }); const [combinations, setCombinations] = useState( () => cutting.combinations.map((c) => ({ id: c.id, material_result: c.material_result ?? 0, })), ); const [selectedMaterialName, setSelectedMaterialName] = useState(''); const [variantSearch, setVariantSearch] = useState(''); const [fetchedVariants, setFetchedVariants] = useState>(new Map()); const [loadingVariants, setLoadingVariants] = useState(false); 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 [photos, setPhotos] = useState<{ key: string; url: string | null }[]>(() => { if (cutting.photo_keys && cutting.photo_keys.length > 0) { return cutting.photo_keys.map((key: string, index: number) => ({ key, url: cutting.photo_urls?.[index] ?? getTemporaryUrl(key), })); } return []; }); const [uploading, setUploading] = useState(false); const submittingRef = useRef(false); const [cartOpen, setCartOpen] = useState(false); const [previewKey, setPreviewKey] = useState(null); const [variantPreview, setVariantPreview] = useState<{ src: string; title?: string; description?: string } | null>(null); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [deleteMaterialIndex, setDeleteMaterialIndex] = useState(null); const [cartDeleteConfirmOpen, setCartDeleteConfirmOpen] = useState(false); const [cartDeleteIndex, setCartDeleteIndex] = useState(null); const [comboDeleteConfirmOpen, setComboDeleteConfirmOpen] = useState(false); const [comboDeleteIndex, setComboDeleteIndex] = useState(null); const [comboDialogOpen, setComboDialogOpen] = useState(false); const [comboMaterialName, setComboMaterialName] = useState(''); const [comboSelectedItems, setComboSelectedItems] = useState<{ materialName: string; materialId: number; priceId: number }[]>([]); const [comboResult, setComboResult] = useState(0); const comboMaterial = useMemo( () => rawMaterials.find((m) => m.name === comboMaterialName) ?? null, [rawMaterials, comboMaterialName], ); useEffect(() => { const materialIds = cutting.existing_material_ids ?? []; if (materialIds.length === 0) return; const toFetch = materialIds.filter((id) => !fetchedVariants.has(id)); if (toFetch.length === 0) return; setLoadingVariants(true); Promise.all( toFetch.map((id) => fetch(`/admin/master/raw-materials/${id}/variants`) .then((res) => res.json()) .then((data) => [id, data.variants] as const) ), ) .then((results) => { setFetchedVariants((prev) => { const next = new Map(prev); for (const [id, variants] of results) { next.set(id, variants); } return next; }); }) .catch(() => { toast.error('Gagal memuat varian bahan baku.'); }) .finally(() => setLoadingVariants(false)); }, [cutting.existing_material_ids]); const materialsRef = useRef(materials); materialsRef.current = materials; const priceMap = useMemo(() => { const map = new Map(); for (const [, variants] of fetchedVariants) { for (const p of variants) { map.set(p.id, p); } } return map; }, [fetchedVariants]); const selectedMaterial = useMemo( () => rawMaterials.find((m) => m.name === selectedMaterialName) ?? null, [rawMaterials, selectedMaterialName], ); const selectedMaterialVariants = useMemo( () => (selectedMaterial ? fetchedVariants.get(selectedMaterial.id) ?? [] : []), [selectedMaterial, fetchedVariants], ); useEffect(() => { if (!selectedMaterial) return; if (fetchedVariants.has(selectedMaterial.id)) return; setLoadingVariants(true); fetch(`/admin/master/raw-materials/${selectedMaterial.id}/variants`) .then((res) => res.json()) .then((data) => { setFetchedVariants((prev) => { const next = new Map(prev); next.set(selectedMaterial.id, data.variants); return next; }); }) .catch(() => { toast.error('Gagal memuat varian bahan baku.'); }) .finally(() => setLoadingVariants(false)); }, [selectedMaterial, fetchedVariants]); const comboMaterialVariants = useMemo( () => (comboMaterial ? fetchedVariants.get(comboMaterial.id) ?? [] : []), [comboMaterial, fetchedVariants], ); useEffect(() => { if (!comboMaterial) return; if (fetchedVariants.has(comboMaterial.id)) return; setLoadingVariants(true); fetch(`/admin/master/raw-materials/${comboMaterial.id}/variants`) .then((res) => res.json()) .then((data) => { setFetchedVariants((prev) => { const next = new Map(prev); next.set(comboMaterial.id, data.variants); return next; }); }) .catch(() => { toast.error('Gagal memuat varian bahan baku.'); }) .finally(() => setLoadingVariants(false)); }, [comboMaterial, fetchedVariants]); const groupedVariants = useMemo(() => { if (!variantSearch) return []; if (!selectedMaterial) return []; const search = variantSearch.toLowerCase(); return [{ ...selectedMaterial, raw_material_prices: selectedMaterialVariants.filter( (p) => p.variant.toLowerCase().includes(search), ), }]; }, [selectedMaterial, selectedMaterialVariants, variantSearch]); const addVariant = useCallback( (rawMaterial: { id: number; name: string; unit: string }, priceId: number) => { const variants = fetchedVariants.get(rawMaterial.id) ?? []; const price = variants.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: rawMaterial.name, unit: rawMaterial.unit, photo_url: price.photo_url, photo_conversion_url: price.photo_conversion_url, }, ]; }); }, [], ); const openComboDialog = useCallback((material: { id: number; name: string }, preSelectPriceId?: number) => { setComboMaterialName(material.name); if (preSelectPriceId) { setComboSelectedItems([{ materialName: material.name, materialId: material.id, priceId: preSelectPriceId }]); } else { setComboSelectedItems([]); } setComboResult(0); setComboDialogOpen(true); }, []); const toggleComboPrice = useCallback((priceId: number) => { const material = comboMaterial; if (!material) return; setComboSelectedItems((prev) => { const existing = prev.find((item) => item.priceId === priceId); if (existing) { return prev.filter((item) => item.priceId !== priceId); } return [...prev, { materialName: material.name, materialId: material.id, priceId }]; }); }, [comboMaterial]); const confirmCombo = useCallback(() => { if (comboSelectedItems.length < 2) { return; } const comboIndex = combinations.length; const newMaterials: MaterialState[] = comboSelectedItems.map((item) => { const variants = fetchedVariants.get(item.materialId) ?? []; const price = variants.find((p) => p.id === item.priceId); return { raw_material_price_id: item.priceId, material_usage: '0', material_result: 0, combination_id: comboIndex, variant: price?.variant ?? '', material_name: item.materialName, unit: rawMaterials.find((m) => m.id === item.materialId)?.unit ?? '', photo_url: price?.photo_url ?? null, photo_conversion_url: price?.photo_conversion_url ?? null, }; }); setMaterials((prev) => [...prev, ...newMaterials]); setCombinations((prev) => [ ...prev, { material_result: comboResult, }, ]); setComboDialogOpen(false); setComboMaterialName(''); setComboSelectedItems([]); setComboResult(0); }, [comboSelectedItems, comboResult, rawMaterials, fetchedVariants, combinations.length]); const updateMaterial = useCallback( (index: number, field: keyof MaterialState, value: unknown) => { setMaterials((prev) => { const updated = [...prev]; (updated[index] as Record)[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 * (parseFloat(String(m.material_usage)) || 0) : 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: parseFloat(String(m.material_usage)) || 0, material_result: m.material_result, combination_index: m.combination_id, })), combinations: combinations.map((c) => ({ material_result: c.material_result, })), photo_keys: photos.map((p) => p.key), }; } return ( <>

Edit Cutting

({ ...data, ...getPayload() })} onSubmit={() => { submittingRef.current = true; }} onError={() => { toast.error('Ada data yang belum sesuai, silakan periksa kembali input Anda.'); }}> {({ errors, processing }) => (
Pilih Bahan Baku
m.name} value={selectedMaterial} onValueChange={(value) => setSelectedMaterialName(value?.name ?? '')} > Tidak ada bahan baku ditemukan. {(m) => ( {m.name} ({m.unit}) )}
setVariantSearch(e.target.value)} className="pl-8" />
{variantSearch && groupedVariants.length > 0 && (
{groupedVariants.map((rm) => (

{rm.name} ({rm.unit})

{selectedMaterialVariants.filter((p) => p.variant.toLowerCase().includes(variantSearch.toLowerCase())).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 (
{price.photo_conversion_url ?? price.photo_url ? ( {price.variant} setVariantPreview({ src: price.photo_url, title: price.variant, description: rm.name })} /> ) : (
N/A
)}

{price.variant}

Stok: {formatNumber(Number(price.stock))} {rm.unit} · {formatCurrency(price.price)} {addedCount > 0 && ` · ×${addedCount}`}

); })}
))}
)} {variantSearch && groupedVariants.length === 0 && (

Tidak ada varian ditemukan.

)} {!variantSearch && selectedMaterial && (
{loadingVariants ? (
Memuat varian...
) : selectedMaterialVariants.length === 0 ? (
Tidak ada varian ditemukan.
) : (
{selectedMaterialVariants.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 (
{price.photo_conversion_url ?? price.photo_url ? ( {price.variant} setVariantPreview({ src: price.photo_url, title: price.variant, description: selectedMaterial?.name })} /> ) : (
N/A
)}

{price.variant}

Stok: {formatNumber(Number(price.stock))} {selectedMaterial.unit} · {formatCurrency(price.price)} {addedCount > 0 && ` · ×${addedCount}`}

); })}
)}
)}
Ringkasan
setProductName(e.target.value)} placeholder="Masukkan nama produk" />
Total {formatCurrency(totalCost)}
{cuttingResult > 0 && (
Biaya Per Produk {formatCurrency(costPerUnit)}
)}