diff --git a/app/Http/Controllers/Admin/Master/Product/ProductVariantController.php b/app/Http/Controllers/Admin/Master/Product/ProductVariantController.php index 2b001be..ec2e314 100644 --- a/app/Http/Controllers/Admin/Master/Product/ProductVariantController.php +++ b/app/Http/Controllers/Admin/Master/Product/ProductVariantController.php @@ -29,7 +29,9 @@ public function update(ProductVariantRequest $request, Product $product, Product return $this->handleAction( fn () => $this->variantService->update($variant, $request->validated()), 'Varian berhasil diperbarui.', - 'admin.master.products.index' + 'admin.master.products.index', + 'admin.master.products.variants.edit', + ['product' => $product, 'variant' => $variant] ); } diff --git a/resources/js/hooks/use-product-draft.ts b/resources/js/hooks/use-product-draft.ts new file mode 100644 index 0000000..a98dc8e --- /dev/null +++ b/resources/js/hooks/use-product-draft.ts @@ -0,0 +1,68 @@ +import { useEffect, useRef, useCallback } from 'react'; +import { + saveProductDraft, + clearProductDraft, + type ProductDraftData, +} from '@/lib/product-draft'; + +type DraftType = 'create' | 'edit'; + +export function useProductDraftSave( + type: DraftType, + data: ProductDraftData, + userId?: number, + productId?: number, + delay = 500, +) { + const timeoutRef = useRef | null>(null); + const dataRef = useRef(data); + dataRef.current = data; + + const flush = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + timeoutRef.current = null; + } + const ok = saveProductDraft(type, dataRef.current, userId, productId); + return ok; + }, [type, userId, productId]); + + useEffect(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + timeoutRef.current = setTimeout(() => { + saveProductDraft(type, dataRef.current, userId, productId); + timeoutRef.current = null; + }, delay); + + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + }; + }, [data, type, userId, productId, delay]); + + useEffect(() => { + return () => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current); + } + saveProductDraft(type, dataRef.current, userId, productId); + }; + }, [type, userId, productId]); + + return { flush }; +} + +export function useProductDraftClear( + type: DraftType, + userId?: number, + productId?: number, +) { + useEffect(() => { + return () => { + clearProductDraft(type, userId, productId); + }; + }, [type, userId, productId]); +} diff --git a/resources/js/lib/product-draft.ts b/resources/js/lib/product-draft.ts new file mode 100644 index 0000000..751a545 --- /dev/null +++ b/resources/js/lib/product-draft.ts @@ -0,0 +1,73 @@ +const DRAFT_PREFIX = 'product-draft'; + +export type ProductDraftData = { + productName: string; + status: string; + description: string; + categoryIds: number[]; + useSamePrice: boolean; + sharedPrices: Array<{ type: string; price: number }>; + variants: Array<{ + id?: number | null; + name: string; + stock: number; + reject_stock: number; + retail_stock: number; + photo: string | null; + prices: Array<{ type: string; price: number }>; + }>; +}; + +function getKey( + type: 'create' | 'edit', + userId?: number, + productId?: number, +): string { + if (type === 'edit' && productId) { + return `${DRAFT_PREFIX}-edit-${userId ?? 'anon'}-${productId}`; + } + return `${DRAFT_PREFIX}-create-${userId ?? 'anon'}`; +} + +export function saveProductDraft( + type: 'create' | 'edit', + data: ProductDraftData, + userId?: number, + productId?: number, +): boolean { + try { + const key = getKey(type, userId, productId); + localStorage.setItem(key, JSON.stringify(data)); + return true; + } catch { + return false; + } +} + +export function loadProductDraft( + type: 'create' | 'edit', + userId?: number, + productId?: number, +): ProductDraftData | null { + try { + const key = getKey(type, userId, productId); + const raw = localStorage.getItem(key); + if (!raw) return null; + return JSON.parse(raw) as ProductDraftData; + } catch { + return null; + } +} + +export function clearProductDraft( + type: 'create' | 'edit', + userId?: number, + productId?: number, +): void { + try { + const key = getKey(type, userId, productId); + localStorage.removeItem(key); + } catch { + // ignore + } +} diff --git a/resources/js/pages/admin/master/product/create.tsx b/resources/js/pages/admin/master/product/create.tsx index eeadbf0..573b2b1 100644 --- a/resources/js/pages/admin/master/product/create.tsx +++ b/resources/js/pages/admin/master/product/create.tsx @@ -8,8 +8,11 @@ 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 { loadProductDraft, clearProductDraft } from '@/lib/product-draft'; +import { useProductDraftSave } from '@/hooks/use-product-draft'; +import { getTemporaryUrl } from '@/lib/upload'; import { index as productIndex, store } from '@/routes/admin/master/products'; -import { Form, Head } from '@inertiajs/react'; +import { Form, Head, router, usePage } from '@inertiajs/react'; import { ArrowLeft, Copy, @@ -18,7 +21,7 @@ import { Plus, Trash2, } from 'lucide-react'; -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; type Category = { id: number; @@ -51,30 +54,72 @@ type VariantState = { reject_stock: number; retail_stock: number; photo: string | null; + photoUrl: string | null; uploading: boolean; prices: Array<{ type: string; price: number }>; }; export default function ProductCreate({ categories }: Props) { - const [categoryIds, setCategoryIds] = useState([]); - const [useSamePrice, setUseSamePrice] = useState(true); - const [sharedPrices, setSharedPrices] = - useState>(createEmptyPrices()); - const [variants, setVariants] = useState([ - { - name: '', - stock: 0, - reject_stock: 0, - retail_stock: 0, - photo: null, - uploading: false, - prices: createEmptyPrices(), - }, - ]); + const { auth } = usePage().props as { auth: { user?: { id?: number } } }; + const userId = auth.user?.id; + + const draft = loadProductDraft('create', userId); + + const [productName, setProductName] = useState(draft?.productName ?? ''); + const [status, setStatus] = useState(draft?.status ?? 'active'); + const [description, setDescription] = useState(draft?.description ?? ''); + const [categoryIds, setCategoryIds] = useState( + draft?.categoryIds ?? [], + ); + const [useSamePrice, setUseSamePrice] = useState( + draft?.useSamePrice ?? true, + ); + const [sharedPrices, setSharedPrices] = useState< + Array<{ type: string; price: number }> + >(draft?.sharedPrices ?? createEmptyPrices()); + const [variants, setVariants] = useState(() => { + if (draft?.variants && draft.variants.length > 0) { + return draft.variants.map((v) => ({ + ...v, + photoUrl: v.photo ? getTemporaryUrl(v.photo) : null, + uploading: false, + })); + } + return [ + { + 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 draftData = { + productName, + status, + description, + categoryIds, + useSamePrice, + sharedPrices, + variants: variants.map(({ uploading, photoUrl, ...v }) => v), + }; + + useProductDraftSave('create', draftData, userId); + + useEffect(() => { + return router.on('success', () => { + clearProductDraft('create', userId); + }); + }, [userId]); + const addVariant = useCallback(() => { setVariants((prev) => [ ...prev, @@ -84,6 +129,7 @@ export default function ProductCreate({ categories }: Props) { reject_stock: 0, retail_stock: 0, photo: null, + photoUrl: null, uploading: false, prices: createEmptyPrices(), }, @@ -185,6 +231,9 @@ export default function ProductCreate({ categories }: Props) { function getPayload() { return { + name: productName, + status, + description, category_ids: categoryIds, use_same_price: useSamePrice, shared_prices: useSamePrice @@ -251,6 +300,12 @@ export default function ProductCreate({ categories }: Props) { + setProductName( + e.target.value, + ) + } placeholder="Masukkan nama produk" /> @@ -264,7 +319,8 @@ export default function ProductCreate({ categories }: Props) {
@@ -365,6 +421,12 @@ export default function ProductCreate({ categories }: Props) {