From be38482c884153be5ab702291b8766218a5a861b Mon Sep 17 00:00:00 2001 From: Anang Yusman Date: Thu, 29 Jan 2026 02:19:14 +0800 Subject: [PATCH] fix:product,promo form --- components/dialog/promo-dialog.tsx | 2 +- components/dialog/promo-edit-dialog.tsx | 181 ++++++++++++++++ components/form/agent/detail-agent-form.tsx | 2 +- .../form/product/create-product-form.tsx | 97 ++++++--- .../form/product/detail-product-form.tsx | 166 ++++++++------- .../form/product/update-product-form.tsx | 194 ++++++++++++------ components/table/promotion-table.tsx | 29 ++- next.config.ts | 17 +- package-lock.json | 3 +- 9 files changed, 510 insertions(+), 181 deletions(-) create mode 100644 components/dialog/promo-edit-dialog.tsx diff --git a/components/dialog/promo-dialog.tsx b/components/dialog/promo-dialog.tsx index 9aa9527..a334d54 100644 --- a/components/dialog/promo-dialog.tsx +++ b/components/dialog/promo-dialog.tsx @@ -289,7 +289,7 @@ export default function PromoDetailDialog({
- Profile void; + onSuccess?: () => void; +}; + +export default function PromoEditDialog({ + promoId, + open, + onOpenChange, + onSuccess, +}: PromoEditDialogProps) { + const [loadingData, setLoadingData] = useState(false); + + // 🔹 FORM STATE + const [title, setTitle] = useState(""); + const [thumbnailUrl, setThumbnailUrl] = useState(null); + const [thumbnailFile, setThumbnailFile] = useState(null); + + const MySwal = withReactContent(Swal); + + /* ========================= + * Fetch promo detail + ========================= */ + useEffect(() => { + if (!promoId || !open) return; + + async function fetchPromo() { + try { + setLoadingData(true); + const res = await getPromotionById(promoId); + const data = res?.data?.data; + + setTitle(data?.title || ""); + setThumbnailUrl(data?.thumbnail_url || null); + } catch (e) { + console.error("FETCH PROMO ERROR:", e); + } finally { + setLoadingData(false); + } + } + + fetchPromo(); + }, [promoId, open]); + + if (!open) return null; + + /* ========================= + * Handlers + ========================= */ + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + setThumbnailFile(file); + setThumbnailUrl(URL.createObjectURL(file)); + }; + + const handleSubmit = async () => { + if (!promoId) return; + + const formData = new FormData(); + formData.append("title", title); + + if (thumbnailFile) { + formData.append("file", thumbnailFile); + } + + loading(); + const res = await updatePromotion(formData, promoId); + + if (res?.error) { + error(res.message || "Gagal update promo"); + return; + } + + success("Promo berhasil diperbarui"); + onOpenChange(false); + onSuccess?.(); + }; + + /* ========================= + * Render + ========================= */ + return ( +
onOpenChange(false)} + > +
e.stopPropagation()} + > + {/* HEADER */} +
+ +

Edit Promo

+
+ + {/* BODY */} +
+ {loadingData ? ( +

Memuat data...

+ ) : ( + <> + {/* TITLE */} +
+ + setTitle(e.target.value)} + className="w-full border rounded-lg px-3 py-2 focus:ring-2 focus:ring-[#0F6C75]" + placeholder="Judul promo" + /> +
+ + {/* THUMBNAIL */} +
+ + + {thumbnailUrl && ( +
+ Thumbnail +
+ )} + + +
+ + )} +
+ + {/* FOOTER */} +
+ + + +
+
+
+ ); +} diff --git a/components/form/agent/detail-agent-form.tsx b/components/form/agent/detail-agent-form.tsx index 5e99e32..b0b150b 100644 --- a/components/form/agent/detail-agent-form.tsx +++ b/components/form/agent/detail-agent-form.tsx @@ -278,7 +278,7 @@ export default function DetailAgentForm(props: { isDetail: boolean }) {
- Profile([{ id: 1, title: "", images: [], files: [] }]); - + const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false); const [uploadTarget, setUploadTarget] = useState<{ type: "spec"; index: number; } | null>(null); - + const fileInputId = "spec-upload-input"; + const fileInputRef = useRef(null); + const isUploadingRef = useRef(false); const [file, setFile] = useState(null); const router = useRouter(); @@ -80,30 +83,57 @@ export default function AddProductForm() { setSpecs(updated); }; - const handleFileSelected = (event: React.ChangeEvent) => { - const file = event.target.files?.[0]; - if (!file || !uploadTarget) return; + const handleFileSelected = (e: React.ChangeEvent) => { + const input = e.target; + const selectedFile = input.files?.[0]; - const reader = new FileReader(); - reader.onload = () => { - const fileUrl = reader.result as string; + if (!selectedFile || !uploadTarget) return; - if (uploadTarget.type === "spec") { - setSpecs((prev) => { - const updated = [...prev]; - updated[uploadTarget.index].images.push(fileUrl); - updated[uploadTarget.index].files.push(file); - return updated; - }); + // 🔒 CEGAH DOUBLE EVENT + if (isUploadingRef.current) return; + isUploadingRef.current = true; + + setSpecs((prev) => { + const updated = [...prev]; + const spec = updated[uploadTarget.index]; + + // max 5 gambar + if (spec.files.length >= 5) { + isUploadingRef.current = false; + return prev; } - }; - reader.readAsDataURL(file); + // cegah file sama + if ( + spec.files.some( + (f) => f.name === selectedFile.name && f.size === selectedFile.size, + ) + ) { + isUploadingRef.current = false; + return prev; + } + + const previewUrl = URL.createObjectURL(selectedFile); + + spec.images = [...spec.images, previewUrl]; + spec.files = [...spec.files, selectedFile]; + + return updated; + }); + + // reset input + input.value = ""; setIsUploadDialogOpen(false); + + // release lock + setTimeout(() => { + isUploadingRef.current = false; + }, 0); }; const onSubmit = async (data: z.infer) => { try { + loading(); const formData = new FormData(); formData.append("title", data.name); @@ -370,7 +400,10 @@ export default function AddProductForm() { {specs.map((spec, index) => ( -
+
@@ -456,36 +489,32 @@ export default function AddProductForm() { -
document.getElementById(fileInputId)?.click()} - > +

- Klik untuk upload atau drag & drop + Klik tombol di bawah untuk memilih file

PNG, JPG (max 2 MB)

- -
+ +
@@ -360,6 +390,7 @@ export default function UpdateProductForm() { setColors((prev) => { const updated = [...prev]; updated[index].colorSelected = colorCode; + updated[index].name = colorCode; // ✅ sinkron ke input return updated; }); }} @@ -415,9 +446,16 @@ export default function UpdateProductForm() { Judul Spesifikasi {index + 1} { + setSpecs((prev) => { + const updated = [...prev]; + updated[index].title = e.target.value; + return updated; + }); + }} />