diff --git a/apps/backend/prisma/migrations/20260607114437_add_customer_address/migration.sql b/apps/backend/prisma/migrations/20260607114437_add_customer_address/migration.sql new file mode 100644 index 0000000..59b2ab9 --- /dev/null +++ b/apps/backend/prisma/migrations/20260607114437_add_customer_address/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "CustomerAddress" ( + "id" SERIAL NOT NULL, + "customerId" INTEGER NOT NULL, + "recipientName" TEXT NOT NULL, + "phone" TEXT NOT NULL, + "addressLine" TEXT NOT NULL, + "subdistrict" TEXT NOT NULL, + "district" TEXT NOT NULL, + "province" TEXT NOT NULL, + "zipcode" TEXT NOT NULL, + "isDefault" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "CustomerAddress_pkey" PRIMARY KEY ("id") +); + +-- AddForeignKey +ALTER TABLE "CustomerAddress" ADD CONSTRAINT "CustomerAddress_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "Customer"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/backend/prisma/schema.prisma b/apps/backend/prisma/schema.prisma index d7beece..c632f0c 100644 --- a/apps/backend/prisma/schema.prisma +++ b/apps/backend/prisma/schema.prisma @@ -76,17 +76,34 @@ model User { } model Customer { - id Int @id @default(autoincrement()) - lineUserId String @unique + id Int @id @default(autoincrement()) + lineUserId String @unique displayName String? pictureUrl String? email String? phone String? - points Int @default(0) + points Int @default(0) orders Order[] reviews Review[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + addresses CustomerAddress[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model CustomerAddress { + id Int @id @default(autoincrement()) + customerId Int + customer Customer @relation(fields: [customerId], references: [id], onDelete: Cascade) + recipientName String + phone String + addressLine String // บ้านเลขที่ หมู่บ้าน ถนน ซอย + subdistrict String // แขวง/ตำบล + district String // เขต/อำเภอ + province String // จังหวัด + zipcode String // รหัสไปรษณีย์ + isDefault Boolean @default(false) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Hero { diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index 408c702..4312425 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -22,6 +22,7 @@ import { reviewsRoute } from "./routes/reviews"; import { promotionsRoute } from "./routes/promotions"; import { pagesRoute } from "./routes/pages"; import { paymentsRoute } from "./routes/payments"; +import { profileRoute } from "./routes/profile"; const PORT = process.env.PORT ? parseInt(process.env.PORT) : 3000; const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173"; const JWT_SECRET = process.env.JWT_SECRET || "homaroma_secret_2026_scent"; @@ -85,7 +86,7 @@ const app = new Elysia() return file; }) .group("/api", (app) => - app.use(authRoute).use(categoriesRoute).use(productsRoute).use(uploadRoute).use(heroRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute).use(paymentsRoute) + app.use(authRoute).use(categoriesRoute).use(productsRoute).use(uploadRoute).use(heroRoute).use(ordersRoute).use(settingsRoute).use(articlesRoute).use(tiktokRoute).use(rawMaterialsRoute).use(shopeeRoute).use(tiktokOrderRoute).use(lazadaOrderRoute).use(customersRoute).use(couponsRoute).use(reviewsRoute).use(promotionsRoute).use(pagesRoute).use(paymentsRoute).use(profileRoute) ) .listen(PORT); diff --git a/apps/backend/src/routes/profile.ts b/apps/backend/src/routes/profile.ts new file mode 100644 index 0000000..c660f8a --- /dev/null +++ b/apps/backend/src/routes/profile.ts @@ -0,0 +1,162 @@ +import { Elysia, t } from "elysia"; +import prisma from "../lib/prisma"; + +export const profileRoute = new Elysia({ prefix: "/profile" }) + .get("/:customerId", async ({ params: { customerId }, set }) => { + const customer = await prisma.customer.findUnique({ + where: { id: Number(customerId) }, + include: { addresses: true } + }); + if (!customer) { + set.status = 404; + return { error: "Customer not found" }; + } + return customer; + }) + .put("/:customerId", async ({ params: { customerId }, body, set }) => { + try { + const { email, phone } = body as any; + const customer = await prisma.customer.update({ + where: { id: Number(customerId) }, + data: { + email: email || null, + phone: phone || null + } + }); + return customer; + } catch (err: any) { + set.status = 500; + return { error: err.message || "Failed to update customer profile" }; + } + }, { + body: t.Object({ + email: t.Optional(t.Nullable(t.String())), + phone: t.Optional(t.Nullable(t.String())) + }) + }) + .get("/:customerId/addresses", async ({ params: { customerId } }) => { + return await prisma.customerAddress.findMany({ + where: { customerId: Number(customerId) }, + orderBy: { isDefault: 'desc' } + }); + }) + .post("/:customerId/addresses", async ({ params: { customerId }, body, set }) => { + try { + const { recipientName, phone, addressLine, subdistrict, district, province, zipcode, isDefault } = body as any; + const cId = Number(customerId); + + // If isDefault is true, unset other defaults first + if (isDefault) { + await prisma.customerAddress.updateMany({ + where: { customerId: cId }, + data: { isDefault: false } + }); + } + + const address = await prisma.customerAddress.create({ + data: { + customerId: cId, + recipientName, + phone, + addressLine, + subdistrict, + district, + province, + zipcode, + isDefault: !!isDefault + } + }); + return address; + } catch (err: any) { + set.status = 500; + return { error: err.message || "Failed to create address" }; + } + }, { + body: t.Object({ + recipientName: t.String(), + phone: t.String(), + addressLine: t.String(), + subdistrict: t.String(), + district: t.String(), + province: t.String(), + zipcode: t.String(), + isDefault: t.Boolean() + }) + }) + .put("/:customerId/addresses/:addressId", async ({ params: { customerId, addressId }, body, set }) => { + try { + const { recipientName, phone, addressLine, subdistrict, district, province, zipcode, isDefault } = body as any; + const cId = Number(customerId); + const aId = Number(addressId); + + // If setting as default, unset other defaults first + if (isDefault) { + await prisma.customerAddress.updateMany({ + where: { customerId: cId }, + data: { isDefault: false } + }); + } + + const address = await prisma.customerAddress.update({ + where: { id: aId }, + data: { + recipientName, + phone, + addressLine, + subdistrict, + district, + province, + zipcode, + isDefault: !!isDefault + } + }); + return address; + } catch (err: any) { + set.status = 500; + return { error: err.message || "Failed to update address" }; + } + }, { + body: t.Object({ + recipientName: t.String(), + phone: t.String(), + addressLine: t.String(), + subdistrict: t.String(), + district: t.String(), + province: t.String(), + zipcode: t.String(), + isDefault: t.Boolean() + }) + }) + .delete("/:customerId/addresses/:addressId", async ({ params: { addressId }, set }) => { + try { + await prisma.customerAddress.delete({ + where: { id: Number(addressId) } + }); + return { success: true }; + } catch (err: any) { + set.status = 500; + return { error: err.message || "Failed to delete address" }; + } + }) + .patch("/:customerId/addresses/:addressId/default", async ({ params: { customerId, addressId }, set }) => { + try { + const cId = Number(customerId); + const aId = Number(addressId); + + // Unset other defaults + await prisma.customerAddress.updateMany({ + where: { customerId: cId }, + data: { isDefault: false } + }); + + // Set this one as default + const address = await prisma.customerAddress.update({ + where: { id: aId }, + data: { isDefault: true } + }); + return address; + } catch (err: any) { + set.status = 500; + return { error: err.message || "Failed to set default address" }; + } + }); diff --git a/apps/spa/src/App.tsx b/apps/spa/src/App.tsx index 9be26cb..85fce9f 100644 --- a/apps/spa/src/App.tsx +++ b/apps/spa/src/App.tsx @@ -19,6 +19,7 @@ import OrderSuccessPage from './pages/OrderSuccessPage' import TrackOrderPage from './pages/TrackOrderPage' import WishlistPage from './pages/WishlistPage' import CustomPage from './pages/CustomPage' +import ProfilePage from './pages/ProfilePage' import CartDrawer from './components/CartDrawer' function App() { @@ -52,6 +53,7 @@ function App() { } /> } /> } /> + } /> } /> diff --git a/apps/spa/src/components/Navbar.tsx b/apps/spa/src/components/Navbar.tsx index c76eb15..c8d8397 100644 --- a/apps/spa/src/components/Navbar.tsx +++ b/apps/spa/src/components/Navbar.tsx @@ -183,20 +183,28 @@ export default function Navbar({ categories = [] }: NavbarProps) { {customer ? (
- Profile -
+ + Profile + +

Signed in as

{customer.displayName}

+ + My Profile + @@ -212,17 +220,18 @@ export default function Navbar({ categories = [] }: NavbarProps) { )}
- + {/* ── MOBILE RIGHT ACTIONS ── */}
{customer ? (
- Profile setMobileOpen(true)} - className="w-7 h-7 rounded-full border-2 border-spa-forest/30 object-cover cursor-pointer" - /> + + Profile +
) : ( + ) + })} +
+
+ )}
diff --git a/apps/spa/src/pages/ProfilePage.tsx b/apps/spa/src/pages/ProfilePage.tsx new file mode 100644 index 0000000..f437e79 --- /dev/null +++ b/apps/spa/src/pages/ProfilePage.tsx @@ -0,0 +1,800 @@ +import { useState, useEffect } from 'react' +import { Link } from 'react-router-dom' +import Navbar from '../components/Navbar' +import Footer from '../components/Footer' +import { API_BASE, formatPrice, type Category, type Order, type CustomerAddress } from '../types' +import { useAuthStore } from '../store/authStore' +import { + User, + MapPin, + Package, + Settings, + Plus, + Edit3, + Trash2, + Check, + Loader2, + ExternalLink, + Phone, + Mail, + Award +} from 'lucide-react' + +export default function ProfilePage() { + const { token, customer, setAuth } = useAuthStore() + const [categories, setCategories] = useState([]) + + // Tab State + const [activeTab, setActiveTab] = useState<'orders' | 'addresses' | 'settings'>('orders') + + // Data States + const [dbCustomer, setDbCustomer] = useState(null) + const [orders, setOrders] = useState([]) + const [addresses, setAddresses] = useState([]) + + // Loading States + const [loadingOrders, setLoadingOrders] = useState(false) + const [loadingAddresses, setLoadingAddresses] = useState(false) + + // Address Form States + const [showAddressForm, setShowAddressForm] = useState(false) + const [editingAddress, setEditingAddress] = useState(null) + + const [recipientName, setRecipientName] = useState('') + const [phone, setPhone] = useState('') + const [addressLine, setAddressLine] = useState('') + const [subdistrict, setSubdistrict] = useState('') + const [district, setDistrict] = useState('') + const [province, setProvince] = useState('') + const [zipcode, setZipcode] = useState('') + const [isDefault, setIsDefault] = useState(false) + const [submittingAddress, setSubmittingAddress] = useState(false) + + // Settings Form States + const [emailInput, setEmailInput] = useState('') + const [phoneInput, setPhoneInput] = useState('') + const [submittingSettings, setSubmittingSettings] = useState(false) + const [settingsSuccess, setSettingsSuccess] = useState(false) + + // Fetch Categories + useEffect(() => { + fetch(`${API_BASE}/categories`) + .then(res => res.json()) + .then(setCategories) + .catch(console.error) + }, []) + + // Fetch Customer Profile details & Points dynamically + const fetchProfile = () => { + if (!customer) return + fetch(`${API_BASE}/profile/${customer.id}`) + .then(res => res.json()) + .then(data => { + if (data && !data.error) { + setDbCustomer(data) + setEmailInput(data.email || '') + setPhoneInput(data.phone || '') + } + }) + .catch(console.error) + } + + // Fetch Orders + const fetchOrders = () => { + if (!customer) return + setLoadingOrders(true) + fetch(`${API_BASE}/orders/customer/${customer.id}`) + .then(res => res.json()) + .then(data => { + if (Array.isArray(data)) { + setOrders(data) + } + }) + .catch(console.error) + .finally(() => setLoadingOrders(false)) + } + + // Fetch Addresses + const fetchAddresses = () => { + if (!customer) return + setLoadingAddresses(true) + fetch(`${API_BASE}/profile/${customer.id}/addresses`) + .then(res => res.json()) + .then(data => { + if (Array.isArray(data)) { + setAddresses(data) + } + }) + .catch(console.error) + .finally(() => setLoadingAddresses(false)) + } + + useEffect(() => { + if (customer) { + fetchProfile() + fetchOrders() + fetchAddresses() + } + }, [customer]) + + // Reset Address Form + const resetAddressForm = () => { + setRecipientName('') + setPhone('') + setAddressLine('') + setSubdistrict('') + setDistrict('') + setProvince('') + setZipcode('') + setIsDefault(false) + setEditingAddress(null) + setShowAddressForm(false) + } + + // Handle Edit Address Click + const startEditAddress = (addr: CustomerAddress) => { + setEditingAddress(addr) + setRecipientName(addr.recipientName) + setPhone(addr.phone) + setAddressLine(addr.addressLine) + setSubdistrict(addr.subdistrict) + setDistrict(addr.district) + setProvince(addr.province) + setZipcode(addr.zipcode) + setIsDefault(addr.isDefault) + setShowAddressForm(true) + } + + // Handle Add/Edit Address Submission + const handleAddressSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!customer) return + + setSubmittingAddress(true) + const payload = { + recipientName, + phone, + addressLine, + subdistrict, + district, + province, + zipcode, + isDefault + } + + try { + const url = editingAddress + ? `${API_BASE}/profile/${customer.id}/addresses/${editingAddress.id}` + : `${API_BASE}/profile/${customer.id}/addresses` + const method = editingAddress ? 'PUT' : 'POST' + + const res = await fetch(url, { + method, + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }) + + if (res.ok) { + resetAddressForm() + fetchAddresses() + } else { + const err = await res.json() + alert(err.error || 'Failed to save address') + } + } catch (err) { + console.error(err) + alert('Failed to connect to server') + } finally { + setSubmittingAddress(false) + } + } + + // Handle Delete Address + const handleDeleteAddress = async (addrId: number) => { + if (!customer) return + if (!confirm('ยืนยันที่จะลบที่อยู่จัดส่งนี้?')) return + + try { + const res = await fetch(`${API_BASE}/profile/${customer.id}/addresses/${addrId}`, { + method: 'DELETE' + }) + if (res.ok) { + fetchAddresses() + } else { + alert('Failed to delete address') + } + } catch (err) { + console.error(err) + } + } + + // Handle Set Default Address + const handleSetDefault = async (addrId: number) => { + if (!customer) return + try { + const res = await fetch(`${API_BASE}/profile/${customer.id}/addresses/${addrId}/default`, { + method: 'PATCH' + }) + if (res.ok) { + fetchAddresses() + } + } catch (err) { + console.error(err) + } + } + + // Handle Settings Submit (Update email & phone) + const handleSettingsSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!customer) return + + setSubmittingSettings(true) + setSettingsSuccess(false) + + try { + const res = await fetch(`${API_BASE}/profile/${customer.id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: emailInput || null, + phone: phoneInput || null + }) + }) + + if (res.ok) { + const updatedCustomer = await res.json() + setDbCustomer(updatedCustomer) + // Update store + if (token) { + setAuth(token, updatedCustomer) + } + setSettingsSuccess(true) + setTimeout(() => setSettingsSuccess(false), 3000) + } else { + const err = await res.json() + alert(err.error || 'Failed to update profile') + } + } catch (err) { + console.error(err) + alert('Failed to connect to server') + } finally { + setSubmittingSettings(false) + } + } + + const getOrderStatusColor = (status: string) => { + switch (status.toUpperCase()) { + case 'PAID': + return 'bg-emerald-500/10 text-emerald-600 border-emerald-500/20' + case 'PENDING_PAYMENT': + return 'bg-amber-500/10 text-amber-600 border-amber-500/20' + case 'SHIPPED': + return 'bg-blue-500/10 text-blue-600 border-blue-500/20' + case 'CANCELLED': + return 'bg-rose-500/10 text-rose-600 border-rose-500/20' + default: + return 'bg-spa-stone/10 text-spa-stone border-spa-stone/20' + } + } + + const translateStatus = (status: string) => { + switch (status.toUpperCase()) { + case 'PAID': + return 'ชำระเงินสำเร็จ' + case 'PENDING_PAYMENT': + return 'รอการชำระเงิน' + case 'SHIPPED': + return 'จัดส่งแล้ว' + case 'CANCELLED': + return 'ยกเลิกแล้ว' + default: + return status + } + } + + return ( +
+ + +
+ + {!customer ? ( + /* Login CTA screen */ +
+
+ +
+
+

เข้าสู่ระบบสมาชิก

+

ล็อกอินเพื่อตรวจสอบประวัติคำสั่งซื้อ สะสมคะแนนสมาชิก และบันทึกสมุดที่อยู่จัดส่งสินค้า

+
+ + กลับไปเลือกซื้อสินค้า +
+ ) : ( + /* Logged In Dashboard Layout */ +
+ {/* Header Profile Banner */} +
+
+ + {/* User Avatar & Info */} +
+
+ LINE Profile +
+
+

{customer.displayName}

+
+ {dbCustomer?.phone && ( + + + {dbCustomer.phone} + + )} + {dbCustomer?.email && ( + + + {dbCustomer.email} + + )} +
+
+
+ + {/* Point Cards */} +
+
+

คะแนนสะสมของคุณ

+
+ {dbCustomer?.points ?? customer.points ?? 0} +
+

1 คะแนนมีค่าเท่ากับ ฿1 สำหรับใช้เป็นส่วนลด

+
+
+ + {/* Dashboard Tabs & Content */} +
+ + {/* Vertical Menu Tabs */} +
+ + + + + +
+ + {/* Tab Display Area */} +
+ + {/* 1. ORDERS TAB */} + {activeTab === 'orders' && ( +
+

+ + ประวัติคำสั่งซื้อทั้งหมด +

+ + {loadingOrders ? ( +
+ +
+ ) : orders.length === 0 ? ( +
+ +
+

ไม่มีประวัติการสั่งซื้อ

+

คุณยังไม่ได้ทำการสั่งซื้อสินค้าใดๆ กับแบรนด์ของเรา

+
+ + ไปหน้าช้อปปิ้ง + +
+ ) : ( +
+ {orders.map(order => ( +
+ {/* Card Header */} +
+
+

เลขออเดอร์ #{order.id.toString().padStart(4, '0')}

+

สั่งเมื่อ {new Date(order.createdAt).toLocaleDateString('th-TH', { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' })}

+
+
+ + {translateStatus(order.status)} + + + + +
+
+ + {/* Card Items */} +
+ {order.items.map(item => ( +
+
+ Product +
+
+

{item.productName}

+

{item.variantSize} x {item.quantity}

+
+

{formatPrice(item.priceAtPurchase * item.quantity)}

+
+ ))} +
+ + {/* Card Footer */} +
+
+ {order.trackingNo && ( +

+ เลขแทรกพัสดุ: {order.trackingNo} +

+ )} +
+
+ ยอดรวมสุทธิ: + {formatPrice(order.totalAmount)} +
+
+
+ ))} +
+ )} +
+ )} + + {/* 2. ADDRESSES TAB */} + {activeTab === 'addresses' && ( +
+
+

+ + สมุดที่อยู่จัดส่งของฉัน +

+ {!showAddressForm && ( + + )} +
+ + {/* Address Creation/Editing Form */} + {showAddressForm && ( +
+

+ {editingAddress ? 'แก้ไขที่อยู่จัดส่ง' : 'เพิ่มที่อยู่จัดส่งใหม่'} +

+ +
+
+ + setRecipientName(e.target.value)} + className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all" + placeholder="เช่น สมชาย ใจดี" + /> +
+ +
+ + setPhone(e.target.value)} + className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all" + placeholder="เช่น 0812345678" + /> +
+
+ +
+ + setAddressLine(e.target.value)} + className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all" + placeholder="เช่น 99/9 หมู่ 9 ซอยสุขุมวิท 23" + /> +
+ +
+
+ + setSubdistrict(e.target.value)} + className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all" + /> +
+ +
+ + setDistrict(e.target.value)} + className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all" + /> +
+ +
+ + setProvince(e.target.value)} + className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all" + /> +
+ +
+ + setZipcode(e.target.value.replace(/\D/g, ''))} + className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-2.5 text-xs text-pop-black focus:border-spa-forest outline-none transition-all" + /> +
+
+ +
+ setIsDefault(e.target.checked)} + className="w-4 h-4 text-spa-forest focus:ring-spa-forest border-spa-stone/30 rounded cursor-pointer" + /> + +
+ +
+ + +
+
+ )} + + {loadingAddresses ? ( +
+ +
+ ) : addresses.length === 0 ? ( +
+ +
+

ไม่มีข้อมูลที่อยู่จัดส่ง

+

กรุณากดปุ่มเพิ่มที่อยู่ด้านบนเพื่อบันทึกสมุดที่อยู่จัดส่งสินค้าของคุณ

+
+
+ ) : ( +
+ {addresses.map(addr => ( +
+
+
+
+

{addr.recipientName}

+ {addr.isDefault && ( + + เริ่มต้น + + )} +
+

+ + {addr.phone} +

+
+ +
+ + +
+
+ +

+ {addr.addressLine} ต.{addr.subdistrict} อ.{addr.district} จ.{addr.province} {addr.zipcode} +

+ + {!addr.isDefault && ( + + )} +
+ ))} +
+ )} +
+ )} + + {/* 3. SETTINGS TAB */} + {activeTab === 'settings' && ( +
+

+ + ข้อมูลติดต่อสมาชิก +

+ +
+ {settingsSuccess && ( +
+ + บันทึกข้อมูลติดต่อของคุณสำเร็จแล้ว +
+ )} + +
+ + setEmailInput(e.target.value)} + className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3 text-xs text-pop-black focus:border-spa-forest outline-none transition-all placeholder:text-spa-stone/30" + placeholder="เช่น example@email.com" + /> +
+ +
+ + setPhoneInput(e.target.value)} + className="w-full bg-spa-light border border-spa-stone/20 rounded-lg p-3 text-xs text-pop-black focus:border-spa-forest outline-none transition-all placeholder:text-spa-stone/30" + placeholder="เช่น 0812345678" + /> +
+ +
+ +
+
+
+ )} + +
+ +
+
+ )} + +
+ +
+ ) +} diff --git a/packages/types/index.ts b/packages/types/index.ts index 573443c..213d3ce 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -132,6 +132,7 @@ export interface Order { shippingFee: number status: string paymentSlipUrl?: string | null + trackingNo?: string | null customerId?: number | null customer?: any | null items: OrderItem[] @@ -261,3 +262,19 @@ export interface EditorialCarouselSectionPayload { }[] } +export interface CustomerAddress { + id: number + customerId: number + recipientName: string + phone: string + addressLine: string + subdistrict: string + district: string + province: string + zipcode: string + isDefault: boolean + createdAt?: string + updatedAt?: string +} + +