From 56b2d981afa541703e74fdb838ec18e6d899b624 Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Wed, 28 Jan 2026 08:39:32 +0700 Subject: [PATCH] feat: fixing create, detail, update for products --- .../admin/product/update/[id]/page.tsx | 10 + .../form/product/create-product-form.tsx | 179 +++++++++++--- .../form/product/detail-product-form.tsx | 19 +- .../form/product/update-product-form.tsx | 234 ++++++++++++++++-- components/table/product-table.tsx | 2 +- service/product.ts | 5 +- 6 files changed, 375 insertions(+), 74 deletions(-) create mode 100644 app/(admin)/admin/product/update/[id]/page.tsx diff --git a/app/(admin)/admin/product/update/[id]/page.tsx b/app/(admin)/admin/product/update/[id]/page.tsx new file mode 100644 index 0000000..f40b42a --- /dev/null +++ b/app/(admin)/admin/product/update/[id]/page.tsx @@ -0,0 +1,10 @@ +import UpdateProductForm from "@/components/form/product/update-product-form"; + +export default function UpdateProductPage() { + return ( +
+ +
+ ); +} + diff --git a/components/form/product/create-product-form.tsx b/components/form/product/create-product-form.tsx index b368a73..a81a624 100644 --- a/components/form/product/create-product-form.tsx +++ b/components/form/product/create-product-form.tsx @@ -8,6 +8,14 @@ import * as z from "zod"; import { Upload, Plus, UploadCloud } from "lucide-react"; import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"; import { Label } from "@radix-ui/react-dropdown-menu"; +import Image from "next/image"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Button } from "@/components/ui/button"; @@ -30,8 +38,16 @@ export default function AddProductForm() { const [selectedColor, setSelectedColor] = useState(null); const [specs, setSpecs] = useState< - { id: number; title: string; file: File | null }[] - >([{ id: 1, title: "", file: null }]); + { id: number; title: string; images: string[]; files: File[] }[] + >([{ 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 [file, setFile] = useState(null); const router = useRouter(); @@ -43,9 +59,12 @@ export default function AddProductForm() { if (selected) setFile(selected); }; - // const handleAddSpec = () => { - // setSpecs((prev) => [...prev, { id: prev.length + 1 }]); - // }; + const handleAddSpec = () => { + setSpecs((prev) => [ + ...prev, + { id: prev.length + 1, title: "", images: [], files: [] }, + ]); + }; const { register, handleSubmit, @@ -61,14 +80,26 @@ export default function AddProductForm() { setSpecs(updated); }; - const handleSpecFileChange = ( - index: number, - e: React.ChangeEvent, - ) => { - const file = e.target.files?.[0] || null; - const updated = [...specs]; - updated[index].file = file; - setSpecs(updated); + const handleFileSelected = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (!file || !uploadTarget) return; + + const reader = new FileReader(); + reader.onload = () => { + const fileUrl = reader.result as string; + + if (uploadTarget.type === "spec") { + setSpecs((prev) => { + const updated = [...prev]; + updated[uploadTarget.index].images.push(fileUrl); + updated[uploadTarget.index].files.push(file); + return updated; + }); + } + }; + + reader.readAsDataURL(file); + setIsUploadDialogOpen(false); }; const onSubmit = async (data: z.infer) => { @@ -99,17 +130,18 @@ export default function AddProductForm() { } }); - // 🔥 specifications JSON + // 🔥 specifications JSON (include image count for backend processing) const specificationsPayload = specs.map((s) => ({ title: s.title, + imageCount: s.files.length, })); formData.append("specifications", JSON.stringify(specificationsPayload)); - // 🔥 imagespecification_url (files) + // 🔥 specification images (multiple files per spec) specs.forEach((s) => { - if (s.file) { - formData.append("imagespecification_url", s.file); - } + s.files.forEach((file) => { + formData.append("specification_images", file); + }); }); await createProduct(formData); @@ -338,7 +370,7 @@ export default function AddProductForm() { {specs.map((spec, index) => ( -
+
@@ -353,39 +385,58 @@ export default function AddProductForm() { Foto Spesifikasi {index + 1} -
))} - {/* */} +
+ + + + + ); } diff --git a/components/form/product/detail-product-form.tsx b/components/form/product/detail-product-form.tsx index 997b681..b4bffaf 100644 --- a/components/form/product/detail-product-form.tsx +++ b/components/form/product/detail-product-form.tsx @@ -299,11 +299,22 @@ export default function DetailProductForm(props: { isDetail: boolean }) { // colors if (data.colors?.length) { setColors( - data.colors.map((color: string, index: number) => ({ + data.colors.map((color: any, index: number) => ({ id: index + 1, - name: color, - preview: data.thumbnail_url, - colorSelected: color, + name: color.name || "", + preview: color.image_url || data.thumbnail_url || "", + colorSelected: color.name || null, + })), + ); + } + + // specifications + if (data.specifications?.length) { + setSpecs( + data.specifications.map((spec: any, index: number) => ({ + id: index + 1, + title: spec.title || "", + images: spec.image_urls || [], })), ); } diff --git a/components/form/product/update-product-form.tsx b/components/form/product/update-product-form.tsx index d9e10dd..50a7d09 100644 --- a/components/form/product/update-product-form.tsx +++ b/components/form/product/update-product-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Upload, Plus, Settings } from "lucide-react"; import Image from "next/image"; import { Input } from "@/components/ui/input"; @@ -14,15 +14,19 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; +import { useParams } from "next/navigation"; +import { getProductDataById, updateProduct } from "@/service/product"; +import { useRouter } from "next/navigation"; +import { success, error, close, loading } from "@/config/swal"; export default function UpdateProductForm() { - const [specs, setSpecs] = useState([ - { - id: 1, - title: "Jaecoo 7 SHS Teknologi dan Exterior", - images: ["/spec1.jpg", "/spec2.jpg", "/spec3.jpg", "/spec4.jpg"], - }, - ]); + const params = useParams(); + const id = params?.id; + const router = useRouter(); + const [specs, setSpecs] = useState< + { id: number; title: string; images: string[]; files: File[] }[] + >([]); + const [specFiles, setSpecFiles] = useState>(new Map()); type ColorType = { id: number; @@ -31,20 +35,12 @@ export default function UpdateProductForm() { colorSelected: string | null; }; - const [colors, setColors] = useState([ - { - id: 1, - name: "", - preview: "/car-1.png", - colorSelected: null, - }, - { - id: 2, - name: "", - preview: "/car-2.png", - colorSelected: null, - }, - ]); + const [colors, setColors] = useState([]); + const [colorFiles, setColorFiles] = useState>(new Map()); + const [thumbnail, setThumbnail] = useState(""); + const [title, setTitle] = useState(""); + const [variant, setVariant] = useState(""); + const [price, setPrice] = useState(""); const palette = [ "#1E4E52", @@ -69,6 +65,7 @@ export default function UpdateProductForm() { id: prev.length + 1, title: "", images: [], + files: [], }, ]); }; @@ -107,6 +104,15 @@ export default function UpdateProductForm() { updated[uploadTarget.index].images.push(fileUrl); return updated; }); + // Store the file for upload + if (file) { + setSpecFiles((prev) => { + const newMap = new Map(prev); + const existingFiles = newMap.get(uploadTarget.index) || []; + newMap.set(uploadTarget.index, [...existingFiles, file]); + return newMap; + }); + } } if (uploadTarget.type === "color") { @@ -115,6 +121,14 @@ export default function UpdateProductForm() { updated[uploadTarget.index].preview = fileUrl; return updated; }); + // Store the file for upload + if (file) { + setColorFiles((prev) => { + const newMap = new Map(prev); + newMap.set(uploadTarget.index, file); + return newMap; + }); + } } }; @@ -122,6 +136,127 @@ export default function UpdateProductForm() { setIsUploadDialogOpen(false); }; + const formatRupiah = (value: string) => + "Rp " + Number(value).toLocaleString("id-ID"); + + useEffect(() => { + if (id) { + initState(); + } + }, [id]); + + async function initState() { + if (!id) return; + + try { + loading(); + const res = await getProductDataById(id); + const data = res?.data?.data; + + if (!data) { + error("Produk tidak ditemukan"); + return; + } + close(); + + // Set form values + setTitle(data.title || ""); + setVariant(data.variant || ""); + setPrice(formatRupiah(data.price || "0")); + setThumbnail(data.thumbnail_url || ""); + + // Set colors + if (data.colors?.length) { + setColors( + data.colors.map((color: any, index: number) => ({ + id: index + 1, + name: color.name || "", + preview: color.image_url || data.thumbnail_url || "", + colorSelected: color.name || null, + })), + ); + } else { + setColors([]); + } + + // Set specifications + if (data.specifications?.length) { + setSpecs( + data.specifications.map((spec: any, index: number) => ({ + id: index + 1, + title: spec.title || "", + images: spec.image_urls || [], + files: [], + })), + ); + } else { + setSpecs([]); + } + } catch (err) { + error("Gagal memuat data produk"); + console.error(err); + } + } + + const handleSubmit = async () => { + if (!id) { + error("ID produk tidak ditemukan"); + return; + } + + try { + loading(); + const formData = new FormData(); + + formData.append("title", title); + if (variant) formData.append("variant", variant); + if (price) { + const priceValue = price.replace(/\D/g, ""); + formData.append("price", priceValue); + } + + // Colors JSON + const colorsPayload = colors.map((c) => ({ + name: c.name, + })); + formData.append("colors", JSON.stringify(colorsPayload)); + + // Color images (only new files if uploaded) + // Append files in order of color indices + colors.forEach((_, index) => { + const file = colorFiles.get(index); + if (file) { + formData.append("color_images", file); + } + }); + + // Specifications JSON (include image count for new files only) + const specificationsPayload = specs.map((s, index) => { + const newFiles = specFiles.get(index) || []; + return { + title: s.title, + imageCount: newFiles.length, // Only count new files being uploaded + }; + }); + formData.append("specifications", JSON.stringify(specificationsPayload)); + + // Specification images (only new files if uploaded) + specs.forEach((_, index) => { + const files = specFiles.get(index) || []; + files.forEach((file) => { + formData.append("specification_images", file); + }); + }); + + await updateProduct(formData, id); + success("Produk berhasil diperbarui"); + router.push("/admin/product"); + } catch (err) { + error("Gagal memperbarui produk"); + console.error(err); + } + }; + return ( <> @@ -135,17 +270,51 @@ export default function UpdateProductForm() {
- + setTitle(e.target.value)} + placeholder="Masukkan nama produk" + />
- + setVariant(e.target.value)} + placeholder="Masukkan varian" + />
- + { + const rawValue = e.target.value.replace(/\D/g, ""); + setPrice(formatRupiah(rawValue)); + }} + placeholder="Masukkan harga" + /> +
+
+ +
+ +
+ {thumbnail ? ( + Thumbnail + ) : ( +
+ No Image +
+ )}
@@ -161,7 +330,15 @@ export default function UpdateProductForm() { { + setColors((prev) => { + const updated = [...prev]; + updated[index].name = e.target.value; + updated[index].colorSelected = e.target.value; + return updated; + }); + }} />
@@ -283,7 +460,10 @@ export default function UpdateProductForm() {
- diff --git a/components/table/product-table.tsx b/components/table/product-table.tsx index c451700..1f2c2bb 100644 --- a/components/table/product-table.tsx +++ b/components/table/product-table.tsx @@ -512,7 +512,7 @@ export default function ProductTable() { {userRoleId !== "1" && ( - +