// File: src/app/(protected)/orders/page.tsx "use client"; /** * EOP Orders — มุมมองเต็ม (Full) + ตัวกรองแบบ Select + Modal ปรับแต่งคอลัมน์ * - เฮดเดอร์ตารางแบบ 2 ชั้น (กลุ่มหัวข้อ ➜ หัวคอลัมน์ย่อย) ภาษาไทยทั้งหมด * - เลือกคอลัมน์ที่ต้องการแสดงได้จาก "ปรับแต่งตาราง" (จำค่าบน localStorage) * - แก้ Modal ให้เนื้อหาเลื่อน (scroll) ได้ และปุ่ม Apply ชิดด้านล่างเสมอ * - เพิ่มฟิลด์ "ผู้เปิดออเดอร์" (createdBy*) * - ตารางไม่ fix size โดยให้คอนเทนต์กำหนดความกว้างเอง และให้ทุกเซลล์แสดงบรรทัดเดียว (nowrap) * * หมายเหตุ: ใช้ mock แค่ไม่กี่เรคคอร์ดเพื่อโชว์ UX — ต่อ API จริงได้โดยแทนที่ MOCK_ORDERS */ import { useEffect, useMemo, useState, type ReactNode } from "react"; /* =========================== Types =========================== */ type Channel = "shopee" | "lazada" | "tiktok" | "d2c" | "chat"; type PaymentStatus = "paid" | "pending" | "failed" | "cod" | "refunded"; type FulfillmentStatus = | "unfulfilled" | "processing" | "shipped" | "delivered" | "returned" | "cancelled"; type Order = { // เอกลักษณ์ & ช่องทาง id: string; orderNo: string; orderRef: string; channel: Channel; channelOrderId: string; marketplaceShopId: string; marketplaceShopName: string; // ผู้เปิดออเดอร์ createdById: string; createdByName: string; createdByEmail?: string | null; // วัน–เวลา (ISO) dateCreated: string; datePaid?: string | null; datePacked?: string | null; dateShipped?: string | null; dateDelivered?: string | null; dateCancelled?: string | null; // ลูกค้า customerId: string; customerName: string; customerEmail?: string | null; customerPhone?: string | null; // ที่อยู่จัดส่ง shippingName: string; shippingPhone?: string | null; shippingAddressLine1: string; shippingAddressLine2?: string | null; shippingSubdistrict?: string | null; shippingDistrict: string; shippingProvince: string; shippingPostcode: string; shippingCountry: string; // ออกใบเสร็จ/ภาษี billingName: string; billingTaxId?: string | null; billingAddressLine1: string; billingSubdistrict?: string | null; billingDistrict: string; billingProvince: string; billingPostcode: string; billingCountry: string; // การชำระเงิน paymentMethod: "card" | "bank_qr" | "cod" | "wallet" | "transfer"; paymentStatus: PaymentStatus; paymentProvider?: string | null; paymentTxId?: string | null; paymentFeeTHB?: number | null; // ราคา/ต้นทุนย่อย currency: "THB"; exchangeRate: number; // 1 สำหรับ THB subtotalTHB: number; discountTHB: number; shippingFeeTHB: number; codFeeTHB: number; packagingFeeTHB: number; taxTHB: number; totalTHB: number; // สินค้า/โลจิสติกส์ itemsCount: number; totalWeightGrams: number; packageCount: number; dimensionsCmL?: number | null; dimensionsCmW?: number | null; dimensionsCmH?: number | null; // สถานะจัดส่ง fulfillStatus: FulfillmentStatus; shipBy?: string | null; carrier?: string | null; serviceLevel?: string | null; trackingNo?: string | null; // โกดัง/ปฏิบัติการ warehouseCode: "BKK-DC" | "CNX-HUB" | "HKT-MINI"; pickListId?: string | null; waveId?: string | null; binFrom?: string | null; binTo?: string | null; // ธง/โน้ต/ความเสี่ยง tags?: string[]; rmaFlag?: boolean; rmaReason?: string | null; noteSeller?: string | null; noteBuyer?: string | null; slaShipOnTime?: boolean; giftWrap?: boolean; fragile?: boolean; priorityLevel?: 0 | 1 | 2 | 3; riskScore?: number | null; holdReason?: string | null; fraudCheckStatus?: "clear" | "review" | "hold" | null; }; type ColumnKey = keyof Order; type Column = { key: ColumnKey; labelTH: string; // ป้ายหัวคอลัมน์ (ไทย) groupTH: string; // กลุ่มหัวข้อชั้นบน (ไทย) align?: "left" | "right"; format?: (v: Order[ColumnKey], row: Order) => ReactNode; }; /* =========================== Mock (ไม่กี่เรคคอร์ด) =========================== */ const MOCK_ORDERS: Order[] = [ { id: "1", orderNo: "SO-2025-000781", orderRef: "OREF-7A1C", channel: "shopee", channelOrderId: "SP-9988776655", marketplaceShopId: "sp_shop_10293", marketplaceShopName: "Amrez Beauty Official", createdById: "U-001", createdByName: "อ้อม ส.", createdByEmail: "aom@amrez.co.th", dateCreated: "2025-10-08T08:15:00+07:00", datePaid: "2025-10-08T08:16:10+07:00", datePacked: null, dateShipped: null, dateDelivered: null, dateCancelled: null, customerId: "CUST-000142", customerName: "Ploy Ch.", customerEmail: "ploy@example.com", customerPhone: "0812345678", shippingName: "Ploy Ch.", shippingPhone: "0812345678", shippingAddressLine1: "91/518 หมู่ 1", shippingAddressLine2: null, shippingSubdistrict: "หน้าเมือง", shippingDistrict: "เมือง", shippingProvince: "นนทบุรี", shippingPostcode: "11000", shippingCountry: "TH", billingName: "Ploy Ch.", billingTaxId: null, billingAddressLine1: "91/518 หมู่ 1", billingSubdistrict: "หน้าเมือง", billingDistrict: "เมือง", billingProvince: "นนทบุรี", billingPostcode: "11000", billingCountry: "TH", paymentMethod: "bank_qr", paymentStatus: "paid", paymentProvider: "2C2P", paymentTxId: "2C2P_2a8e7b0b", paymentFeeTHB: 9.5, currency: "THB", exchangeRate: 1, subtotalTHB: 1550, discountTHB: 60, shippingFeeTHB: 0, codFeeTHB: 0, packagingFeeTHB: 0, taxTHB: 0, totalTHB: 1490, itemsCount: 2, totalWeightGrams: 420, packageCount: 1, dimensionsCmL: 22, dimensionsCmW: 16, dimensionsCmH: 10, fulfillStatus: "processing", shipBy: "2025-10-09T18:00:00+07:00", carrier: null, serviceLevel: "Standard", trackingNo: null, warehouseCode: "BKK-DC", pickListId: null, waveId: null, binFrom: "A-01-03", binTo: null, tags: ["priority", "giftwrap"], rmaFlag: false, rmaReason: null, noteSeller: "ห่อของขวัญ", noteBuyer: "การ์ด HBD ด้วย", slaShipOnTime: true, giftWrap: true, fragile: false, priorityLevel: 2, riskScore: 12, holdReason: null, fraudCheckStatus: "clear", }, { id: "2", orderNo: "SO-2025-000782", orderRef: "OREF-9F4D", channel: "lazada", channelOrderId: "LZ-5566778899", marketplaceShopId: "lz_shop_8811", marketplaceShopName: "Kathy Labz Store", createdById: "U-002", createdByName: "บี ที.", createdByEmail: "bee@amrez.co.th", dateCreated: "2025-10-08T08:22:00+07:00", datePaid: null, datePacked: null, dateShipped: null, dateDelivered: null, dateCancelled: null, customerId: "CUST-000320", customerName: "Thanawat K.", customerEmail: "thanawat@example.com", customerPhone: "0820001111", shippingName: "Thanawat K.", shippingPhone: "0820001111", shippingAddressLine1: "128/7 ซอยสวนหลวง", shippingAddressLine2: "ตึก B ชั้น 5", shippingSubdistrict: "สวนหลวง", shippingDistrict: "พระนคร", shippingProvince: "กรุงเทพมหานคร", shippingPostcode: "10200", shippingCountry: "TH", billingName: "Thanawat K.", billingTaxId: "0105561234567", billingAddressLine1: "128/7 ซอยสวนหลวง", billingSubdistrict: "สวนหลวง", billingDistrict: "พระนคร", billingProvince: "กรุงเทพมหานคร", billingPostcode: "10200", billingCountry: "TH", paymentMethod: "cod", paymentStatus: "cod", paymentProvider: null, paymentTxId: null, paymentFeeTHB: 0, currency: "THB", exchangeRate: 1, subtotalTHB: 590, discountTHB: 0, shippingFeeTHB: 30, codFeeTHB: 20, packagingFeeTHB: 0, taxTHB: 0, totalTHB: 640, itemsCount: 1, totalWeightGrams: 220, packageCount: 1, dimensionsCmL: 18, dimensionsCmW: 12, dimensionsCmH: 8, fulfillStatus: "unfulfilled", shipBy: "2025-10-09T18:00:00+07:00", carrier: "Flash", serviceLevel: "COD-Standard", trackingNo: null, warehouseCode: "BKK-DC", pickListId: null, waveId: "WAVE-101", binFrom: "C-02-11", binTo: null, tags: ["fragile"], rmaFlag: false, rmaReason: null, noteSeller: null, noteBuyer: null, slaShipOnTime: true, giftWrap: false, fragile: true, priorityLevel: 1, riskScore: 35, holdReason: null, fraudCheckStatus: "review", }, { id: "3", orderNo: "SO-2025-000783", orderRef: "OREF-3B92", channel: "tiktok", channelOrderId: "TT-4433221100", marketplaceShopId: "tt_shop_5501", marketplaceShopName: "Amrez TikTok", createdById: "U-003", createdByName: "กร พ.", createdByEmail: "korn@amrez.co.th", dateCreated: "2025-10-08T08:40:00+07:00", datePaid: "2025-10-08T08:41:05+07:00", datePacked: "2025-10-08T09:05:30+07:00", dateShipped: "2025-10-08T12:10:00+07:00", dateDelivered: null, dateCancelled: null, customerId: "CUST-000511", customerName: "Mint W.", customerEmail: "mint@example.com", customerPhone: "0869997777", shippingName: "Mint W.", shippingPhone: "0869997777", shippingAddressLine1: "55/9 ถนนสายน้ำ", shippingAddressLine2: null, shippingSubdistrict: "ช้างเผือก", shippingDistrict: "เมือง", shippingProvince: "เชียงใหม่", shippingPostcode: "50300", shippingCountry: "TH", billingName: "Mint W.", billingTaxId: null, billingAddressLine1: "55/9 ถนนสายน้ำ", billingSubdistrict: "ช้างเผือก", billingDistrict: "เมือง", billingProvince: "เชียงใหม่", billingPostcode: "50300", billingCountry: "TH", paymentMethod: "wallet", paymentStatus: "paid", paymentProvider: "TikTokPay", paymentTxId: "TTPAY_c0ffeecafe", paymentFeeTHB: 14.9, currency: "THB", exchangeRate: 1, subtotalTHB: 2250, discountTHB: 60, shippingFeeTHB: 0, codFeeTHB: 0, packagingFeeTHB: 10, taxTHB: 0, totalTHB: 2200, itemsCount: 4, totalWeightGrams: 650, packageCount: 1, dimensionsCmL: 24, dimensionsCmW: 18, dimensionsCmH: 10, fulfillStatus: "shipped", shipBy: "2025-10-10T12:00:00+07:00", carrier: "J&T", serviceLevel: "Express", trackingNo: "JTTH123456789", warehouseCode: "CNX-HUB", pickListId: "PICK-5509", waveId: "WAVE-102", binFrom: "Z-03-02", binTo: null, tags: [], rmaFlag: false, rmaReason: null, noteSeller: "ออเดอร์ไลฟ์สด เร่งจัดส่ง", noteBuyer: null, slaShipOnTime: true, giftWrap: false, fragile: false, priorityLevel: 2, riskScore: 9, holdReason: null, fraudCheckStatus: "clear", }, ]; /* =========================== Helpers =========================== */ const LS_KEY = "orders.visibleCols.v2"; const THB = (n: number) => `฿${n.toLocaleString()}`; const fmtDate = (iso?: string | null) => iso ? new Date(iso).toLocaleString("th-TH", { hour12: false }) : "-"; const pill = (variant: "ok" | "warn" | "bad" | "neutral") => ({ ok: "bg-emerald-50 text-emerald-700 border border-emerald-200", warn: "bg-amber-50 text-amber-700 border border-amber-200", bad: "bg-red-50 text-red-700 border border-red-200", neutral: "bg-neutral-50 text-neutral-700 border border-neutral-200", }[variant]); function mapPaymentColor(p: PaymentStatus) { switch (p) { case "paid": return pill("ok"); case "pending": case "cod": return pill("warn"); case "failed": return pill("bad"); case "refunded": return pill("neutral"); } } function mapFulfillmentColor(f: FulfillmentStatus) { switch (f) { case "delivered": return pill("ok"); case "processing": case "shipped": case "unfulfilled": return pill("warn"); case "returned": case "cancelled": return pill("bad"); } } // typed getter (เลี่ยง as any) function getProp(obj: T, key: K): T[K] { return obj[key]; } function escapeCsv(v: unknown): string { if (v === null || v === undefined) return ""; const s = typeof v === "string" ? v : Array.isArray(v) ? v.join("|") : String(v); return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; } function downloadCsv(filename: string, rows: Order[], cols: Column[]) { if (!rows.length || !cols.length) return; const headers = cols.map((c) => c.labelTH); const keys = cols.map((c) => c.key); const csv = headers.join(",") + "\n" + rows .map((r) => keys.map((k) => escapeCsv(getProp(r, k))).join(",")) .join("\n"); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); } function defaultCell(v: unknown): ReactNode { if (v === null || v === undefined) return "-"; if (typeof v === "string") return v.trim() === "" ? "-" : v; if (typeof v === "number") return Number.isFinite(v) ? v.toString() : "-"; if (typeof v === "boolean") return v ? "YES" : "NO"; if (Array.isArray(v)) return v.length ? v.join("|") : "-"; return String(v); } /* =========================== คอนฟิกคอลัมน์ (ไทย + กลุ่ม) =========================== */ const ALL_COLS: Column[] = [ // เอกลักษณ์ & ช่องทาง { key: "id", labelTH: "ไอดี", groupTH: "เอกลักษณ์ & ช่องทาง" }, { key: "orderNo", labelTH: "เลขออเดอร์", groupTH: "เอกลักษณ์ & ช่องทาง" }, { key: "orderRef", labelTH: "อ้างอิงภายใน", groupTH: "เอกลักษณ์ & ช่องทาง" }, { key: "channel", labelTH: "ช่องทาง", groupTH: "เอกลักษณ์ & ช่องทาง" }, { key: "channelOrderId", labelTH: "เลขออเดอร์ช่องทาง", groupTH: "เอกลักษณ์ & ช่องทาง", }, { key: "marketplaceShopId", labelTH: "รหัสร้าน", groupTH: "เอกลักษณ์ & ช่องทาง", }, { key: "marketplaceShopName", labelTH: "ชื่อร้าน", groupTH: "เอกลักษณ์ & ช่องทาง", }, // ผู้เปิดออเดอร์ { key: "createdByName", labelTH: "ผู้เปิดออเดอร์", groupTH: "ผู้เปิดออเดอร์" }, { key: "createdByEmail", labelTH: "อีเมลผู้เปิด", groupTH: "ผู้เปิดออเดอร์" }, { key: "createdById", labelTH: "รหัสผู้เปิด", groupTH: "ผู้เปิดออเดอร์" }, // วัน–เวลา { key: "dateCreated", labelTH: "สร้างเมื่อ", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) }, { key: "datePaid", labelTH: "ชำระเงิน", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) }, { key: "datePacked", labelTH: "แพ็คของ", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) }, { key: "dateShipped", labelTH: "ส่งของ", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) }, { key: "dateDelivered", labelTH: "ถึงปลายทาง", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) }, { key: "dateCancelled", labelTH: "ยกเลิก", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) }, // ลูกค้า { key: "customerId", labelTH: "รหัสลูกค้า", groupTH: "ลูกค้า" }, { key: "customerName", labelTH: "ชื่อลูกค้า", groupTH: "ลูกค้า" }, { key: "customerEmail", labelTH: "อีเมลลูกค้า", groupTH: "ลูกค้า" }, { key: "customerPhone", labelTH: "เบอร์ลูกค้า", groupTH: "ลูกค้า" }, // ที่อยู่จัดส่ง { key: "shippingName", labelTH: "ชื่อผู้รับ", groupTH: "ที่อยู่จัดส่ง" }, { key: "shippingPhone", labelTH: "เบอร์ผู้รับ", groupTH: "ที่อยู่จัดส่ง" }, { key: "shippingAddressLine1", labelTH: "ที่อยู่ (บรรทัด 1)", groupTH: "ที่อยู่จัดส่ง", }, { key: "shippingAddressLine2", labelTH: "ที่อยู่ (บรรทัด 2)", groupTH: "ที่อยู่จัดส่ง", }, { key: "shippingSubdistrict", labelTH: "แขวง/ตำบล", groupTH: "ที่อยู่จัดส่ง" }, { key: "shippingDistrict", labelTH: "เขต/อำเภอ", groupTH: "ที่อยู่จัดส่ง" }, { key: "shippingProvince", labelTH: "จังหวัด", groupTH: "ที่อยู่จัดส่ง" }, { key: "shippingPostcode", labelTH: "รหัสไปรษณีย์", groupTH: "ที่อยู่จัดส่ง" }, { key: "shippingCountry", labelTH: "ประเทศ", groupTH: "ที่อยู่จัดส่ง" }, // ออกใบเสร็จ/ภาษี { key: "billingName", labelTH: "ชื่อสำหรับบิล", groupTH: "ออกใบเสร็จ/ภาษี" }, { key: "billingTaxId", labelTH: "เลขภาษี", groupTH: "ออกใบเสร็จ/ภาษี" }, { key: "billingAddressLine1", labelTH: "ที่อยู่บิล (บรรทัด 1)", groupTH: "ออกใบเสร็จ/ภาษี", }, { key: "billingSubdistrict", labelTH: "แขวง/ตำบล (บิล)", groupTH: "ออกใบเสร็จ/ภาษี" }, { key: "billingDistrict", labelTH: "เขต/อำเภอ (บิล)", groupTH: "ออกใบเสร็จ/ภาษี" }, { key: "billingProvince", labelTH: "จังหวัด (บิล)", groupTH: "ออกใบเสร็จ/ภาษี" }, { key: "billingPostcode", labelTH: "รหัสไปรษณีย์ (บิล)", groupTH: "ออกใบเสร็จ/ภาษี", }, { key: "billingCountry", labelTH: "ประเทศ (บิล)", groupTH: "ออกใบเสร็จ/ภาษี" }, // การชำระเงิน { key: "paymentMethod", labelTH: "วิธีชำระ", groupTH: "การชำระเงิน" }, { key: "paymentStatus", labelTH: "สถานะชำระ", groupTH: "การชำระเงิน", format: (v) => ( {v as string} ), }, { key: "paymentProvider", labelTH: "ผู้ให้บริการชำระ", groupTH: "การชำระเงิน" }, { key: "paymentTxId", labelTH: "รหัสรายการชำระ", groupTH: "การชำระเงิน" }, { key: "paymentFeeTHB", labelTH: "ค่าธรรมเนียม", groupTH: "การชำระเงิน", align: "right", format: (v) => (typeof v === "number" ? THB(v) : "-"), }, // ราคา/ต้นทุนย่อย { key: "currency", labelTH: "สกุล", groupTH: "ราคา/ต้นทุนย่อย" }, { key: "exchangeRate", labelTH: "เรท", groupTH: "ราคา/ต้นทุนย่อย", align: "right" }, { key: "subtotalTHB", labelTH: "ยอดก่อนลด", groupTH: "ราคา/ต้นทุนย่อย", align: "right", format: (v) => THB(Number(v)), }, { key: "discountTHB", labelTH: "ส่วนลด", groupTH: "ราคา/ต้นทุนย่อย", align: "right", format: (v) => THB(Number(v)), }, { key: "shippingFeeTHB", labelTH: "ค่าส่ง", groupTH: "ราคา/ต้นทุนย่อย", align: "right", format: (v) => THB(Number(v)), }, { key: "codFeeTHB", labelTH: "ค่า COD", groupTH: "ราคา/ต้นทุนย่อย", align: "right", format: (v) => THB(Number(v)), }, { key: "packagingFeeTHB", labelTH: "ค่าบรรจุภัณฑ์", groupTH: "ราคา/ต้นทุนย่อย", align: "right", format: (v) => THB(Number(v)), }, { key: "taxTHB", labelTH: "ภาษี", groupTH: "ราคา/ต้นทุนย่อย", align: "right", format: (v) => THB(Number(v)) }, { key: "totalTHB", labelTH: "ยอดรวม", groupTH: "ราคา/ต้นทุนย่อย", align: "right", format: (v) => THB(Number(v)) }, // สินค้า/โลจิสติกส์ { key: "itemsCount", labelTH: "จำนวนชิ้น", groupTH: "สินค้า/โลจิสติกส์", align: "right" }, { key: "totalWeightGrams", labelTH: "น้ำหนัก(g)", groupTH: "สินค้า/โลจิสติกส์", align: "right" }, { key: "packageCount", labelTH: "จำนวนพัสดุ", groupTH: "สินค้า/โลจิสติกส์", align: "right" }, { key: "dimensionsCmL", labelTH: "ยาว(cm)", groupTH: "สินค้า/โลจิสติกส์", align: "right" }, { key: "dimensionsCmW", labelTH: "กว้าง(cm)", groupTH: "สินค้า/โลจิสติกส์", align: "right" }, { key: "dimensionsCmH", labelTH: "สูง(cm)", groupTH: "สินค้า/โลจิสติกส์", align: "right" }, // สถานะจัดส่ง { key: "fulfillStatus", labelTH: "สถานะจัดส่ง", groupTH: "สถานะจัดส่ง", format: (v) => ( {v as string} ), }, { key: "shipBy", labelTH: "กำหนดส่งภายใน", groupTH: "สถานะจัดส่ง", format: (v) => fmtDate(v as string | null | undefined) }, { key: "carrier", labelTH: "ขนส่ง", groupTH: "สถานะจัดส่ง" }, { key: "serviceLevel", labelTH: "บริการ", groupTH: "สถานะจัดส่ง" }, { key: "trackingNo", labelTH: "เลขติดตาม", groupTH: "สถานะจัดส่ง" }, // โกดัง/ปฏิบัติการ { key: "warehouseCode", labelTH: "คลังสินค้า", groupTH: "โกดัง/ปฏิบัติการ" }, { key: "pickListId", labelTH: "ใบหยิบ", groupTH: "โกดัง/ปฏิบัติการ" }, { key: "waveId", labelTH: "รหัสเวฟ", groupTH: "โกดัง/ปฏิบัติการ" }, { key: "binFrom", labelTH: "หยิบจากช่อง", groupTH: "โกดัง/ปฏิบัติการ" }, { key: "binTo", labelTH: "ย้ายไปช่อง", groupTH: "โกดัง/ปฏิบัติการ" }, // ธง/โน้ต/ความเสี่ยง { key: "tags", labelTH: "แท็ก", groupTH: "ธง/โน้ต/ความเสี่ยง", format: (v) => (Array.isArray(v) && v.length ? v.join("|") : "-"), }, { key: "rmaFlag", labelTH: "มีเคลม?", groupTH: "ธง/โน้ต/ความเสี่ยง", format: (v) => (typeof v === "boolean" ? (v ? "YES" : "NO") : "-"), }, { key: "rmaReason", labelTH: "สาเหตุเคลม", groupTH: "ธง/โน้ต/ความเสี่ยง" }, { key: "noteSeller", labelTH: "โน้ตผู้ขาย", groupTH: "ธง/โน้ต/ความเสี่ยง" }, { key: "noteBuyer", labelTH: "โน้ตผู้ซื้อ", groupTH: "ธง/โน้ต/ความเสี่ยง" }, { key: "slaShipOnTime", labelTH: "ส่งทันเวลา?", groupTH: "ธง/โน้ต/ความเสี่ยง", format: (v) => (typeof v === "boolean" ? (v ? "OK" : "N/A") : "N/A"), }, { key: "giftWrap", labelTH: "ห่อของขวัญ", groupTH: "ธง/โน้ต/ความเสี่ยง", format: (v) => (typeof v === "boolean" ? (v ? "YES" : "NO") : "NO"), }, { key: "fragile", labelTH: "แตกหักง่าย", groupTH: "ธง/โน้ต/ความเสี่ยง", format: (v) => (typeof v === "boolean" ? (v ? "YES" : "NO") : "NO"), }, { key: "priorityLevel", labelTH: "ลำดับความสำคัญ", groupTH: "ธง/โน้ต/ความเสี่ยง", align: "right" }, { key: "riskScore", labelTH: "คะแนนเสี่ยง", groupTH: "ธง/โน้ต/ความเสี่ยง", align: "right" }, { key: "holdReason", labelTH: "เหตุผลพักออเดอร์", groupTH: "ธง/โน้ต/ความเสี่ยง" }, { key: "fraudCheckStatus", labelTH: "สถานะ Fraud", groupTH: "ธง/โน้ต/ความเสี่ยง" }, ]; // ค่าตั้งต้นคอลัมน์ที่แสดง const DEFAULT_VISIBLE: ColumnKey[] = [ "orderNo", "dateCreated", "channel", "marketplaceShopName", "createdByName", "customerName", "itemsCount", "totalTHB", "paymentStatus", "fulfillStatus", "shipBy", "trackingNo", "warehouseCode", ]; /* =========================== จัดกลุ่มสำหรับ Modal (ไทย) =========================== */ const GROUPS: { titleTH: string; keys: ColumnKey[] }[] = [ { titleTH: "เอกลักษณ์ & ช่องทาง", keys: ["id", "orderNo", "orderRef", "channel", "channelOrderId", "marketplaceShopId", "marketplaceShopName"], }, { titleTH: "ผู้เปิดออเดอร์", keys: ["createdByName", "createdByEmail", "createdById"], }, { titleTH: "วัน–เวลา", keys: ["dateCreated", "datePaid", "datePacked", "dateShipped", "dateDelivered", "dateCancelled"], }, { titleTH: "ลูกค้า", keys: ["customerId", "customerName", "customerEmail", "customerPhone"], }, { titleTH: "ที่อยู่จัดส่ง", keys: [ "shippingName", "shippingPhone", "shippingAddressLine1", "shippingAddressLine2", "shippingSubdistrict", "shippingDistrict", "shippingProvince", "shippingPostcode", "shippingCountry", ], }, { titleTH: "ออกใบเสร็จ/ภาษี", keys: [ "billingName", "billingTaxId", "billingAddressLine1", "billingSubdistrict", "billingDistrict", "billingProvince", "billingPostcode", "billingCountry", ], }, { titleTH: "การชำระเงิน", keys: ["paymentMethod", "paymentStatus", "paymentProvider", "paymentTxId", "paymentFeeTHB"], }, { titleTH: "ราคา/ต้นทุนย่อย", keys: ["currency", "exchangeRate", "subtotalTHB", "discountTHB", "shippingFeeTHB", "codFeeTHB", "packagingFeeTHB", "taxTHB", "totalTHB"], }, { titleTH: "สินค้า/โลจิสติกส์", keys: ["itemsCount", "totalWeightGrams", "packageCount", "dimensionsCmL", "dimensionsCmW", "dimensionsCmH"], }, { titleTH: "สถานะจัดส่ง", keys: ["fulfillStatus", "shipBy", "carrier", "serviceLevel", "trackingNo"], }, { titleTH: "โกดัง/ปฏิบัติการ", keys: ["warehouseCode", "pickListId", "waveId", "binFrom", "binTo"], }, { titleTH: "ธง/โน้ต/ความเสี่ยง", keys: [ "tags", "rmaFlag", "rmaReason", "noteSeller", "noteBuyer", "slaShipOnTime", "giftWrap", "fragile", "priorityLevel", "riskScore", "holdReason", "fraudCheckStatus", ], }, ]; /* =========================== Page =========================== */ export default function OrdersPage() { const [q, setQ] = useState(""); const [channel, setChannel] = useState<"all" | Channel>("all"); const [payment, setPayment] = useState<"all" | PaymentStatus>("all"); const [fulfillment, setFulfillment] = useState<"all" | FulfillmentStatus>("all"); const [visibleKeys, setVisibleKeys] = useState(DEFAULT_VISIBLE); const [openCustomize, setOpenCustomize] = useState(false); const [tempKeys, setTempKeys] = useState(DEFAULT_VISIBLE); // โหลด/จำคอลัมน์ที่เลือก useEffect(() => { try { const raw = localStorage.getItem(LS_KEY); if (raw) { const parsed = JSON.parse(raw) as ColumnKey[]; if (Array.isArray(parsed) && parsed.length) { setVisibleKeys(parsed); setTempKeys(parsed); } } } catch {} }, []); const persistVisible = (keys: ColumnKey[]) => { setVisibleKeys(keys); try { localStorage.setItem(LS_KEY, JSON.stringify(keys)); } catch {} }; // กรองรายการ const rows = useMemo(() => { let arr = [...MOCK_ORDERS]; if (channel !== "all") arr = arr.filter((o) => o.channel === channel); if (payment !== "all") arr = arr.filter((o) => o.paymentStatus === payment); if (fulfillment !== "all") arr = arr.filter((o) => o.fulfillStatus === fulfillment); if (q.trim()) { const s = q.trim().toLowerCase(); arr = arr.filter((o) => [ o.orderNo, o.orderRef, o.customerName, o.customerEmail, o.customerPhone, o.channelOrderId, o.trackingNo, o.billingTaxId, o.createdByName, o.createdByEmail, ] .filter((x): x is string => typeof x === "string") .some((x) => x.toLowerCase().includes(s)), ); } // ใหม่สุดก่อน arr.sort((a, b) => +new Date(b.dateCreated) - +new Date(a.dateCreated)); return arr; }, [q, channel, payment, fulfillment]); // สร้างคอลัมน์ที่แสดงจริง const visibleCols: Column[] = useMemo(() => { const map = new Map(ALL_COLS.map((c) => [c.key, c])); return visibleKeys.map((k) => map.get(k)!).filter(Boolean); }, [visibleKeys]); // สร้างหัวตาราง 2 ชั้น: ชั้นบน = กลุ่ม, ชั้นล่าง = หัวคอลัมน์ย่อย const topHeaderSegments = useMemo(() => { type Seg = { groupTH: string; colSpan: number }; const segs: Seg[] = []; visibleCols.forEach((c, i) => { const g = c.groupTH; if (i === 0 || segs[segs.length - 1].groupTH !== g) { segs.push({ groupTH: g, colSpan: 1 }); } else { segs[segs.length - 1].colSpan += 1; } }); return segs; }, [visibleCols]); return (
{/* ส่วนหัว */}

ออเดอร์

มุมมองเต็ม • แสดง {visibleCols.length} ฟิลด์ • ทั้งหมด {rows.length} ออเดอร์

{/* ตัวกรอง */}
setQ(e.target.value)} className="w-full rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm outline-none placeholder:text-neutral-400 focus:border-neutral-300" />
{/* ช่องทาง (Select) */}
{/* การชำระเงิน */}
{/* สถานะจัดส่ง */}
{/* ตาราง (2 ชั้น) — ไม่ fix size: ใช้ table-auto, ให้คอนเทนต์กำหนดความกว้าง, บรรทัดเดียวทุกเซลล์ */}
{/* ชั้นบน: กลุ่มหัวข้อ (ไทย) */} {topHeaderSegments.map((seg, idx) => ( ))} {/* ชั้นล่าง: หัวคอลัมน์ย่อย (ไทย) */} {visibleCols.map((c) => ( ))} {rows.map((o) => ( {visibleCols.map((c) => { const raw = getProp(o, c.key); const content: ReactNode = c.format ? c.format(raw, o) : defaultCell(raw); const align = c.align === "right" ? "text-right" : ""; return ( ); })} ))}
{seg.groupTH}
{c.labelTH}
{c.key === "orderNo" ? ( {content} ) : c.key === "tags" && Array.isArray(o.tags) ? ( // ไม่ให้ขึ้นหลายบรรทัด: ตัด flex-wrap ออก
{o.tags!.length ? ( o.tags!.map((t) => ( {t} )) ) : ( - )}
) : ( content )}
{/* Modal: ปรับแต่งตาราง (ทำให้ scroll ได้ + footer ติดล่าง) */} {openCustomize && (
setOpenCustomize(false)} >
e.stopPropagation()} > {/* Header (ติดบน) */}

ปรับแต่งตาราง

{/* Body (เลื่อนลงได้) */}
เลือก {tempKeys.length} คอลัมน์
{GROUPS.map((g) => (
{g.titleTH}
{g.keys.map((k) => { const col = ALL_COLS.find((c) => c.key === k)!; const checked = tempKeys.includes(k); return ( ); })}
))}
{/* Footer (ติดล่าง) */}
เคล็ดลับ: ระบบจะจำคอลัมน์ที่คุณเลือกไว้ในเบราว์เซอร์นี้
)}
); }