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`
|
||||
|
||||
### 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
|
||||
|
||||
Files:
|
||||
|
||||
@ -171,6 +171,7 @@ export default function ProductDetailsPage() {
|
||||
const { submit, submitting } = useProductSubmit();
|
||||
const { t } = useLanguage();
|
||||
const d = t.dashboard.productNew.details;
|
||||
const [preOrderError, setPreOrderError] = useState("");
|
||||
const [keywordInput, setKeywordInput] = useState("");
|
||||
const keywordLimitReached = draft.keywords.filter(Boolean).length >= MAX_KEYWORDS;
|
||||
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 (
|
||||
<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">
|
||||
@ -318,14 +330,17 @@ export default function ProductDetailsPage() {
|
||||
</label>
|
||||
<input
|
||||
value={draft.preOrderDay}
|
||||
onChange={(e) => setDraft((prev) => ({ ...prev, preOrderDay: e.target.value }))}
|
||||
placeholder="14"
|
||||
onChange={(e) => {
|
||||
setPreOrderError("");
|
||||
setDraft((prev) => ({ ...prev, preOrderDay: e.target.value }));
|
||||
}}
|
||||
placeholder="0"
|
||||
type="number"
|
||||
min="1"
|
||||
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">
|
||||
Minimal 1 hari jika pre-order aktif.
|
||||
<p className={`mt-1.5 text-[11px] font-semibold ${preOrderError ? "text-error" : "text-on-surface-variant"}`}>
|
||||
{preOrderError || "Minimal 1 hari jika pre-order aktif."}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -517,7 +532,7 @@ export default function ProductDetailsPage() {
|
||||
</button>
|
||||
<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"
|
||||
>
|
||||
{d.next}
|
||||
|
||||
@ -448,7 +448,7 @@ export default function ProductReviewPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{(draft.warrantyInformation.type || draft.warrantyInformation.duration) && (
|
||||
{draft.warrantyInformation.enabled && (
|
||||
<div>
|
||||
<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} />
|
||||
|
||||
@ -70,6 +70,7 @@ export default function ProductSpecificationsPage() {
|
||||
const msdsInputRef = useRef<HTMLInputElement>(null);
|
||||
const docInputRef = useRef<HTMLInputElement>(null);
|
||||
const [msdsName, setMsdsName] = useState("");
|
||||
const [warrantyError, setWarrantyError] = useState("");
|
||||
|
||||
// --- productInformations helpers ---
|
||||
function getProductInfo(key: string) {
|
||||
@ -140,6 +141,22 @@ export default function ProductSpecificationsPage() {
|
||||
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 ?? [];
|
||||
|
||||
return (
|
||||
@ -157,7 +174,7 @@ export default function ProductSpecificationsPage() {
|
||||
<p>Safety warning maksimal 100 karakter.</p>
|
||||
<p>Country of origin 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>Upload dokumen maksimal 10 MB per file.</p>
|
||||
</div>
|
||||
@ -362,24 +379,47 @@ export default function ProductSpecificationsPage() {
|
||||
|
||||
{/* 4. Warranty */}
|
||||
<section className={sectionClass}>
|
||||
<div className="flex items-center gap-3 mb-5">
|
||||
<div className={sectionIconClass}>
|
||||
<span className="material-symbols-outlined">verified</span>
|
||||
<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}>
|
||||
<span className="material-symbols-outlined">verified</span>
|
||||
</div>
|
||||
<h3 className="text-sm font-black text-on-surface uppercase tracking-wider">{s.warranty}</h3>
|
||||
</div>
|
||||
<h3 className="text-sm font-black text-on-surface uppercase tracking-wider">{s.warranty}</h3>
|
||||
<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">
|
||||
<div className={`grid grid-cols-1 md:grid-cols-2 gap-6 ${draft.warrantyInformation.enabled ? "" : "opacity-50"}`}>
|
||||
<div>
|
||||
<label className={labelClass}>{s.warrantyType}</label>
|
||||
<input
|
||||
value={draft.warrantyInformation.type}
|
||||
onChange={(e) =>
|
||||
onChange={(e) => {
|
||||
setWarrantyError("");
|
||||
setDraft((prev) => ({
|
||||
...prev,
|
||||
warrantyInformation: { ...prev.warrantyInformation, type: e.target.value },
|
||||
}))
|
||||
}
|
||||
}));
|
||||
}}
|
||||
placeholder="e.g. Global Manufacturers Warranty"
|
||||
disabled={!draft.warrantyInformation.enabled}
|
||||
className={inputClass}
|
||||
/>
|
||||
</div>
|
||||
@ -388,15 +428,17 @@ export default function ProductSpecificationsPage() {
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
value={draft.warrantyInformation.duration}
|
||||
onChange={(e) =>
|
||||
onChange={(e) => {
|
||||
setWarrantyError("");
|
||||
setDraft((prev) => ({
|
||||
...prev,
|
||||
warrantyInformation: { ...prev.warrantyInformation, duration: e.target.value },
|
||||
}))
|
||||
}
|
||||
}));
|
||||
}}
|
||||
placeholder="0"
|
||||
type="number"
|
||||
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"
|
||||
/>
|
||||
<select
|
||||
@ -407,6 +449,7 @@ export default function ProductSpecificationsPage() {
|
||||
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"
|
||||
>
|
||||
<option value="DAY">Days</option>
|
||||
@ -416,6 +459,7 @@ export default function ProductSpecificationsPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{warrantyError && <p className="mt-3 text-[11px] font-semibold text-error">{warrantyError}</p>}
|
||||
</section>
|
||||
|
||||
{/* 5. International Export */}
|
||||
@ -550,7 +594,7 @@ export default function ProductSpecificationsPage() {
|
||||
</button>
|
||||
<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"
|
||||
>
|
||||
{s.next}
|
||||
|
||||
@ -102,6 +102,7 @@ export interface ProductDraftState {
|
||||
fileId: string;
|
||||
};
|
||||
warrantyInformation: {
|
||||
enabled: boolean;
|
||||
type: string;
|
||||
duration: string;
|
||||
durationType: string;
|
||||
@ -151,6 +152,7 @@ const defaultDraft: ProductDraftState = {
|
||||
fileId: "",
|
||||
},
|
||||
warrantyInformation: {
|
||||
enabled: false,
|
||||
type: "",
|
||||
duration: "",
|
||||
durationType: "MONTH",
|
||||
@ -199,6 +201,10 @@ function normalizeDraft(stored: Partial<ProductDraftState>): ProductDraftState {
|
||||
features: Array.isArray(stored.features)
|
||||
? stored.features.filter(Boolean).slice(0, MAX_PRODUCT_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;
|
||||
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");
|
||||
}
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ function toNumber(value: string) {
|
||||
}
|
||||
|
||||
export function buildProductPayload(draft: ProductDraftState, state: "DRAFT" | "REVIEW") {
|
||||
return {
|
||||
const payload = {
|
||||
subCategory: draft.subCategoryId ? { id: draft.subCategoryId } : undefined,
|
||||
name: draft.name,
|
||||
description: draft.description,
|
||||
@ -102,11 +102,25 @@ export function buildProductPayload(draft: ProductDraftState, state: "DRAFT" | "
|
||||
),
|
||||
complianceInformation: { ...draft.complianceInformation },
|
||||
warrantyInformation: {
|
||||
...draft.warrantyInformation,
|
||||
duration: toNumber(draft.warrantyInformation.duration),
|
||||
type: null,
|
||||
duration: null,
|
||||
durationType: "MONTH",
|
||||
},
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user