diff --git a/next.config.ts b/next.config.ts index 2cdcdcd..efebc0b 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,6 +1,15 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { + async redirects() { + return [ + { + source: "/onboarding/:path*", + destination: "/dashboard", + permanent: false, + }, + ]; + }, images: { unoptimized: true, remotePatterns: [ diff --git a/src/app/(auth)/login/page.tsx b/src/app/(auth)/login/page.tsx index ee88bdc..b3d2489 100644 --- a/src/app/(auth)/login/page.tsx +++ b/src/app/(auth)/login/page.tsx @@ -59,27 +59,6 @@ export default function LoginPage() { } if (data.role === "seller") { - // Check seller profile completeness - try { - const profileRes = await fetch("/api/seller/profile", { - headers: { "x-auth-token": data.token }, - }); - const profileData = await profileRes.json(); - const profile = profileData?.data || profileData; - - const isIncomplete = - !profile?.storeName && - !profile?.biography && - !profile?.sellerImageUrl; - - if (isIncomplete || data.onboardingRequired) { - router.push("/onboarding/business"); - return; - } - } catch { - // If profile check fails, still proceed to dashboard - } - router.push("/dashboard"); return; } diff --git a/src/app/(auth)/register/verify/page.tsx b/src/app/(auth)/register/verify/page.tsx index 950fe53..cc72041 100644 --- a/src/app/(auth)/register/verify/page.tsx +++ b/src/app/(auth)/register/verify/page.tsx @@ -125,7 +125,7 @@ function VerifyContent() { sessionStorage.removeItem("otpVerified"); sessionStorage.removeItem("otpVerifiedEmail"); setSuccess(v.successSeller); - setTimeout(() => { router.push("/onboarding/business"); }, 1000); + setTimeout(() => { router.push("/dashboard"); }, 1000); return; } diff --git a/src/app/(dashboard)/products/[productId]/edit/page.tsx b/src/app/(dashboard)/products/[productId]/edit/page.tsx index 128446b..4d030e6 100644 --- a/src/app/(dashboard)/products/[productId]/edit/page.tsx +++ b/src/app/(dashboard)/products/[productId]/edit/page.tsx @@ -90,6 +90,7 @@ interface EditState { isNew: boolean; isEligibleToExport: boolean; imageId: string; + productFiles: Array<{ id: string; name: string }>; productImages: string[]; keywords: string[]; features: string[]; @@ -153,6 +154,14 @@ interface ApiProductImage { imageId?: string | number | null; } +interface ApiProductFile { + id?: string | number | null; + fileId?: string | number | null; + name?: string | number | null; + fileName?: string | number | null; + file?: string | number | null; +} + interface ApiParamItem { paramName?: string; paramValue?: string; @@ -178,6 +187,7 @@ interface ApiProduct { isNew?: boolean | null; isEligibleToExport?: boolean | null; imageId?: string | number | null; + productFiles?: ApiProductFile[] | Array; productImages?: ApiProductImage[]; productKeyWords?: string[]; productFeatures?: string[]; @@ -215,6 +225,50 @@ function toStr(v: string | number | null | undefined): string { return String(v); } +function toProductFiles(items: ApiProduct["productFiles"]): Array<{ id: string; name: string }> { + if (!Array.isArray(items)) return []; + + return items + .map((item) => { + if (typeof item === "string" || typeof item === "number") { + const id = toStr(item); + return id ? { id, name: id } : null; + } + + const id = toStr(item?.fileId ?? item?.id ?? item?.file); + if (!id) return null; + + return { + id, + name: toStr(item?.name ?? item?.fileName ?? item?.file) || id, + }; + }) + .filter((item): item is { id: string; name: string } => Boolean(item)); +} + +async function uploadFile(file: File) { + const fd = new FormData(); + fd.append("file", file); + + const res = await fetch("/api/upload", { + method: "POST", + headers: { "x-auth-token": getToken() }, + body: fd, + }); + const data = await res.json(); + + if (!res.ok) { + throw new Error(data?.responseDesc || data?.error || "Upload gagal"); + } + + const fileId = data?.data?.fileId || data?.data?.id || data?.fileId || data?.id || ""; + if (!fileId) { + throw new Error("File id tidak ditemukan"); + } + + return { fileId: String(fileId), fileName: file.name }; +} + function newMeasurement(): EditMeasurement { return { _key: `meas-${Date.now()}-${Math.random().toString(36).slice(2)}`, @@ -354,6 +408,7 @@ function apiToEditState(data: ApiProduct): EditState { isNew: data?.isNew !== false, isEligibleToExport: !!data?.isEligibleToExport, imageId: toStr(data?.imageId), + productFiles: toProductFiles(data?.productFiles), productImages: Array.isArray(data?.productImages) ? data.productImages .sort( @@ -420,18 +475,8 @@ function ImageSlotUpload({ setUploading(true); setError(""); try { - const fd = new FormData(); - fd.append("file", file); - const res = await fetch("/api/upload", { - method: "POST", - headers: { "x-auth-token": getToken() }, - body: fd, - }); - const data = await res.json(); - if (!res.ok) throw new Error(data?.responseDesc || data?.error || "Upload gagal"); - const id = data?.data?.id || data?.data?.fileId || data?.fileId || ""; - if (!id) throw new Error("File id tidak ditemukan"); - onUploaded(id); + const { fileId } = await uploadFile(file); + onUploaded(fileId); } catch (err) { setError(err instanceof Error ? err.message : "Upload gagal"); } finally { @@ -483,18 +528,8 @@ function ModelImageUpload({ value, onUploaded }: { value: string; onUploaded: (i setUploading(true); setError(""); try { - const fd = new FormData(); - fd.append("file", file); - const res = await fetch("/api/upload", { - method: "POST", - headers: { "x-auth-token": getToken() }, - body: fd, - }); - const data = await res.json(); - if (!res.ok) throw new Error(data?.responseDesc || data?.error || "Upload gagal"); - const id = data?.data?.id || data?.data?.fileId || data?.fileId || ""; - if (!id) throw new Error("File id tidak ditemukan"); - onUploaded(id); + const { fileId } = await uploadFile(file); + onUploaded(fileId); } catch (err) { setError(err instanceof Error ? err.message : "Upload gagal"); setPreviewUrl(""); @@ -1009,9 +1044,15 @@ function EditProductPageInner() { const [loadingSubcategories, setLoadingSubcategories] = useState(false); const [warehouses, setWarehouses] = useState([]); const [keywordInput, setKeywordInput] = useState(""); - const [debugPayload, setDebugPayload] = useState(""); const [publishing, setPublishing] = useState(false); const [publishSuccess, setPublishSuccess] = useState(false); + const [msdsName, setMsdsName] = useState(""); + const [uploadingMsds, setUploadingMsds] = useState(false); + const [uploadingDoc, setUploadingDoc] = useState(false); + const [msdsError, setMsdsError] = useState(""); + const [docError, setDocError] = useState(""); + const msdsInputRef = useRef(null); + const docInputRef = useRef(null); // Load product, categories (with subcategory resolution), and warehouses useEffect(() => { @@ -1078,7 +1119,7 @@ function EditProductPageInner() { } loadAll(); - }, [params.productId]); + }, [isDraftParam, params.productId]); // Load subcategories when category changes (for draft editing) useEffect(() => { @@ -1113,17 +1154,62 @@ function EditProductPageInner() { setKeywordInput(""); } + function removeDoc(id: string) { + if (!form) return; + update({ productFiles: form.productFiles.filter((file) => file.id !== id) }); + } + + async function handleMsdsUpload(file: File) { + setUploadingMsds(true); + setMsdsError(""); + try { + const { fileId, fileName } = await uploadFile(file); + update({ + complianceInformation: { ...form!.complianceInformation, fileId }, + }); + setMsdsName(fileName); + } catch (err) { + setMsdsError(err instanceof Error ? err.message : "Upload gagal"); + } finally { + setUploadingMsds(false); + } + } + + async function handleSupportingDocumentsUpload(files: File[]) { + if (!form || files.length === 0) return; + + setUploadingDoc(true); + setDocError(""); + try { + const uploaded: Array<{ id: string; name: string }> = []; + for (const file of files) { + const { fileId, fileName } = await uploadFile(file); + uploaded.push({ id: fileId, name: fileName }); + } + + update({ + productFiles: [...form.productFiles, ...uploaded], + }); + } catch (err) { + setDocError(err instanceof Error ? err.message : "Upload gagal"); + } finally { + setUploadingDoc(false); + } + } + function buildPayload(state?: "DRAFT" | "PUBLISHED") { if (!form) return null; + const resolvedState = state ?? "DRAFT"; const base = { subCategory: form.subCategoryId ? { id: form.subCategoryId } : undefined, name: form.name, description: form.description, isPreOrder: form.isPreOrder, - preOrderDay: form.isPreOrder ? toNum(form.preOrderDay) : 0, + preOrderDay: form.isPreOrder ? toNum(form.preOrderDay) : null, isNew: form.isNew, isEligibleToExport: form.isEligibleToExport, imageId: form.imageId || undefined, + productFiles: form.productFiles.map((file) => file.id).filter(Boolean), productImages: form.productImages.filter(Boolean).map((imageId, i) => ({ imageId, sequence: i + 1 })), productKeyWords: form.keywords.filter(Boolean), productFeatures: form.features.filter(Boolean), @@ -1141,10 +1227,10 @@ function EditProductPageInner() { dimensionType: m.dimensionType || "CM", isMeasurement: m.measurements.length > 0, isConfigurePromotionPrice: m.hasPromotion, - promotionPrice: m.hasPromotion ? toNum(m.promotionPrice) : 0, - promotionCurrency: m.promotionCurrency || m.currency, - promotionStartDate: m.promotionStartDate || undefined, - promotionEndDate: m.promotionEndDate || undefined, + promotionPrice: m.hasPromotion ? toNum(m.promotionPrice) : null, + promotionCurrency: m.hasPromotion ? (m.promotionCurrency || m.currency) : null, + promotionStartDate: m.hasPromotion ? (m.promotionStartDate || null) : null, + promotionEndDate: m.hasPromotion ? (m.promotionEndDate || null) : null, packagingWeight: toNum(m.packagingWeight), packagingWeightType: m.packagingWeightType || "G", packagingLength: toNum(m.packagingLength), @@ -1170,10 +1256,10 @@ function EditProductPageInner() { packagingHeight: toNum(ms.packagingHeight), packagingDimensionType: ms.packagingDimensionType || "CM", isConfigurePromotionPrice: ms.hasPromotion, - promotionPrice: ms.hasPromotion ? toNum(ms.promotionPrice) : 0, - promotionCurrency: ms.promotionCurrency || ms.currency || "IDR", - promotionStartDate: ms.promotionStartDate || undefined, - promotionEndDate: ms.promotionEndDate || undefined, + promotionPrice: ms.hasPromotion ? toNum(ms.promotionPrice) : null, + promotionCurrency: ms.hasPromotion ? (ms.promotionCurrency || ms.currency || "IDR") : null, + promotionStartDate: ms.hasPromotion ? (ms.promotionStartDate || null) : null, + promotionEndDate: ms.hasPromotion ? (ms.promotionEndDate || null) : null, warehouses: ms.warehouses.filter((w) => w.id).map((w) => ({ id: w.id, stock: Number(w.stock || 0) })), })), })), @@ -1181,8 +1267,9 @@ function EditProductPageInner() { categoryInformations: form.categoryInformations.filter((i) => i.paramName && i.paramValue), complianceInformation: { ...form.complianceInformation }, warrantyInformation: { ...form.warrantyInformation, duration: toNum(form.warrantyInformation.duration) }, + state: resolvedState, }; - return state ? { ...base, state } : base; + return base; } async function handleSaveDraft() { @@ -1195,7 +1282,6 @@ function EditProductPageInner() { try { const payload = buildPayload(); - setDebugPayload(JSON.stringify(payload, null, 2)); const res = await fetch(`/api/products/${params.productId}?draft=1`, { method: "PUT", @@ -1227,7 +1313,6 @@ function EditProductPageInner() { try { const payload = buildPayload("PUBLISHED"); - setDebugPayload(JSON.stringify(payload, null, 2)); const res = await fetch("/api/products/create", { method: "POST", @@ -1259,9 +1344,6 @@ function EditProductPageInner() { try { const payload = buildPayload(); - const payloadJson = JSON.stringify(payload, null, 2); - console.log("[EditProduct] PUT payload:", payloadJson); - setDebugPayload(payloadJson); const res = await fetch(`/api/products/${params.productId}`, { method: "PUT", @@ -1638,6 +1720,60 @@ function EditProductPageInner() { onChange={(ev) => update({ complianceInformation: { ...form.complianceInformation, isDangerousGoodRegulation: ev.target.checked } })} className="h-5 w-5 rounded border-outline-variant" /> +
+

MSDS / Compliance File

+ { + const file = ev.target.files?.[0]; + if (file) handleMsdsUpload(file); + ev.target.value = ""; + }} + /> + {form.complianceInformation.fileId ? ( +
+ description + + {msdsName || form.complianceInformation.fileId} + + +
+ ) : ( + + )} + {msdsError &&

{msdsError}

} +
@@ -1661,6 +1797,66 @@ function EditProductPageInner() {
+ +
+ + {/* Supporting Documents */} +
+
+

Product Supporting Documents

+

+ Existing file IDs are preserved and new uploads will append to the update payload. +

+
+
+ {form.productFiles.map((doc) => ( +
+ description + + {doc.name || doc.id} + + +
+ ))} +
+ { + const files = Array.from(ev.target.files || []); + await handleSupportingDocumentsUpload(files); + ev.target.value = ""; + }} + /> + + {docError &&

{docError}

} +
{/* ── Fixed Bottom Footer ───────────────────────────────────────────── */} diff --git a/src/app/(onboarding)/layout.tsx b/src/app/(onboarding)/layout.tsx index f4228f6..93869da 100644 --- a/src/app/(onboarding)/layout.tsx +++ b/src/app/(onboarding)/layout.tsx @@ -2,7 +2,8 @@ import Image from "next/image"; import Link from "next/link"; -import { usePathname } from "next/navigation"; +import { useEffect } from "react"; +import { usePathname, useRouter } from "next/navigation"; import { LanguageToggle } from "@/components/language-toggle"; import { useLanguage } from "@/lib/i18n-context"; @@ -18,6 +19,7 @@ export default function OnboardingLayout({ children: React.ReactNode; }) { const pathname = usePathname(); + const router = useRouter(); const { t } = useLanguage(); const lo = t.onboarding.layout; @@ -25,6 +27,16 @@ export default function OnboardingLayout({ const currentStep = steps.find((s) => pathname.startsWith(s.href)); const stepNumber = currentStep?.step ?? (isSuccessPage ? steps.length : 1); + useEffect(() => { + if (pathname.startsWith("/onboarding")) { + router.replace("/dashboard"); + } + }, [pathname, router]); + + if (pathname.startsWith("/onboarding")) { + return null; + } + if (isSuccessPage) { return (
diff --git a/src/app/api/products/[productId]/route.ts b/src/app/api/products/[productId]/route.ts index f0cbe30..65b5fbd 100644 --- a/src/app/api/products/[productId]/route.ts +++ b/src/app/api/products/[productId]/route.ts @@ -1,11 +1,16 @@ import { NextRequest, NextResponse } from "next/server"; import { API_URL, makeHeaders } from "@/lib/api"; +function normalizeBearerToken(rawToken: string) { + if (!rawToken) return ""; + return rawToken.startsWith("Bearer ") ? rawToken : `Bearer ${rawToken}`; +} + export async function GET( req: NextRequest, context: { params: Promise<{ productId: string }> } ) { - const token = req.headers.get("x-auth-token") || ""; + const token = normalizeBearerToken(req.headers.get("x-auth-token") || ""); const { productId } = await context.params; const isDraft = req.nextUrl.searchParams.get("draft") === "1"; @@ -27,30 +32,81 @@ export async function PUT( req: NextRequest, context: { params: Promise<{ productId: string }> } ) { - const token = req.headers.get("x-auth-token") || ""; - const { productId } = await context.params; - const body = await req.json(); - const isDraft = req.nextUrl.searchParams.get("draft") === "1"; + try { + const token = normalizeBearerToken(req.headers.get("x-auth-token") || ""); + const { productId } = await context.params; + const body = await req.json(); + const isDraft = req.nextUrl.searchParams.get("draft") === "1"; - const endpoint = isDraft - ? `${API_URL}/api/v1.0/product/draft/${productId}` - : `${API_URL}/api/v1.0/product/${productId}`; + const endpoint = isDraft + ? `${API_URL}/api/v1.0/product/draft/${productId}` + : `${API_URL}/api/v1.0/product/${productId}`; - const res = await fetch(endpoint, { - method: "PUT", - headers: makeHeaders(token), - body: JSON.stringify(body), - }); + console.log("[api/products/[productId]] PUT request", { + productId, + isDraft, + endpoint, + hasToken: Boolean(token), + productFilesCount: Array.isArray(body?.productFiles) ? body.productFiles.length : 0, + productImagesCount: Array.isArray(body?.productImages) ? body.productImages.length : 0, + productModelsCount: Array.isArray(body?.productModels) ? body.productModels.length : 0, + complianceFileId: body?.complianceInformation?.fileId || "", + }); - const data = await res.json().catch(() => ({})); - return NextResponse.json(data, { status: res.status }); + const res = await fetch(endpoint, { + method: "PUT", + headers: makeHeaders(token), + body: JSON.stringify(body), + }); + + const raw = await res.text(); + const contentType = res.headers.get("content-type") || ""; + + if (!contentType.includes("application/json")) { + console.log("[api/products/[productId]] PUT non-JSON response", { + productId, + endpoint, + status: res.status, + bodyPreview: raw.slice(0, 500), + }); + return NextResponse.json( + { + responseDesc: "Backend returned a non-JSON response", + details: raw.slice(0, 500), + }, + { status: res.status || 502 } + ); + } + + const data = raw ? JSON.parse(raw) : {}; + console.log("[api/products/[productId]] PUT response", { + productId, + status: res.status, + responseCode: data?.responseCode, + responseDesc: data?.responseDesc, + rawPreview: raw.slice(0, 500), + }); + return NextResponse.json(data, { status: res.status }); + } catch (error) { + console.log("[api/products/[productId]] PUT failed", { + message: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + }); + return NextResponse.json( + { + responseDesc: + error instanceof Error ? error.message : "Unknown proxy error", + }, + { status: 500 } + ); + } } export async function DELETE( req: NextRequest, context: { params: Promise<{ productId: string }> } ) { - const token = req.headers.get("x-auth-token") || ""; + const token = normalizeBearerToken(req.headers.get("x-auth-token") || ""); const { productId } = await context.params; const isDraft = req.nextUrl.searchParams.get("draft") === "1"; diff --git a/src/lib/product-options.ts b/src/lib/product-options.ts index 90eee50..4f5baa4 100644 --- a/src/lib/product-options.ts +++ b/src/lib/product-options.ts @@ -58,7 +58,6 @@ export const WORLD_CURRENCIES = [ { value: "NGN", label: "NGN — Nigerian Naira" }, { value: "KES", label: "KES — Kenyan Shilling" }, { value: "GHS", label: "GHS — Ghanaian Cedi" }, - { value: "EGP", label: "EGP — Egyptian Pound" }, { value: "MAD", label: "MAD — Moroccan Dirham" }, { value: "TND", label: "TND — Tunisian Dinar" }, { value: "BRL", label: "BRL — Brazilian Real" }, diff --git a/src/lib/translations/en.ts b/src/lib/translations/en.ts index 614f64b..007a393 100644 --- a/src/lib/translations/en.ts +++ b/src/lib/translations/en.ts @@ -80,7 +80,7 @@ export const en = { verifyFail: "Verification failed", registerFail: "Seller registration failed", successSeller: - "OTP valid and seller account created. Redirecting to business data...", + "OTP valid and seller account created. Redirecting to dashboard...", successBuyer: "OTP successfully verified. Redirecting to the next step...", securityTitle: "Institutional-Grade Security", diff --git a/src/lib/translations/id.ts b/src/lib/translations/id.ts index 2b8880a..d0d0a4f 100644 --- a/src/lib/translations/id.ts +++ b/src/lib/translations/id.ts @@ -81,7 +81,7 @@ export const id = { verifyFail: "Verifikasi gagal", registerFail: "Registrasi seller gagal", successSeller: - "OTP valid dan akun seller berhasil dibuat. Mengalihkan ke data bisnis...", + "OTP valid dan akun seller berhasil dibuat. Mengalihkan ke dashboard...", successBuyer: "OTP berhasil diverifikasi. Mengalihkan ke langkah berikutnya...", securityTitle: "Keamanan Tingkat Institusional",