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 ? (
-

-
+
+

+
+
Signed in as
{customer.displayName}
+
+ My Profile
+
@@ -212,17 +220,18 @@ export default function Navbar({ categories = [] }: NavbarProps) {
)}
-
+
{/* ── MOBILE RIGHT ACTIONS ── */}
{customer ? (
-

setMobileOpen(true)}
- className="w-7 h-7 rounded-full border-2 border-spa-forest/30 object-cover cursor-pointer"
- />
+
+

+
) : (
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 */
+
+
+
+
+
+
เข้าสู่ระบบสมาชิก
+
ล็อกอินเพื่อตรวจสอบประวัติคำสั่งซื้อ สะสมคะแนนสมาชิก และบันทึกสมุดที่อยู่จัดส่งสินค้า
+
+
{
+ const liff = (window as any).liff
+ if (liff) {
+ liff.login({ redirectUri: window.location.href })
+ } else {
+ alert('LINE LIFF System is initializing. Please refresh.')
+ }
+ }}
+ className="w-full py-4 bg-spa-forest hover:bg-spa-stone text-white font-bold rounded-xl tracking-widest uppercase transition-all duration-300 shadow-sm flex items-center justify-center gap-3"
+ >
+
+ เข้าสู่ระบบผ่าน LINE
+
+
กลับไปเลือกซื้อสินค้า
+
+ ) : (
+ /* Logged In Dashboard Layout */
+
+ {/* Header Profile Banner */}
+
+
+
+ {/* User Avatar & Info */}
+
+
+

+
+
+
{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 */}
+
+
setActiveTab('orders')}
+ className={`w-full py-3 px-4 rounded-xl font-bold text-left text-sm flex items-center gap-3 transition-all ${
+ activeTab === 'orders'
+ ? 'bg-spa-forest text-white shadow-sm'
+ : 'text-spa-stone hover:bg-spa-light hover:text-pop-black'
+ }`}
+ >
+
+ ประวัติการสั่งซื้อ
+
+ {orders.length}
+
+
+
+
setActiveTab('addresses')}
+ className={`w-full py-3 px-4 rounded-xl font-bold text-left text-sm flex items-center gap-3 transition-all ${
+ activeTab === 'addresses'
+ ? 'bg-spa-forest text-white shadow-sm'
+ : 'text-spa-stone hover:bg-spa-light hover:text-pop-black'
+ }`}
+ >
+
+ สมุดที่อยู่จัดส่ง
+
+ {addresses.length}
+
+
+
+
setActiveTab('settings')}
+ className={`w-full py-3 px-4 rounded-xl font-bold text-left text-sm flex items-center gap-3 transition-all ${
+ activeTab === 'settings'
+ ? 'bg-spa-forest text-white shadow-sm'
+ : 'text-spa-stone hover:bg-spa-light hover:text-pop-black'
+ }`}
+ >
+
+ ข้อมูลติดต่อของฉัน
+
+
+
+ {/* 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 => (
+
+
+

+
+
+
{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 && (
+
{
+ resetAddressForm()
+ setShowAddressForm(true)
+ }}
+ className="px-4 py-2 bg-spa-forest hover:bg-spa-stone text-white font-bold rounded-lg text-xs uppercase tracking-wider transition-colors shadow-sm flex items-center gap-2"
+ >
+
+ เพิ่มที่อยู่ใหม่
+
+ )}
+
+
+ {/* Address Creation/Editing Form */}
+ {showAddressForm && (
+
+ )}
+
+ {loadingAddresses ? (
+
+
+
+ ) : addresses.length === 0 ? (
+
+
+
+
ไม่มีข้อมูลที่อยู่จัดส่ง
+
กรุณากดปุ่มเพิ่มที่อยู่ด้านบนเพื่อบันทึกสมุดที่อยู่จัดส่งสินค้าของคุณ
+
+
+ ) : (
+
+ {addresses.map(addr => (
+
+
+
+
+
{addr.recipientName}
+ {addr.isDefault && (
+
+ เริ่มต้น
+
+ )}
+
+
+
+ {addr.phone}
+
+
+
+
+ startEditAddress(addr)}
+ className="p-1.5 hover:bg-spa-light rounded-lg border border-spa-stone/10 transition-colors"
+ title="แก้ไขที่อยู่"
+ >
+
+
+ handleDeleteAddress(addr.id)}
+ className="p-1.5 hover:bg-rose-500/10 rounded-lg border border-rose-500/10 transition-colors"
+ title="ลบที่อยู่"
+ >
+
+
+
+
+
+
+ {addr.addressLine} ต.{addr.subdistrict} อ.{addr.district} จ.{addr.province} {addr.zipcode}
+
+
+ {!addr.isDefault && (
+
handleSetDefault(addr.id)}
+ className="w-full py-1.5 border border-spa-stone/20 hover:bg-spa-light text-[10px] font-bold uppercase tracking-wider text-spa-stone rounded-lg transition-colors"
+ >
+ ตั้งเป็นที่อยู่เริ่มต้น
+
+ )}
+
+ ))}
+
+ )}
+
+ )}
+
+ {/* 3. SETTINGS TAB */}
+ {activeTab === 'settings' && (
+
+
+
+ ข้อมูลติดต่อสมาชิก
+
+
+
+
+ )}
+
+
+
+
+
+ )}
+
+
+
+
+
+ )
+}
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
+}
+
+