Fix product preorder and warranty submit flow
This commit is contained in:
35
HANDOFF.md
35
HANDOFF.md
@ -22,6 +22,41 @@ npx tsc --noEmit
|
|||||||
|
|
||||||
## Latest Codex Changes After `76fb4f3`
|
## Latest Codex Changes After `76fb4f3`
|
||||||
|
|
||||||
|
### Product create preorder and warranty submit rules
|
||||||
|
|
||||||
|
Files:
|
||||||
|
- `src/app/(dashboard)/products/new/details/page.tsx`
|
||||||
|
- `src/app/(dashboard)/products/new/specifications/page.tsx`
|
||||||
|
- `src/app/(dashboard)/products/new/review/page.tsx`
|
||||||
|
- `src/lib/product-draft.tsx`
|
||||||
|
- `src/lib/use-product-submit.ts`
|
||||||
|
- `src/lib/product-request-validation.ts`
|
||||||
|
|
||||||
|
Behavior:
|
||||||
|
- Pre-order day placeholder is now `0`, not `14`, so an empty/unfilled value is visually obvious.
|
||||||
|
- If pre-order is checked, the Details step blocks Next unless `preOrderDay` is a valid number greater than or equal to 1.
|
||||||
|
- Warranty now has an On/Off switch in the Specifications step.
|
||||||
|
- When warranty is On:
|
||||||
|
- warranty type is required
|
||||||
|
- warranty duration must be greater than or equal to 1
|
||||||
|
- the review page shows warranty information
|
||||||
|
- submit payload sends the filled `warrantyInformation` object
|
||||||
|
- When warranty is Off:
|
||||||
|
- warranty inputs are disabled and dimmed
|
||||||
|
- the review page hides warranty information
|
||||||
|
- submit payload still sends `warrantyInformation` as an object required by the backend, with:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": null,
|
||||||
|
"duration": null,
|
||||||
|
"durationType": "MONTH"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- Frontend payload validation treats the above null-valued warranty object as warranty Off, so it does not block submit with warranty type/duration errors.
|
||||||
|
- Local test server was rebuilt and restarted against `https://api-dev.inatrading.co.id` in `.env.local`; `.env.local` is not committed.
|
||||||
|
|
||||||
### AddProductRequest validation
|
### AddProductRequest validation
|
||||||
|
|
||||||
Files:
|
Files:
|
||||||
|
|||||||
@ -171,6 +171,7 @@ export default function ProductDetailsPage() {
|
|||||||
const { submit, submitting } = useProductSubmit();
|
const { submit, submitting } = useProductSubmit();
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const d = t.dashboard.productNew.details;
|
const d = t.dashboard.productNew.details;
|
||||||
|
const [preOrderError, setPreOrderError] = useState("");
|
||||||
const [keywordInput, setKeywordInput] = useState("");
|
const [keywordInput, setKeywordInput] = useState("");
|
||||||
const keywordLimitReached = draft.keywords.filter(Boolean).length >= MAX_KEYWORDS;
|
const keywordLimitReached = draft.keywords.filter(Boolean).length >= MAX_KEYWORDS;
|
||||||
const featureLimitReached = draft.features.length >= MAX_FEATURES;
|
const featureLimitReached = draft.features.length >= MAX_FEATURES;
|
||||||
@ -249,6 +250,17 @@ export default function ProductDetailsPage() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goToPricing() {
|
||||||
|
const preOrderDay = Number(draft.preOrderDay);
|
||||||
|
if (draft.isPreOrder && (!Number.isFinite(preOrderDay) || preOrderDay < 1)) {
|
||||||
|
setPreOrderError("Hari pre-order wajib diisi minimal 1 hari.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPreOrderError("");
|
||||||
|
router.push("/products/new/pricing");
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 xl:grid-cols-12 gap-8">
|
<div className="grid grid-cols-1 xl:grid-cols-12 gap-8">
|
||||||
<div className="xl:col-span-12 rounded-xl border border-outline-variant/15 bg-surface-container-lowest p-5">
|
<div className="xl:col-span-12 rounded-xl border border-outline-variant/15 bg-surface-container-lowest p-5">
|
||||||
@ -318,14 +330,17 @@ export default function ProductDetailsPage() {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
value={draft.preOrderDay}
|
value={draft.preOrderDay}
|
||||||
onChange={(e) => setDraft((prev) => ({ ...prev, preOrderDay: e.target.value }))}
|
onChange={(e) => {
|
||||||
placeholder="14"
|
setPreOrderError("");
|
||||||
|
setDraft((prev) => ({ ...prev, preOrderDay: e.target.value }));
|
||||||
|
}}
|
||||||
|
placeholder="0"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
className="w-full bg-surface-container-low border-none rounded-xl p-4 font-semibold focus:ring-2 focus:ring-primary/10"
|
className="w-full bg-surface-container-low border-none rounded-xl p-4 font-semibold focus:ring-2 focus:ring-primary/10"
|
||||||
/>
|
/>
|
||||||
<p className="mt-1.5 text-[11px] font-semibold text-on-surface-variant">
|
<p className={`mt-1.5 text-[11px] font-semibold ${preOrderError ? "text-error" : "text-on-surface-variant"}`}>
|
||||||
Minimal 1 hari jika pre-order aktif.
|
{preOrderError || "Minimal 1 hari jika pre-order aktif."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -517,7 +532,7 @@ export default function ProductDetailsPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push("/products/new/pricing")}
|
onClick={goToPricing}
|
||||||
className="editorial-gradient text-white px-8 py-3.5 rounded-xl font-black text-sm uppercase tracking-[0.18em] shadow-lg shadow-primary/20 hover:-translate-y-0.5 transition-all flex items-center gap-2"
|
className="editorial-gradient text-white px-8 py-3.5 rounded-xl font-black text-sm uppercase tracking-[0.18em] shadow-lg shadow-primary/20 hover:-translate-y-0.5 transition-all flex items-center gap-2"
|
||||||
>
|
>
|
||||||
{d.next}
|
{d.next}
|
||||||
|
|||||||
@ -448,7 +448,7 @@ export default function ProductReviewPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{(draft.warrantyInformation.type || draft.warrantyInformation.duration) && (
|
{draft.warrantyInformation.enabled && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold text-on-surface-variant mb-2">{r.warranty}</p>
|
<p className="text-xs font-bold text-on-surface-variant mb-2">{r.warranty}</p>
|
||||||
<Row label={r.warrantyType} value={draft.warrantyInformation.type} yes={r.yes} no={r.no} />
|
<Row label={r.warrantyType} value={draft.warrantyInformation.type} yes={r.yes} no={r.no} />
|
||||||
|
|||||||
@ -70,6 +70,7 @@ export default function ProductSpecificationsPage() {
|
|||||||
const msdsInputRef = useRef<HTMLInputElement>(null);
|
const msdsInputRef = useRef<HTMLInputElement>(null);
|
||||||
const docInputRef = useRef<HTMLInputElement>(null);
|
const docInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [msdsName, setMsdsName] = useState("");
|
const [msdsName, setMsdsName] = useState("");
|
||||||
|
const [warrantyError, setWarrantyError] = useState("");
|
||||||
|
|
||||||
// --- productInformations helpers ---
|
// --- productInformations helpers ---
|
||||||
function getProductInfo(key: string) {
|
function getProductInfo(key: string) {
|
||||||
@ -140,6 +141,22 @@ export default function ProductSpecificationsPage() {
|
|||||||
setDraft((prev) => ({ ...prev, productFiles: (prev.productFiles ?? []).filter((f) => f.id !== id) }));
|
setDraft((prev) => ({ ...prev, productFiles: (prev.productFiles ?? []).filter((f) => f.id !== id) }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goToReview() {
|
||||||
|
const warrantyDuration = Number(draft.warrantyInformation.duration);
|
||||||
|
if (
|
||||||
|
draft.warrantyInformation.enabled &&
|
||||||
|
(!draft.warrantyInformation.type.trim() ||
|
||||||
|
!Number.isFinite(warrantyDuration) ||
|
||||||
|
warrantyDuration < 1)
|
||||||
|
) {
|
||||||
|
setWarrantyError("Tipe garansi dan durasi minimal 1 wajib diisi jika garansi aktif.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setWarrantyError("");
|
||||||
|
router.push("/products/new/review");
|
||||||
|
}
|
||||||
|
|
||||||
const subCategoryAttributes = draft.subCategoryAttributes ?? [];
|
const subCategoryAttributes = draft.subCategoryAttributes ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -157,7 +174,7 @@ export default function ProductSpecificationsPage() {
|
|||||||
<p>Safety warning maksimal 100 karakter.</p>
|
<p>Safety warning maksimal 100 karakter.</p>
|
||||||
<p>Country of origin maksimal 100 karakter.</p>
|
<p>Country of origin maksimal 100 karakter.</p>
|
||||||
<p>Compliance file ID maksimal 100 karakter.</p>
|
<p>Compliance file ID maksimal 100 karakter.</p>
|
||||||
<p>Warranty type maksimal 50 karakter, durasi minimal 1 jika garansi diisi.</p>
|
<p>Warranty type maksimal 50 karakter, durasi minimal 1 jika garansi aktif.</p>
|
||||||
<p>Dokumen pendukung maksimal 100 karakter per file ID.</p>
|
<p>Dokumen pendukung maksimal 100 karakter per file ID.</p>
|
||||||
<p>Upload dokumen maksimal 10 MB per file.</p>
|
<p>Upload dokumen maksimal 10 MB per file.</p>
|
||||||
</div>
|
</div>
|
||||||
@ -362,24 +379,47 @@ export default function ProductSpecificationsPage() {
|
|||||||
|
|
||||||
{/* 4. Warranty */}
|
{/* 4. Warranty */}
|
||||||
<section className={sectionClass}>
|
<section className={sectionClass}>
|
||||||
<div className="flex items-center gap-3 mb-5">
|
<div className="flex flex-col gap-4 mb-5 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
<div className={sectionIconClass}>
|
<div className={sectionIconClass}>
|
||||||
<span className="material-symbols-outlined">verified</span>
|
<span className="material-symbols-outlined">verified</span>
|
||||||
</div>
|
</div>
|
||||||
<h3 className="text-sm font-black text-on-surface uppercase tracking-wider">{s.warranty}</h3>
|
<h3 className="text-sm font-black text-on-surface uppercase tracking-wider">{s.warranty}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<label className="inline-flex items-center gap-3 text-xs font-black uppercase tracking-[0.16em] text-on-surface-variant">
|
||||||
|
<span>{draft.warrantyInformation.enabled ? "On" : "Off"}</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={draft.warrantyInformation.enabled}
|
||||||
|
onChange={(e) => {
|
||||||
|
setWarrantyError("");
|
||||||
|
setDraft((prev) => ({
|
||||||
|
...prev,
|
||||||
|
warrantyInformation: {
|
||||||
|
...prev.warrantyInformation,
|
||||||
|
enabled: e.target.checked,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
className="peer sr-only"
|
||||||
|
/>
|
||||||
|
<span className="relative h-7 w-12 rounded-full bg-surface-container-high transition-colors peer-checked:bg-primary after:absolute after:left-1 after:top-1 after:h-5 after:w-5 after:rounded-full after:bg-white after:shadow-sm after:transition-transform peer-checked:after:translate-x-5" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${draft.warrantyInformation.enabled ? "" : "opacity-50"}`}>
|
||||||
<div>
|
<div>
|
||||||
<label className={labelClass}>{s.warrantyType}</label>
|
<label className={labelClass}>{s.warrantyType}</label>
|
||||||
<input
|
<input
|
||||||
value={draft.warrantyInformation.type}
|
value={draft.warrantyInformation.type}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
|
setWarrantyError("");
|
||||||
setDraft((prev) => ({
|
setDraft((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
warrantyInformation: { ...prev.warrantyInformation, type: e.target.value },
|
warrantyInformation: { ...prev.warrantyInformation, type: e.target.value },
|
||||||
}))
|
}));
|
||||||
}
|
}}
|
||||||
placeholder="e.g. Global Manufacturers Warranty"
|
placeholder="e.g. Global Manufacturers Warranty"
|
||||||
|
disabled={!draft.warrantyInformation.enabled}
|
||||||
className={inputClass}
|
className={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -388,15 +428,17 @@ export default function ProductSpecificationsPage() {
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<input
|
<input
|
||||||
value={draft.warrantyInformation.duration}
|
value={draft.warrantyInformation.duration}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
|
setWarrantyError("");
|
||||||
setDraft((prev) => ({
|
setDraft((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
warrantyInformation: { ...prev.warrantyInformation, duration: e.target.value },
|
warrantyInformation: { ...prev.warrantyInformation, duration: e.target.value },
|
||||||
}))
|
}));
|
||||||
}
|
}}
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="0"
|
||||||
|
disabled={!draft.warrantyInformation.enabled}
|
||||||
className="w-24 bg-surface-container-low rounded-xl border-none px-4 py-2.5 text-sm font-medium focus:ring-2 focus:ring-primary/10 outline-none"
|
className="w-24 bg-surface-container-low rounded-xl border-none px-4 py-2.5 text-sm font-medium focus:ring-2 focus:ring-primary/10 outline-none"
|
||||||
/>
|
/>
|
||||||
<select
|
<select
|
||||||
@ -407,6 +449,7 @@ export default function ProductSpecificationsPage() {
|
|||||||
warrantyInformation: { ...prev.warrantyInformation, durationType: e.target.value },
|
warrantyInformation: { ...prev.warrantyInformation, durationType: e.target.value },
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
disabled={!draft.warrantyInformation.enabled}
|
||||||
className="flex-1 bg-surface-container-low rounded-xl border-none px-4 py-2.5 text-sm font-semibold focus:ring-2 focus:ring-primary/10 outline-none"
|
className="flex-1 bg-surface-container-low rounded-xl border-none px-4 py-2.5 text-sm font-semibold focus:ring-2 focus:ring-primary/10 outline-none"
|
||||||
>
|
>
|
||||||
<option value="DAY">Days</option>
|
<option value="DAY">Days</option>
|
||||||
@ -416,6 +459,7 @@ export default function ProductSpecificationsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{warrantyError && <p className="mt-3 text-[11px] font-semibold text-error">{warrantyError}</p>}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* 5. International Export */}
|
{/* 5. International Export */}
|
||||||
@ -550,7 +594,7 @@ export default function ProductSpecificationsPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.push("/products/new/review")}
|
onClick={goToReview}
|
||||||
className="editorial-gradient text-white px-8 py-3.5 rounded-xl font-black text-sm uppercase tracking-[0.18em] shadow-lg shadow-primary/20 hover:-translate-y-0.5 transition-all flex items-center gap-2"
|
className="editorial-gradient text-white px-8 py-3.5 rounded-xl font-black text-sm uppercase tracking-[0.18em] shadow-lg shadow-primary/20 hover:-translate-y-0.5 transition-all flex items-center gap-2"
|
||||||
>
|
>
|
||||||
{s.next}
|
{s.next}
|
||||||
|
|||||||
@ -102,6 +102,7 @@ export interface ProductDraftState {
|
|||||||
fileId: string;
|
fileId: string;
|
||||||
};
|
};
|
||||||
warrantyInformation: {
|
warrantyInformation: {
|
||||||
|
enabled: boolean;
|
||||||
type: string;
|
type: string;
|
||||||
duration: string;
|
duration: string;
|
||||||
durationType: string;
|
durationType: string;
|
||||||
@ -151,6 +152,7 @@ const defaultDraft: ProductDraftState = {
|
|||||||
fileId: "",
|
fileId: "",
|
||||||
},
|
},
|
||||||
warrantyInformation: {
|
warrantyInformation: {
|
||||||
|
enabled: false,
|
||||||
type: "",
|
type: "",
|
||||||
duration: "",
|
duration: "",
|
||||||
durationType: "MONTH",
|
durationType: "MONTH",
|
||||||
@ -199,6 +201,10 @@ function normalizeDraft(stored: Partial<ProductDraftState>): ProductDraftState {
|
|||||||
features: Array.isArray(stored.features)
|
features: Array.isArray(stored.features)
|
||||||
? stored.features.filter(Boolean).slice(0, MAX_PRODUCT_FEATURES)
|
? stored.features.filter(Boolean).slice(0, MAX_PRODUCT_FEATURES)
|
||||||
: defaultDraft.features,
|
: defaultDraft.features,
|
||||||
|
warrantyInformation: {
|
||||||
|
...defaultDraft.warrantyInformation,
|
||||||
|
...stored.warrantyInformation,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -147,7 +147,13 @@ export function validateAddProductPayload(payload: Record<string, unknown>) {
|
|||||||
|
|
||||||
const warrantyInformation = payload.warrantyInformation as Record<string, unknown> | undefined;
|
const warrantyInformation = payload.warrantyInformation as Record<string, unknown> | undefined;
|
||||||
pushMaxLength(issues, warrantyInformation?.type, 50, "Tipe garansi");
|
pushMaxLength(issues, warrantyInformation?.type, 50, "Tipe garansi");
|
||||||
if (text(warrantyInformation?.type) || numberValue(warrantyInformation?.duration) > 0) {
|
const hasWarranty =
|
||||||
|
Boolean(text(warrantyInformation?.type).trim()) ||
|
||||||
|
(warrantyInformation?.duration !== null && warrantyInformation?.duration !== undefined);
|
||||||
|
if (warrantyInformation && hasWarranty) {
|
||||||
|
if (!text(warrantyInformation.type).trim()) {
|
||||||
|
issues.push("Tipe garansi wajib diisi jika garansi aktif.");
|
||||||
|
}
|
||||||
pushMinNumber(issues, warrantyInformation?.duration, 1, "Durasi garansi");
|
pushMinNumber(issues, warrantyInformation?.duration, 1, "Durasi garansi");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -21,7 +21,7 @@ function toNumber(value: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function buildProductPayload(draft: ProductDraftState, state: "DRAFT" | "REVIEW") {
|
export function buildProductPayload(draft: ProductDraftState, state: "DRAFT" | "REVIEW") {
|
||||||
return {
|
const payload = {
|
||||||
subCategory: draft.subCategoryId ? { id: draft.subCategoryId } : undefined,
|
subCategory: draft.subCategoryId ? { id: draft.subCategoryId } : undefined,
|
||||||
name: draft.name,
|
name: draft.name,
|
||||||
description: draft.description,
|
description: draft.description,
|
||||||
@ -102,11 +102,25 @@ export function buildProductPayload(draft: ProductDraftState, state: "DRAFT" | "
|
|||||||
),
|
),
|
||||||
complianceInformation: { ...draft.complianceInformation },
|
complianceInformation: { ...draft.complianceInformation },
|
||||||
warrantyInformation: {
|
warrantyInformation: {
|
||||||
...draft.warrantyInformation,
|
type: null,
|
||||||
duration: toNumber(draft.warrantyInformation.duration),
|
duration: null,
|
||||||
|
durationType: "MONTH",
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (draft.warrantyInformation.enabled) {
|
||||||
|
return {
|
||||||
|
...payload,
|
||||||
|
warrantyInformation: {
|
||||||
|
type: draft.warrantyInformation.type,
|
||||||
|
duration: toNumber(draft.warrantyInformation.duration),
|
||||||
|
durationType: draft.warrantyInformation.durationType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useProductSubmit() {
|
export function useProductSubmit() {
|
||||||
|
|||||||
Reference in New Issue
Block a user