import { Form, Head, Link, usePage } from '@inertiajs/react'; import { ArrowLeft, Copy, ClipboardPaste, Check, Plus, Trash2, } from 'lucide-react'; import { useCallback, useRef, useState } from 'react'; import { toast } from 'sonner'; import { ConfirmDialog } from '@/components/dialogs'; import { FileUploadMultiple } from '@/components/inputs'; import { InputError } from '@/components/ui'; import { NumberInput } from '@/components/inputs'; import { RupiahInput } from '@/components/inputs'; import { Button } from '@/components/ui/button'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { Textarea } from '@/components/ui/textarea'; import { useProductDraftSave } from '@/hooks/use-product-draft'; import { clearProductDraft } from '@/lib/product-draft'; import { index as productIndex, update } from '@/routes/admin/master/products'; type Category = { id: number; name: string; }; type ProductVariant = { id: number; name: string; stock: number; reject_stock: number; retail_stock: number; photo_keys: string[]; photo_urls: string[]; prices: Array<{ type: string; price: number }>; }; type Props = { product: { id: number; name: string; description: string | null; status: 'active' | 'inactive' | 'draft'; category_ids: number[]; product_variants: ProductVariant[]; }; categories: Category[]; }; const PRICE_TYPES = [ { key: 'distributor', label: 'Distributor' }, { key: 'agent', label: 'Agen' }, { key: 'sub_agent', label: 'Sub Agen' }, { key: 'wholesale', label: 'Grosir' }, { key: 'retail', label: 'Ecer' }, { key: 'tiktok', label: 'TikTok' }, { key: 'shopee', label: 'Shopee' }, { key: 'capital', label: 'Modal' }, { key: 'reject', label: 'Reject' }, ]; function createEmptyPrices(): Array<{ type: string; price: number }> { return PRICE_TYPES.map((pt) => ({ type: pt.key, price: 0 })); } function arePricesEqual( a: Array<{ type: string; price: number }>, b: Array<{ type: string; price: number }>, ): boolean { if (a.length !== b.length) { return false; } return a.every( (pa, i) => pa.type === b[i]?.type && pa.price === b[i]?.price, ); } type VariantState = { id: number | null; name: string; stock: number; reject_stock: number; retail_stock: number; photos: Array<{ key: string; url: string | null }>; uploading: boolean; prices: Array<{ type: string; price: number }>; }; export default function ProductEdit({ product, categories }: Props) { const { auth } = usePage().props as { auth: { user?: { id?: number } } }; const userId = auth.user?.id; clearProductDraft('edit', userId, product.id); const [productName, setProductName] = useState(product.name); const [status, setStatus] = useState(product.status); const [description, setDescription] = useState( product.description ?? '', ); const serverVariants: VariantState[] = product.product_variants.map( (v) => ({ id: v.id, name: v.name, stock: v.stock, reject_stock: v.reject_stock, retail_stock: v.retail_stock, photos: v.photo_keys.map((key, i) => ({ key, url: v.photo_urls[i] ?? null, })), uploading: false, prices: v.prices.length > 0 ? v.prices : createEmptyPrices(), }), ); const [categoryIds, setCategoryIds] = useState( product.category_ids, ); const [useSamePrice, setUseSamePrice] = useState( serverVariants.length > 1 ? serverVariants.every((v) => arePricesEqual(v.prices, serverVariants[0].prices), ) : true, ); const [sharedPrices, setSharedPrices] = useState< Array<{ type: string; price: number }> >( serverVariants.length > 0 ? serverVariants[0].prices : createEmptyPrices(), ); const [variants, setVariants] = useState(() => { return serverVariants.length > 0 ? serverVariants : [ { id: null, name: '', stock: 0, reject_stock: 0, retail_stock: 0, photos: [], uploading: false, prices: createEmptyPrices(), }, ]; }); const variantsRef = useRef(variants); variantsRef.current = variants; const draftData = { productName, status, description, categoryIds, useSamePrice, sharedPrices, variants: variants.map((v) => ({ id: v.id, name: v.name, stock: v.stock, reject_stock: v.reject_stock, retail_stock: v.retail_stock, photos: v.photos.map((p) => ({ key: p.key })), prices: v.prices, })), }; useProductDraftSave('edit', draftData, userId, product.id); const addVariant = useCallback(() => { setVariants((prev) => [ ...prev, { id: null, name: '', stock: 0, reject_stock: 0, retail_stock: 0, photos: [], uploading: false, prices: createEmptyPrices(), }, ]); }, []); 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 updateVariantPrice = useCallback( (variantIndex: number, priceIndex: number, value: number) => { setVariants((prev) => { const updated = [...prev]; updated[variantIndex] = { ...updated[variantIndex], prices: updated[variantIndex].prices.map((p, i) => i === priceIndex ? { ...p, price: value } : p, ), }; return updated; }); }, [], ); const updateSharedPrice = useCallback( (priceIndex: number, value: number) => { setSharedPrices((prev) => { const updated = [...prev]; updated[priceIndex] = { ...updated[priceIndex], price: 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 copyPrices = useCallback((variantIndex: number) => { setVariants((prev) => { const prices = prev[variantIndex].prices; navigator.clipboard.writeText(JSON.stringify(prices)); setCopiedIndex(variantIndex); setTimeout(() => setCopiedIndex(null), 1500); return prev; }); }, []); const pastePrices = useCallback((variantIndex: number) => { navigator.clipboard.readText().then((text) => { try { const prices = JSON.parse(text) as Array<{ type: string; price: number; }>; setVariants((prev) => { const updated = [...prev]; updated[variantIndex] = { ...updated[variantIndex], prices, }; return updated; }); } catch { // invalid clipboard data } }); }, []); const applyToAll = useCallback((variantIndex: number) => { setVariants((prev) => { const sourcePrices = prev[variantIndex].prices; return prev.map((v, i) => i === variantIndex ? v : { ...v, prices: [...sourcePrices] }, ); }); }, []); function getPayload() { return { name: productName, status, description, category_ids: categoryIds, use_same_price: useSamePrice, shared_prices: useSamePrice ? sharedPrices.map((p) => ({ type: p.type, price: Number(p.price), })) : [], variants: variantsRef.current.map((v) => ({ id: v.id, name: v.name, stock: Number(v.stock), reject_stock: Number(v.reject_stock), retail_stock: Number(v.retail_stock), photo_keys: v.photos.map((p) => p.key), prices: useSamePrice ? [] : v.prices.map((p) => ({ type: p.type, price: Number(p.price), })), })), }; } return ( <>

Edit Produk

({ ...data, ...getPayload(), })} onError={() => { toast.error('Ada data yang belum sesuai, silakan periksa kembali input Anda.'); }} > {({ errors, processing }) => ( <>
Informasi Produk
setProductName( e.target.value, ) } placeholder="Masukkan nama produk" />
{categories.map((category) => ( ))}