'use no memo'; import { Form, Head, Link, usePage } from '@inertiajs/react'; import { ArrowLeft, Check, Layers, Plus, Search, ShoppingCart, Trash2 } from 'lucide-react'; import { useCallback, 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 { useCuttingDraftSave } from '@/hooks/use-cutting-draft'; import { loadCuttingDraft } from '@/lib/cutting-draft'; import { formatNumber } from '@/lib/format'; import { getTemporaryUrl } from '@/lib/upload'; import { formatCurrency } from '@/lib/utils'; import { index as cuttingIndex, store } from '@/routes/admin/manage/cuttings'; import type { CuttingCreateData } from './columns'; type MaterialState = { 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 = { material_result: number; }; type Props = { rawMaterials: CuttingCreateData['rawMaterials']; }; export default function CuttingCreate({ rawMaterials }: Props) { const { auth, errors } = usePage().props as { auth: { user?: { id?: number } }; errors: Record }; const userId = auth.user?.id; const draft = loadCuttingDraft('create', userId); const [materials, setMaterials] = useState(() => { if (draft?.materials && draft.materials.length > 0) { return draft.materials.map((m) => ({ 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_index ?? null, variant: m.variant, material_name: m.material_name, unit: m.unit, photo_url: m.photo_url, photo_conversion_url: m.photo_conversion_url, })); } return []; }); const [combinations, setCombinations] = useState(() => { if (draft?.combinations && draft.combinations.length > 0) { return draft.combinations.map((c) => ({ material_result: c.material_result ?? 0, })); } return []; }); const [selectedMaterialName, setSelectedMaterialName] = useState(draft?.selectedMaterialName ?? ''); const [variantSearch, setVariantSearch] = useState(''); const [productName, setProductName] = useState(draft?.productName ?? ''); const [sample, setSample] = useState(draft?.sample ?? 0); const [originalOutsideSample, setOriginalOutsideSample] = useState(draft?.originalOutsideSample ?? 0); const cuttingResult = sample + originalOutsideSample; const [notes, setNotes] = useState(draft?.notes ?? ''); const [photos, setPhotos] = useState<{ key: string; url: string | null }[]>(() => { if (draft?.photo_keys && draft.photo_keys.length > 0) { return draft.photo_keys.map((key: string) => ({ key, url: getTemporaryUrl(key), })); } return []; }); const [uploading, setUploading] = useState(false); const submittingRef = useRef(false); const [cartOpen, setCartOpen] = useState(false); const [previewKey, setPreviewKey] = useState(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 [comboSelectedPriceIds, setComboSelectedPriceIds] = useState([]); const [comboResult, setComboResult] = useState(0); const comboMaterial = useMemo( () => rawMaterials.find((m) => m.name === comboMaterialName) ?? null, [rawMaterials, comboMaterialName], ); const draftData = useMemo( () => ({ productName, sample, originalOutsideSample, notes, materials: materials.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, variant: m.variant, material_name: m.material_name, unit: m.unit, photo_url: m.photo_url, photo_conversion_url: m.photo_conversion_url, })), combinations: combinations.map((c) => ({ material_result: c.material_result, })), selectedMaterialName, photo_keys: photos.map((p) => p.key), }), [productName, sample, originalOutsideSample, notes, materials, combinations, selectedMaterialName, photos], ); useCuttingDraftSave('create', draftData, userId); 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 groupedVariants = useMemo(() => { if (!variantSearch) return []; const search = variantSearch.toLowerCase(); return rawMaterials .map((rm) => ({ ...rm, raw_material_prices: rm.raw_material_prices.filter( (p) => p.variant.toLowerCase().includes(search), ), })) .filter((rm) => rm.raw_material_prices.length > 0); }, [rawMaterials, variantSearch]); const addVariant = useCallback( (rawMaterial: (typeof rawMaterials)[number], priceId: number) => { const price = rawMaterial.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: rawMaterial.name, unit: rawMaterial.unit, photo_url: price.photo_url, photo_conversion_url: price.photo_conversion_url, }, ]; }); }, [], ); 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, photo_conversion_url: foundPrice?.photo_conversion_url ?? null, }; }); setMaterials((prev) => [...prev, ...newMaterials]); setCombinations((prev) => [ ...prev, { material_result: comboResult, }, ]); setComboDialogOpen(false); setComboMaterialName(''); setComboSelectedPriceIds([]); setComboResult(0); }, [comboSelectedPriceIds, comboResult, rawMaterials, combinations.length]); const removeMaterial = useCallback((index: number) => { setDeleteMaterialIndex(index); setDeleteConfirmOpen(true); }, []); 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 ( <>

Tambah 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})

{rm.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 (
{price.photo_conversion_url ?? price.photo_url ? ( {price.variant} ) : (
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 && selectedMaterial.raw_material_prices.length > 0 && (
{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 (
{price.photo_conversion_url ?? price.photo_url ? ( {price.variant} ) : (
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)}
)}