diff --git a/resources/js/pages/admin/manage/purchase/edit.tsx b/resources/js/pages/admin/manage/purchase/edit.tsx index 6bc3eae..6cb6eb8 100644 --- a/resources/js/pages/admin/manage/purchase/edit.tsx +++ b/resources/js/pages/admin/manage/purchase/edit.tsx @@ -3,15 +3,12 @@ import { Form, Head, Link } from '@inertiajs/react'; import { ArrowLeft, - Check, - ClipboardPaste, - Copy, Minus, Plus, ShoppingCart, Trash2, } from 'lucide-react'; -import { useCallback, useMemo, useRef, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { ConfirmDialog } from '@/components/confirm-dialog'; import { FileUpload } from '@/components/file-upload'; import { ImagePreviewModal } from '@/components/image-preview-modal'; @@ -28,7 +25,6 @@ import { ComboboxItem, ComboboxList, } from '@/components/ui/combobox'; -import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Sheet, @@ -37,10 +33,8 @@ import { SheetHeader, SheetTitle, } from '@/components/ui/sheet'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Textarea } from '@/components/ui/textarea'; import { formatNumber } from '@/lib/format'; -import { getTemporaryUrl } from '@/lib/upload'; import { formatCurrency } from '@/lib/utils'; import { index as purchaseIndex, @@ -48,16 +42,6 @@ import { } from '@/routes/admin/manage/purchases'; import type { PurchaseCreateData, PurchaseForEdit } from './columns'; -type VariantState = { - id?: number; - variant: string; - price: number; - stock: number; - photo: string | null; - photoUrl: string | null; - uploading: boolean; -}; - type CartLine = { key: string; photoUrl: string | null; @@ -78,19 +62,6 @@ type Props = { export default function PurchaseEdit({ purchase, data }: Props) { const { suppliers, rawMaterials } = data; - const [name] = useState(purchase.name); - const [variants, setVariants] = useState(() => - purchase.variants.map((v) => ({ - id: v.id, - variant: v.variant, - price: v.price, - stock: v.stock, - photo: v.photo_key ?? null, - photoUrl: v.photo_url ?? null, - uploading: false, - })), - ); - const [supplierId, setSupplierId] = useState(String(purchase.supplier_id)); const selectedSupplier = suppliers.find((s) => String(s.id) === supplierId) ?? null; @@ -101,7 +72,6 @@ export default function PurchaseEdit({ purchase, data }: Props) { const [photoUrl, setPhotoUrl] = useState(purchase.photo_url); const [uploading, setUploading] = useState(false); - const [mode, setMode] = useState<'new' | 'existing'>(purchase.default_mode); const [selectedMaterialName, setSelectedMaterialName] = useState( purchase.existing_material_name ?? '', ); @@ -117,9 +87,6 @@ export default function PurchaseEdit({ purchase, data }: Props) { const [previewKey, setPreviewKey] = useState(null); const [cartRemoveKey, setCartRemoveKey] = useState(null); - const variantsRef = useRef(variants); - variantsRef.current = variants; - const priceMap = useMemo( () => new Map( @@ -143,11 +110,7 @@ export default function PurchaseEdit({ purchase, data }: Props) { [rawMaterials], ); - const newSubtotal = variants.reduce( - (sum, v) => sum + Number(v.price) * Number(v.stock), - 0, - ); - const existingSubtotal = Object.entries(quantities).reduce( + const subtotal = Object.entries(quantities).reduce( (sum, [priceId, quantity]) => { const price = priceMap.get(Number(priceId)); @@ -155,93 +118,8 @@ export default function PurchaseEdit({ purchase, data }: Props) { }, 0, ); - const subtotal = mode === 'existing' ? existingSubtotal : newSubtotal; const total = subtotal - discount + shippingCost; - const addVariant = useCallback(() => { - setVariants((prev) => [ - ...prev, - { - variant: '', - price: 0, - stock: 0, - photo: null, - photoUrl: null, - uploading: false, - }, - ]); - }, []); - - const removeVariant = useCallback((index: number) => { - setVariants((prev) => prev.filter((_, i) => i !== index)); - }, []); - - const updateVariant = useCallback( - (index: number, field: keyof VariantState, value: unknown) => { - setVariants((prev) => { - const updated = [...prev]; - (updated[index] as Record)[field] = value; - - return updated; - }); - }, - [], - ); - - const [copiedIndex, setCopiedIndex] = useState(null); - const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); - const [deleteVariantIndex, setDeleteVariantIndex] = useState( - null, - ); - - const confirmRemoveVariant = useCallback((index: number) => { - setDeleteVariantIndex(index); - setDeleteConfirmOpen(true); - }, []); - - const copyPrice = useCallback((variantIndex: number) => { - setVariants((prev) => { - const price = prev[variantIndex].price; - navigator.clipboard.writeText(String(price)); - setCopiedIndex(variantIndex); - setTimeout(() => setCopiedIndex(null), 1500); - - return prev; - }); - }, []); - - const pastePrice = useCallback((variantIndex: number) => { - navigator.clipboard.readText().then((text) => { - try { - const price = Number(text); - - if (!isNaN(price)) { - setVariants((prev) => { - const updated = [...prev]; - updated[variantIndex] = { - ...updated[variantIndex], - price, - }; - - return updated; - }); - } - } catch { - // invalid clipboard data - } - }); - }, []); - - const applyToAll = useCallback((variantIndex: number) => { - setVariants((prev) => { - const sourcePrice = prev[variantIndex].price; - - return prev.map((v, i) => - i === variantIndex ? v : { ...v, price: sourcePrice }, - ); - }); - }, []); - const selectedMaterial = useMemo( () => rawMaterials.find((m) => m.name === selectedMaterialName) ?? null, [rawMaterials, selectedMaterialName], @@ -261,62 +139,34 @@ export default function PurchaseEdit({ purchase, data }: Props) { })); }, []); - const adjustVariantStock = useCallback((index: number, amount: number) => { - setVariants((prev) => { - const updated = [...prev]; - updated[index] = { - ...updated[index], - stock: Math.max(0, Number(updated[index].stock) + amount), - }; - - return updated; - }); - }, []); - const cartItems: CartLine[] = (() => { - if (mode === 'existing') { - const lines: CartLine[] = []; + const lines: CartLine[] = []; - for (const [priceId, quantity] of Object.entries(quantities)) { - if (quantity <= 0) { - continue; - } - - const id = Number(priceId); - const price = priceMap.get(id); - const material = materialByPriceId.get(id); - - if (price && material) { - lines.push({ - key: `existing-${id}`, - photoUrl: price.photo_url, - title: `${material.name} — ${price.variant}`, - subtitle: `${formatCurrency(price.price)} / ${material.unit}`, - price: price.price, - quantity, - onAdjust: (delta) => incrementQuantity(id, delta), - onSet: (value) => updateQuantity(id, value), - onRemove: () => updateQuantity(id, 0), - }); - } + for (const [priceId, quantity] of Object.entries(quantities)) { + if (quantity <= 0) { + continue; } - return lines.sort((a, b) => a.title.localeCompare(b.title)); + const id = Number(priceId); + const price = priceMap.get(id); + const material = materialByPriceId.get(id); + + if (price && material) { + lines.push({ + key: `existing-${id}`, + photoUrl: price.photo_url, + title: `${material.name} — ${price.variant}`, + subtitle: `${formatCurrency(price.price)} / ${material.unit}`, + price: price.price, + quantity, + onAdjust: (delta) => incrementQuantity(id, delta), + onSet: (value) => updateQuantity(id, value), + onRemove: () => updateQuantity(id, 0), + }); + } } - return variants - .map((v, index): CartLine => ({ - key: `new-${index}`, - photoUrl: v.photoUrl, - title: `${name || 'Bahan Baku Baru'} — ${v.variant || `Varian ${index + 1}`}`, - subtitle: `${formatCurrency(v.price)} / ${purchase.unit}`, - price: v.price, - quantity: v.stock, - onAdjust: (delta) => adjustVariantStock(index, delta), - onSet: (value) => updateVariant(index, 'stock', value), - onRemove: () => removeVariant(index), - })) - .filter((line) => line.quantity > 0); + return lines.sort((a, b) => a.title.localeCompare(b.title)); })(); function formatQuantity(value: number): string { @@ -324,38 +174,20 @@ export default function PurchaseEdit({ purchase, data }: Props) { } function getPayload() { - const base = { - mode, + return { + mode: 'existing', supplier_id: supplierId ? Number(supplierId) : null, discount, shipping_cost: shippingCost, notes: notes || null, photo_key: photo, - }; - - if (mode === 'existing') { - return { - ...base, - existing_items: Object.entries(quantities) - .map(([priceId, quantity]) => ({ - raw_material_price_id: Number(priceId), - quantity: Number(quantity), - unit_price: priceMap.get(Number(priceId))?.price ?? 0, - })) - .filter((item) => item.quantity > 0), - }; - } - - return { - ...base, - name, - variants: variantsRef.current.map((v) => ({ - id: v.id, - variant: v.variant, - price: Number(v.price), - stock: Number(v.stock), - photo_key: v.photo, - })), + existing_items: Object.entries(quantities) + .map(([priceId, quantity]) => ({ + raw_material_price_id: Number(priceId), + quantity: Number(quantity), + unit_price: priceMap.get(Number(priceId))?.price ?? 0, + })) + .filter((item) => item.quantity > 0), }; } @@ -387,489 +219,191 @@ export default function PurchaseEdit({ purchase, data }: Props) { {({ errors, processing }) => (
- - setMode(value as 'new' | 'existing') - } - > - - - Baru - - - Lama - - + + + + Pilih Bahan Baku + + + +
+ + m.name} + value={selectedMaterial} + onValueChange={( + value, + ) => + setSelectedMaterialName( + value?.name ?? + '', + ) + } + > + + + + Tidak ada bahan + baku ditemukan. + + + {(m) => ( + + {m.name}{' '} + ( + {m.unit} + ) + + )} + + + + +
- - - - - Informasi Bahan Baku - - - -
- - - -
-
-
- - - - - Varian Bahan Baku - - - - {variants.map( - (variant, variantIndex) => ( + {selectedMaterial && ( +
+ {selectedMaterial.raw_material_prices.map( + (price) => (
+ 0 + ? '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' + } > -
-

- Varian{' '} - {variantIndex + - 1} -

-
- - - - {variantIndex > - 0 && ( - - )} +

-
-
- - - updateVariant( - variantIndex, - 'variant', - e - .target - .value, - ) - } - placeholder="Contoh: Ukuran L, Warna Merah" - /> - -
-
- - - updateVariant( - variantIndex, - 'price', - val, - ) - } - /> - -
-
- - - updateVariant( - variantIndex, - 'stock', - val, - ) - } - /> - -
-
-
- - + + { - updateVariant( - variantIndex, - 'photo', - key, - ); - updateVariant( - variantIndex, - 'photoUrl', - key - ? getTemporaryUrl( - key, - ) - : null, - ); - }} - folder="raw-material-variant" - existingUrl={ - variant.photoUrl - } - onUploadingChange={( - uploading, + onValueChange={( + val, ) => - updateVariant( - variantIndex, - 'uploading', - uploading, + updateQuantity( + price.id, + val, ) } /> - + incrementQuantity( + price.id, + 1, + ) } - /> + > + +
), )} - - - - - - - - - - - Pilih Bahan Baku - - - -
- - m.name} - value={selectedMaterial} - onValueChange={( - value, - ) => - setSelectedMaterialName( - value?.name ?? - '', - ) - } - > - - - - Tidak ada bahan - baku ditemukan. - - - {(m) => ( - - {m.name}{' '} - ( - {m.unit} - ) - - )} - - - - -
- - {selectedMaterial && ( -
- {selectedMaterial.raw_material_prices.map( - (price) => ( -
- 0 - ? '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' - } - > -
- {price.photo_url ? ( - { - ) : ( -
- N/A -
- )} -
-

- { - price.variant - } -

-

- Stok:{' '} - {formatQuantity( - Number( - price.stock, - ), - )}{' '} - { - selectedMaterial.unit - }{' '} - ·{' '} - {formatCurrency( - price.price, - )} -

-
-
-
- - - updateQuantity( - price.id, - val, - ) - } - /> - -
-
- ), - )} -
- )} -
-
-
- +
+ )} +
+
@@ -1020,15 +554,10 @@ export default function PurchaseEdit({ purchase, data }: Props) { disabled={ processing || uploading || - variants.some( - (v) => v.uploading, - ) || !supplierId || - (mode === 'existing' - ? Object.values( - quantities, - ).every((q) => q <= 0) - : !name) + Object.values( + quantities, + ).every((q) => q <= 0) } > {processing @@ -1184,27 +713,6 @@ export default function PurchaseEdit({ purchase, data }: Props) { title={cartItems.find((i) => i.key === previewKey)?.title} /> - { - if (!open) { - setDeleteConfirmOpen(false); - setDeleteVariantIndex(null); - } - }} - title="Hapus Varian" - description="Apakah Anda yakin ingin menghapus varian ini?" - confirmLabel="Hapus" - onConfirm={() => { - if (deleteVariantIndex !== null) { - removeVariant(deleteVariantIndex); - } - - setDeleteConfirmOpen(false); - setDeleteVariantIndex(null); - }} - /> - {