Update onboarding and product edit flows
This commit is contained in:
@ -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: [
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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<string | number>;
|
||||
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<WarehouseOption[]>([]);
|
||||
const [keywordInput, setKeywordInput] = useState("");
|
||||
const [debugPayload, setDebugPayload] = useState<string>("");
|
||||
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<HTMLInputElement | null>(null);
|
||||
const docInputRef = useRef<HTMLInputElement | null>(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" />
|
||||
</label>
|
||||
<div>
|
||||
<p className="block text-[11px] font-black uppercase tracking-[0.18em] text-outline mb-2">MSDS / Compliance File</p>
|
||||
<input
|
||||
ref={msdsInputRef}
|
||||
type="file"
|
||||
accept=".pdf,.doc,.docx"
|
||||
className="hidden"
|
||||
onChange={(ev) => {
|
||||
const file = ev.target.files?.[0];
|
||||
if (file) handleMsdsUpload(file);
|
||||
ev.target.value = "";
|
||||
}}
|
||||
/>
|
||||
{form.complianceInformation.fileId ? (
|
||||
<div className="flex items-center gap-3 bg-surface-container-low rounded-xl px-4 py-3">
|
||||
<span className="material-symbols-outlined text-primary text-[20px]">description</span>
|
||||
<span className="flex-1 text-sm font-semibold text-on-surface truncate">
|
||||
{msdsName || form.complianceInformation.fileId}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
update({
|
||||
complianceInformation: { ...form.complianceInformation, fileId: "" },
|
||||
});
|
||||
setMsdsName("");
|
||||
}}
|
||||
className="text-outline hover:text-error transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">close</span>
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => msdsInputRef.current?.click()}
|
||||
disabled={uploadingMsds}
|
||||
className="w-full border border-dashed border-outline-variant rounded-xl px-4 py-6 flex flex-col items-center justify-center gap-2 bg-surface-container-low/20 hover:bg-surface-container-low/60 transition-all disabled:opacity-60"
|
||||
>
|
||||
{uploadingMsds ? (
|
||||
<div className="w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<span className="material-symbols-outlined text-primary text-2xl">upload_file</span>
|
||||
)}
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-bold text-on-surface">
|
||||
{uploadingMsds ? e.uploading : "Upload MSDS / Compliance File"}
|
||||
</p>
|
||||
<p className="text-[10px] text-on-surface-variant">PDF, DOC, DOCX</p>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
{msdsError && <p className="text-[11px] text-error mt-1.5 font-semibold">{msdsError}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-surface-container" />
|
||||
@ -1661,6 +1797,66 @@ function EditProductPageInner() {
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-surface-container" />
|
||||
|
||||
{/* Supporting Documents */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h3 className="text-sm font-black text-on-surface">Product Supporting Documents</h3>
|
||||
<p className="text-[11px] text-on-surface-variant mt-1">
|
||||
Existing file IDs are preserved and new uploads will append to the update payload.
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{form.productFiles.map((doc) => (
|
||||
<div key={doc.id} className="flex items-center gap-3 bg-surface-container-low rounded-xl px-4 py-3">
|
||||
<span className="material-symbols-outlined text-primary text-[20px]">description</span>
|
||||
<span className="flex-1 text-sm font-semibold text-on-surface truncate">
|
||||
{doc.name || doc.id}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeDoc(doc.id)}
|
||||
className="text-outline hover:text-error transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">close</span>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
ref={docInputRef}
|
||||
type="file"
|
||||
accept=".pdf,.doc,.docx,.xlsx,.xls,.ppt,.pptx,.zip"
|
||||
multiple
|
||||
className="hidden"
|
||||
onChange={async (ev) => {
|
||||
const files = Array.from(ev.target.files || []);
|
||||
await handleSupportingDocumentsUpload(files);
|
||||
ev.target.value = "";
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => docInputRef.current?.click()}
|
||||
disabled={uploadingDoc}
|
||||
className="w-full border border-dashed border-outline-variant rounded-xl px-4 py-7 flex flex-col items-center justify-center gap-2 bg-surface-container-low/20 hover:bg-surface-container-low/60 transition-all disabled:opacity-60"
|
||||
>
|
||||
{uploadingDoc ? (
|
||||
<div className="w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin" />
|
||||
) : (
|
||||
<span className="material-symbols-outlined text-primary text-2xl">cloud_upload</span>
|
||||
)}
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-bold text-on-surface">
|
||||
{uploadingDoc ? e.uploading : "Upload Supporting Documents"}
|
||||
</p>
|
||||
<p className="text-[10px] text-on-surface-variant">PDF, DOCX, XLSX, PPTX, ZIP</p>
|
||||
</div>
|
||||
</button>
|
||||
{docError && <p className="text-[11px] text-error mt-1.5 font-semibold">{docError}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Fixed Bottom Footer ───────────────────────────────────────────── */}
|
||||
|
||||
@ -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 (
|
||||
<div className="min-h-screen bg-surface text-on-surface font-body antialiased">
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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" },
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
Reference in New Issue
Block a user