Update onboarding and product edit flows
This commit is contained in:
@ -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: [
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 ───────────────────────────────────────────── */}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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" },
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user