Update onboarding and product edit flows

This commit is contained in:
2026-05-08 09:47:03 +07:00
parent a1e20ccd9b
commit 0aa5227943
9 changed files with 334 additions and 83 deletions

View File

@ -1,6 +1,15 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
async redirects() {
return [
{
source: "/onboarding/:path*",
destination: "/dashboard",
permanent: false,
},
];
},
images: { images: {
unoptimized: true, unoptimized: true,
remotePatterns: [ remotePatterns: [

View File

@ -59,27 +59,6 @@ export default function LoginPage() {
} }
if (data.role === "seller") { 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"); router.push("/dashboard");
return; return;
} }

View File

@ -125,7 +125,7 @@ function VerifyContent() {
sessionStorage.removeItem("otpVerified"); sessionStorage.removeItem("otpVerified");
sessionStorage.removeItem("otpVerifiedEmail"); sessionStorage.removeItem("otpVerifiedEmail");
setSuccess(v.successSeller); setSuccess(v.successSeller);
setTimeout(() => { router.push("/onboarding/business"); }, 1000); setTimeout(() => { router.push("/dashboard"); }, 1000);
return; return;
} }

View File

@ -90,6 +90,7 @@ interface EditState {
isNew: boolean; isNew: boolean;
isEligibleToExport: boolean; isEligibleToExport: boolean;
imageId: string; imageId: string;
productFiles: Array<{ id: string; name: string }>;
productImages: string[]; productImages: string[];
keywords: string[]; keywords: string[];
features: string[]; features: string[];
@ -153,6 +154,14 @@ interface ApiProductImage {
imageId?: string | number | null; 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 { interface ApiParamItem {
paramName?: string; paramName?: string;
paramValue?: string; paramValue?: string;
@ -178,6 +187,7 @@ interface ApiProduct {
isNew?: boolean | null; isNew?: boolean | null;
isEligibleToExport?: boolean | null; isEligibleToExport?: boolean | null;
imageId?: string | number | null; imageId?: string | number | null;
productFiles?: ApiProductFile[] | Array<string | number>;
productImages?: ApiProductImage[]; productImages?: ApiProductImage[];
productKeyWords?: string[]; productKeyWords?: string[];
productFeatures?: string[]; productFeatures?: string[];
@ -215,6 +225,50 @@ function toStr(v: string | number | null | undefined): string {
return String(v); 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 { function newMeasurement(): EditMeasurement {
return { return {
_key: `meas-${Date.now()}-${Math.random().toString(36).slice(2)}`, _key: `meas-${Date.now()}-${Math.random().toString(36).slice(2)}`,
@ -354,6 +408,7 @@ function apiToEditState(data: ApiProduct): EditState {
isNew: data?.isNew !== false, isNew: data?.isNew !== false,
isEligibleToExport: !!data?.isEligibleToExport, isEligibleToExport: !!data?.isEligibleToExport,
imageId: toStr(data?.imageId), imageId: toStr(data?.imageId),
productFiles: toProductFiles(data?.productFiles),
productImages: Array.isArray(data?.productImages) productImages: Array.isArray(data?.productImages)
? data.productImages ? data.productImages
.sort( .sort(
@ -420,18 +475,8 @@ function ImageSlotUpload({
setUploading(true); setUploading(true);
setError(""); setError("");
try { try {
const fd = new FormData(); const { fileId } = await uploadFile(file);
fd.append("file", file); onUploaded(fileId);
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);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Upload gagal"); setError(err instanceof Error ? err.message : "Upload gagal");
} finally { } finally {
@ -483,18 +528,8 @@ function ModelImageUpload({ value, onUploaded }: { value: string; onUploaded: (i
setUploading(true); setUploading(true);
setError(""); setError("");
try { try {
const fd = new FormData(); const { fileId } = await uploadFile(file);
fd.append("file", file); onUploaded(fileId);
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);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Upload gagal"); setError(err instanceof Error ? err.message : "Upload gagal");
setPreviewUrl(""); setPreviewUrl("");
@ -1009,9 +1044,15 @@ function EditProductPageInner() {
const [loadingSubcategories, setLoadingSubcategories] = useState(false); const [loadingSubcategories, setLoadingSubcategories] = useState(false);
const [warehouses, setWarehouses] = useState<WarehouseOption[]>([]); const [warehouses, setWarehouses] = useState<WarehouseOption[]>([]);
const [keywordInput, setKeywordInput] = useState(""); const [keywordInput, setKeywordInput] = useState("");
const [debugPayload, setDebugPayload] = useState<string>("");
const [publishing, setPublishing] = useState(false); const [publishing, setPublishing] = useState(false);
const [publishSuccess, setPublishSuccess] = 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 // Load product, categories (with subcategory resolution), and warehouses
useEffect(() => { useEffect(() => {
@ -1078,7 +1119,7 @@ function EditProductPageInner() {
} }
loadAll(); loadAll();
}, [params.productId]); }, [isDraftParam, params.productId]);
// Load subcategories when category changes (for draft editing) // Load subcategories when category changes (for draft editing)
useEffect(() => { useEffect(() => {
@ -1113,17 +1154,62 @@ function EditProductPageInner() {
setKeywordInput(""); 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") { function buildPayload(state?: "DRAFT" | "PUBLISHED") {
if (!form) return null; if (!form) return null;
const resolvedState = state ?? "DRAFT";
const base = { const base = {
subCategory: form.subCategoryId ? { id: form.subCategoryId } : undefined, subCategory: form.subCategoryId ? { id: form.subCategoryId } : undefined,
name: form.name, name: form.name,
description: form.description, description: form.description,
isPreOrder: form.isPreOrder, isPreOrder: form.isPreOrder,
preOrderDay: form.isPreOrder ? toNum(form.preOrderDay) : 0, preOrderDay: form.isPreOrder ? toNum(form.preOrderDay) : null,
isNew: form.isNew, isNew: form.isNew,
isEligibleToExport: form.isEligibleToExport, isEligibleToExport: form.isEligibleToExport,
imageId: form.imageId || undefined, 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 })), productImages: form.productImages.filter(Boolean).map((imageId, i) => ({ imageId, sequence: i + 1 })),
productKeyWords: form.keywords.filter(Boolean), productKeyWords: form.keywords.filter(Boolean),
productFeatures: form.features.filter(Boolean), productFeatures: form.features.filter(Boolean),
@ -1141,10 +1227,10 @@ function EditProductPageInner() {
dimensionType: m.dimensionType || "CM", dimensionType: m.dimensionType || "CM",
isMeasurement: m.measurements.length > 0, isMeasurement: m.measurements.length > 0,
isConfigurePromotionPrice: m.hasPromotion, isConfigurePromotionPrice: m.hasPromotion,
promotionPrice: m.hasPromotion ? toNum(m.promotionPrice) : 0, promotionPrice: m.hasPromotion ? toNum(m.promotionPrice) : null,
promotionCurrency: m.promotionCurrency || m.currency, promotionCurrency: m.hasPromotion ? (m.promotionCurrency || m.currency) : null,
promotionStartDate: m.promotionStartDate || undefined, promotionStartDate: m.hasPromotion ? (m.promotionStartDate || null) : null,
promotionEndDate: m.promotionEndDate || undefined, promotionEndDate: m.hasPromotion ? (m.promotionEndDate || null) : null,
packagingWeight: toNum(m.packagingWeight), packagingWeight: toNum(m.packagingWeight),
packagingWeightType: m.packagingWeightType || "G", packagingWeightType: m.packagingWeightType || "G",
packagingLength: toNum(m.packagingLength), packagingLength: toNum(m.packagingLength),
@ -1170,10 +1256,10 @@ function EditProductPageInner() {
packagingHeight: toNum(ms.packagingHeight), packagingHeight: toNum(ms.packagingHeight),
packagingDimensionType: ms.packagingDimensionType || "CM", packagingDimensionType: ms.packagingDimensionType || "CM",
isConfigurePromotionPrice: ms.hasPromotion, isConfigurePromotionPrice: ms.hasPromotion,
promotionPrice: ms.hasPromotion ? toNum(ms.promotionPrice) : 0, promotionPrice: ms.hasPromotion ? toNum(ms.promotionPrice) : null,
promotionCurrency: ms.promotionCurrency || ms.currency || "IDR", promotionCurrency: ms.hasPromotion ? (ms.promotionCurrency || ms.currency || "IDR") : null,
promotionStartDate: ms.promotionStartDate || undefined, promotionStartDate: ms.hasPromotion ? (ms.promotionStartDate || null) : null,
promotionEndDate: ms.promotionEndDate || undefined, promotionEndDate: ms.hasPromotion ? (ms.promotionEndDate || null) : null,
warehouses: ms.warehouses.filter((w) => w.id).map((w) => ({ id: w.id, stock: Number(w.stock || 0) })), 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), categoryInformations: form.categoryInformations.filter((i) => i.paramName && i.paramValue),
complianceInformation: { ...form.complianceInformation }, complianceInformation: { ...form.complianceInformation },
warrantyInformation: { ...form.warrantyInformation, duration: toNum(form.warrantyInformation.duration) }, warrantyInformation: { ...form.warrantyInformation, duration: toNum(form.warrantyInformation.duration) },
state: resolvedState,
}; };
return state ? { ...base, state } : base; return base;
} }
async function handleSaveDraft() { async function handleSaveDraft() {
@ -1195,7 +1282,6 @@ function EditProductPageInner() {
try { try {
const payload = buildPayload(); const payload = buildPayload();
setDebugPayload(JSON.stringify(payload, null, 2));
const res = await fetch(`/api/products/${params.productId}?draft=1`, { const res = await fetch(`/api/products/${params.productId}?draft=1`, {
method: "PUT", method: "PUT",
@ -1227,7 +1313,6 @@ function EditProductPageInner() {
try { try {
const payload = buildPayload("PUBLISHED"); const payload = buildPayload("PUBLISHED");
setDebugPayload(JSON.stringify(payload, null, 2));
const res = await fetch("/api/products/create", { const res = await fetch("/api/products/create", {
method: "POST", method: "POST",
@ -1259,9 +1344,6 @@ function EditProductPageInner() {
try { try {
const payload = buildPayload(); 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}`, { const res = await fetch(`/api/products/${params.productId}`, {
method: "PUT", method: "PUT",
@ -1638,6 +1720,60 @@ function EditProductPageInner() {
onChange={(ev) => update({ complianceInformation: { ...form.complianceInformation, isDangerousGoodRegulation: ev.target.checked } })} onChange={(ev) => update({ complianceInformation: { ...form.complianceInformation, isDangerousGoodRegulation: ev.target.checked } })}
className="h-5 w-5 rounded border-outline-variant" /> className="h-5 w-5 rounded border-outline-variant" />
</label> </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>
<div className="border-t border-surface-container" /> <div className="border-t border-surface-container" />
@ -1661,6 +1797,66 @@ function EditProductPageInner() {
</select> </select>
</div> </div>
</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> </div>
{/* ── Fixed Bottom Footer ───────────────────────────────────────────── */} {/* ── Fixed Bottom Footer ───────────────────────────────────────────── */}

View File

@ -2,7 +2,8 @@
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; 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 { LanguageToggle } from "@/components/language-toggle";
import { useLanguage } from "@/lib/i18n-context"; import { useLanguage } from "@/lib/i18n-context";
@ -18,6 +19,7 @@ export default function OnboardingLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const pathname = usePathname(); const pathname = usePathname();
const router = useRouter();
const { t } = useLanguage(); const { t } = useLanguage();
const lo = t.onboarding.layout; const lo = t.onboarding.layout;
@ -25,6 +27,16 @@ export default function OnboardingLayout({
const currentStep = steps.find((s) => pathname.startsWith(s.href)); const currentStep = steps.find((s) => pathname.startsWith(s.href));
const stepNumber = currentStep?.step ?? (isSuccessPage ? steps.length : 1); 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) { if (isSuccessPage) {
return ( return (
<div className="min-h-screen bg-surface text-on-surface font-body antialiased"> <div className="min-h-screen bg-surface text-on-surface font-body antialiased">

View File

@ -1,11 +1,16 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { API_URL, makeHeaders } from "@/lib/api"; 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( export async function GET(
req: NextRequest, req: NextRequest,
context: { params: Promise<{ productId: string }> } 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 { productId } = await context.params;
const isDraft = req.nextUrl.searchParams.get("draft") === "1"; const isDraft = req.nextUrl.searchParams.get("draft") === "1";
@ -27,7 +32,8 @@ export async function PUT(
req: NextRequest, req: NextRequest,
context: { params: Promise<{ productId: string }> } context: { params: Promise<{ productId: string }> }
) { ) {
const token = req.headers.get("x-auth-token") || ""; try {
const token = normalizeBearerToken(req.headers.get("x-auth-token") || "");
const { productId } = await context.params; const { productId } = await context.params;
const body = await req.json(); const body = await req.json();
const isDraft = req.nextUrl.searchParams.get("draft") === "1"; const isDraft = req.nextUrl.searchParams.get("draft") === "1";
@ -36,21 +42,71 @@ export async function PUT(
? `${API_URL}/api/v1.0/product/draft/${productId}` ? `${API_URL}/api/v1.0/product/draft/${productId}`
: `${API_URL}/api/v1.0/product/${productId}`; : `${API_URL}/api/v1.0/product/${productId}`;
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 res = await fetch(endpoint, { const res = await fetch(endpoint, {
method: "PUT", method: "PUT",
headers: makeHeaders(token), headers: makeHeaders(token),
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
const data = await res.json().catch(() => ({})); 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 }); 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( export async function DELETE(
req: NextRequest, req: NextRequest,
context: { params: Promise<{ productId: string }> } 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 { productId } = await context.params;
const isDraft = req.nextUrl.searchParams.get("draft") === "1"; const isDraft = req.nextUrl.searchParams.get("draft") === "1";

View File

@ -58,7 +58,6 @@ export const WORLD_CURRENCIES = [
{ value: "NGN", label: "NGN — Nigerian Naira" }, { value: "NGN", label: "NGN — Nigerian Naira" },
{ value: "KES", label: "KES — Kenyan Shilling" }, { value: "KES", label: "KES — Kenyan Shilling" },
{ value: "GHS", label: "GHS — Ghanaian Cedi" }, { value: "GHS", label: "GHS — Ghanaian Cedi" },
{ value: "EGP", label: "EGP — Egyptian Pound" },
{ value: "MAD", label: "MAD — Moroccan Dirham" }, { value: "MAD", label: "MAD — Moroccan Dirham" },
{ value: "TND", label: "TND — Tunisian Dinar" }, { value: "TND", label: "TND — Tunisian Dinar" },
{ value: "BRL", label: "BRL — Brazilian Real" }, { value: "BRL", label: "BRL — Brazilian Real" },

View File

@ -80,7 +80,7 @@ export const en = {
verifyFail: "Verification failed", verifyFail: "Verification failed",
registerFail: "Seller registration failed", registerFail: "Seller registration failed",
successSeller: successSeller:
"OTP valid and seller account created. Redirecting to business data...", "OTP valid and seller account created. Redirecting to dashboard...",
successBuyer: successBuyer:
"OTP successfully verified. Redirecting to the next step...", "OTP successfully verified. Redirecting to the next step...",
securityTitle: "Institutional-Grade Security", securityTitle: "Institutional-Grade Security",

View File

@ -81,7 +81,7 @@ export const id = {
verifyFail: "Verifikasi gagal", verifyFail: "Verifikasi gagal",
registerFail: "Registrasi seller gagal", registerFail: "Registrasi seller gagal",
successSeller: successSeller:
"OTP valid dan akun seller berhasil dibuat. Mengalihkan ke data bisnis...", "OTP valid dan akun seller berhasil dibuat. Mengalihkan ke dashboard...",
successBuyer: successBuyer:
"OTP berhasil diverifikasi. Mengalihkan ke langkah berikutnya...", "OTP berhasil diverifikasi. Mengalihkan ke langkah berikutnya...",
securityTitle: "Keamanan Tingkat Institusional", securityTitle: "Keamanan Tingkat Institusional",