import InputError from '@/components/input-error'; import { RupiahInput } from '@/components/rupiah-input'; import { FileUpload } from '@/components/file-upload'; 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 { index as productIndex, update } from '@/routes/admin/master/products'; import { Form, Head } from '@inertiajs/react'; import { ArrowLeft, Copy, ClipboardPaste, Check, Plus, Trash2, } from 'lucide-react'; import { useCallback, useRef, useState } from 'react'; type Category = { id: number; name: string; }; type ProductVariant = { id: number; name: string; stock: number; reject_stock: number; retail_stock: number; photo_key: string | null; photo_url: string | null; 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; photo: string | null; photoUrl: string | null; uploading: boolean; prices: Array<{ type: string; price: number }>; }; export default function ProductEdit({ product, categories }: Props) { const initialVariants: 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, photo: v.photo_key, photoUrl: v.photo_url, uploading: false, prices: v.prices.length > 0 ? v.prices : createEmptyPrices(), }), ); const allSamePrice = initialVariants.length > 1 ? initialVariants.every((v) => arePricesEqual(v.prices, initialVariants[0].prices), ) : true; const [categoryIds, setCategoryIds] = useState( product.category_ids, ); const [useSamePrice, setUseSamePrice] = useState(allSamePrice); const [sharedPrices, setSharedPrices] = useState< Array<{ type: string; price: number }> >( initialVariants.length > 0 ? initialVariants[0].prices : createEmptyPrices(), ); const [variants, setVariants] = useState( initialVariants.length > 0 ? initialVariants : [ { id: null, name: '', stock: 0, reject_stock: 0, retail_stock: 0, photo: null, photoUrl: null, uploading: false, prices: createEmptyPrices(), }, ], ); const variantsRef = useRef(variants); variantsRef.current = variants; const addVariant = useCallback(() => { setVariants((prev) => [ ...prev, { id: null, name: '', stock: 0, reject_stock: 0, retail_stock: 0, photo: null, photoUrl: null, 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 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 { 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_key: v.photo, prices: useSamePrice ? [] : v.prices.map((p) => ({ type: p.type, price: Number(p.price), })), })), }; } return ( <>

Edit Produk

({ ...data, ...getPayload(), })} > {({ errors, processing }) => ( <>
Informasi Produk
{categories.map((category) => ( ))}