From 24a2fd13697f2fd1098671676a56b7b1ae9250b0 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Thu, 16 Apr 2026 17:02:20 +0700 Subject: [PATCH] fix(web,prisma): fix TypeScript errors in transfer wizard and schema - Fix Zod v4 enum API: replace deprecated `required_error` with `error` - Create missing TransferWizardClient component (4-step wizard: category, items, AI estimate, submit) - Add CANCELLED status to TransferListingStatus enum for soft-delete support Co-Authored-By: Paperclip --- .../chuyen-nhuong/transfer-wizard-client.tsx | 337 ++++++++++++++++++ apps/web/lib/validations/transfer.ts | 76 ++++ prisma/schema.prisma | 1 + 3 files changed, 414 insertions(+) create mode 100644 apps/web/components/chuyen-nhuong/transfer-wizard-client.tsx create mode 100644 apps/web/lib/validations/transfer.ts diff --git a/apps/web/components/chuyen-nhuong/transfer-wizard-client.tsx b/apps/web/components/chuyen-nhuong/transfer-wizard-client.tsx new file mode 100644 index 0000000..8a5b1e2 --- /dev/null +++ b/apps/web/components/chuyen-nhuong/transfer-wizard-client.tsx @@ -0,0 +1,337 @@ +'use client'; + +import { ChevronLeft, ChevronRight, Loader2, Send } from 'lucide-react'; +import { useRouter } from 'next/navigation'; +import * as React from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; +import { + type TransferCategory, + type TransferCondition, + CATEGORY_ICONS, + CATEGORY_LABELS, + CONDITION_LABELS, + transferApi, +} from '@/lib/chuyen-nhuong-api'; +import { cn } from '@/lib/utils'; +import { + type TransferItemDraft, + useTransferWizardStore, +} from '@/lib/transfer-wizard-store'; + +const STEPS = [ + 'Danh mục', + 'Sản phẩm', + 'Định giá AI', + 'Thông tin & Gửi', +] as const; + +// ─── Step 1: Category Selection ──────────────────────── + +function StepCategory() { + const { category, setCategory, setStep } = useTransferWizardStore(); + const categories = Object.entries(CATEGORY_LABELS) as [TransferCategory, string][]; + + return ( +
+

Chọn danh mục chuyển nhượng:

+
+ {categories.map(([value, label]) => ( + + ))} +
+
+ ); +} + +// ─── Step 2: Items ───────────────────────────────────── + +function StepItems() { + const { items, addItem, removeItem, category } = useTransferWizardStore(); + const [name, setName] = React.useState(''); + const [condition, setCondition] = React.useState('GOOD'); + const [askingPrice, setAskingPrice] = React.useState(''); + + const handleAdd = () => { + if (!name || !askingPrice || !category) return; + addItem({ + name, + condition, + askingPriceVND: Number(askingPrice), + quantity: 1, + }); + setName(''); + setAskingPrice(''); + }; + + return ( +
+
+
+ + setName(e.target.value)} placeholder="VD: Bàn làm việc" /> +
+
+ + +
+
+ + setAskingPrice(e.target.value)} placeholder="0" /> +
+
+ + + {items.length > 0 && ( +
+ {items.map((item) => ( +
+ {item.name} — {CONDITION_LABELS[item.condition]} +
+ {new Intl.NumberFormat('vi-VN').format(item.askingPriceVND)} VND + +
+
+ ))} +
+ )} +
+ ); +} + +// ─── Step 3: AI Estimate ─────────────────────────────── + +function StepAiEstimate() { + const { items, category, aiEstimate, isEstimating, setAiEstimate, setIsEstimating } = useTransferWizardStore(); + + const handleEstimate = async () => { + if (!category || items.length === 0) return; + setIsEstimating(true); + try { + const payload = items.map((item) => ({ + category, + condition: item.condition, + originalPriceVND: item.askingPriceVND, + purchaseYear: new Date().getFullYear(), + })); + const result = await transferApi.estimate(payload); + setAiEstimate(result); + } catch { + setAiEstimate(null); + } finally { + setIsEstimating(false); + } + }; + + return ( +
+

+ Sử dụng AI để ước tính giá trị chuyển nhượng dựa trên danh mục, tình trạng và giá gốc. +

+ + {aiEstimate && ( + + +

+ Tổng ước tính: {new Intl.NumberFormat('vi-VN').format(Number(aiEstimate.totalEstimateVND))} VND +

+

+ Độ tin cậy: {Math.round(aiEstimate.avgConfidence * 100)}% +

+
+
+ )} +
+ ); +} + +// ─── Step 4: Details & Submit ────────────────────────── + +function StepDetails() { + const store = useTransferWizardStore(); + const router = useRouter(); + const [submitting, setSubmitting] = React.useState(false); + + const handleSubmit = async () => { + if (!store.category) return; + setSubmitting(true); + try { + const payload = { + category: store.category, + title: store.title, + description: store.description || undefined, + address: store.address, + district: store.district, + city: store.city, + latitude: 0, + longitude: 0, + askingPriceVND: store.askingPriceVND || store.items.reduce((sum, i) => sum + i.askingPriceVND * i.quantity, 0), + pricingSource: store.pricingSource, + isNegotiable: store.isNegotiable, + items: store.items.map((i) => ({ + name: i.name, + brand: i.brand, + modelName: i.modelName, + category: store.category!, + condition: i.condition, + purchaseYear: i.purchaseYear, + originalPriceVND: i.askingPriceVND, + askingPriceVND: i.askingPriceVND, + quantity: i.quantity, + notes: i.notes, + })), + }; + await transferApi.create(payload); + store.reset(); + router.push('/chuyen-nhuong'); + } catch { + // Error handling deferred to toast integration + } finally { + setSubmitting(false); + } + }; + + return ( +
+
+
+ + store.setListingDetails({ title: e.target.value })} + placeholder="VD: Chuyển nhượng bộ nội thất văn phòng" + /> +
+
+ +