"use client"; import { useEffect, useMemo, useState, type ReactNode } from "react"; import type { LucideIcon } from "lucide-react"; import { Package, Tag, CheckCircle2, Hourglass, RotateCcw, Ban, ShieldCheck, ShieldAlert, ShieldX, FileText, Image as ImageIcon, Link as LinkIcon, Search, Eraser, X as XIcon } from "lucide-react"; /* ======================== Types ======================== */ type ProductType = "simple" | "variant" | "bundle"; type LifecycleStatus = "draft" | "active" | "archived" | "discontinued"; type ComplianceStatus = "na" | "pending" | "approved" | "rejected"; type QaStage = "idea" | "lab" | "pilot" | "approved" | "deprecated"; type Product = { /* เอกลักษณ์ & โครงสร้าง */ id: string; productCode: string; // รหัสสินค้าแม่ parentProductId?: string | null; // ถ้าเป็น variant อ้างอิงแม่ sku: string; // SKU ระดับ variant gtin?: string | null; type: ProductType; attributesSummary?: string | null; brandId?: string | null; brand: string; categoryId?: string | null; category: string; categoryPath?: string | null; /* คอนเทนต์ */ productName: string; variantName?: string | null; descriptionShort?: string | null; slug?: string | null; /* หน่วย & ขนาด */ uom: string; weightGrams?: number | null; dimLcm?: number | null; dimWcm?: number | null; dimHcm?: number | null; netVolumeMl?: number | null; /* ราคา/การตลาด (สรุป) */ currency?: "THB" | "USD" | "JPY" | "EUR"; listPrice?: number | null; compareAtPrice?: number | null; priceListsCount?: number | null; /* สื่อ & เอกสาร/SEO */ imageCount?: number | null; docsCount?: number | null; seoTitle?: string | null; /* ช่องทาง/ลิสติ้ง */ d2cPublished?: boolean; shopeePublished?: boolean; lazadaPublished?: boolean; tiktokPublished?: boolean; channelsPublished?: string | null; d2cUrl?: string | null; shopeeListingId?: string | null; lazadaListingId?: string | null; tiktokListingId?: string | null; /* กฎหมาย/ความสอดคล้อง */ complianceStatus: ComplianceStatus; thaiFdaNo?: string | null; hazardClass?: string | null; storageInstruction?: string | null; shelfLifeMonths?: number | null; ageRestrictionMin?: number | null; allergens?: string[] | null; certificationsCount?: number | null; msdsUrl?: string | null; /* R&D / สูตร */ qaStage?: QaStage; formulationVersion?: string | null; ingredientsCount?: number | null; rdOwner?: string | null; /* บรรจุภัณฑ์ */ packLevels?: number | null; // 1=Unit, 2=Unit+Case, 3=Unit+Inner+Case packPrimaryBarcode?: string | null; // GTIN Unit packCaseBarcode?: string | null; // GTIN Case packNotes?: string | null; /* ซัพพลาย/การผลิต */ manufacturer?: string | null; mfgCountry?: string | null; mfgLeadTimeDays?: number | null; moq?: number | null; /* วงจรชีวิต */ status: LifecycleStatus; publishedAt?: string | null; createdAt: string; updatedAt?: string | null; archivedAt?: string | null; /* อื่น ๆ */ tags?: string[]; note?: string | null; }; type ColumnKey = keyof Product; type Column = { key: ColumnKey; labelTH: string; groupTH: string; align?: "left" | "right"; format?: (v: Product[ColumnKey], row: Product) => ReactNode; }; /* ======================== Meta & helpers ======================== */ type Tone = "ok" | "warn" | "bad" | "neutral"; const pill = (t: Tone) => ({ 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", }[t]); const U = (s: string) => s.toUpperCase(); // เส้นคอลัมน์โทนอ่อน (ใช้กับทั้ง th/td) const COL_BORDER = "border-r border-neutral-200/60 last:border-r-0"; const IconLabel = ({ Icon, label, pillTone }: { Icon: LucideIcon; label: string; pillTone?: Tone }) => { const content = ( {label} ); if (!pillTone) return content; return ( {label} ); }; /* Renderers */ const TYPE_META: Record = { simple: { label: U("Simple"), Icon: Package }, variant: { label: U("Variant"), Icon: Tag }, bundle: { label: U("Bundle"), Icon: Package }, }; const renderType = (t: ProductType) => { const { Icon, label } = TYPE_META[t]; return ; }; const LIFECYCLE_META: Record = { draft: { label: U("Draft"), Icon: Hourglass, tone: "warn" }, active: { label: U("Active"), Icon: CheckCircle2, tone: "ok" }, archived: { label: U("Archived"), Icon: RotateCcw, tone: "neutral" }, discontinued: { label: U("Discontinued"), Icon: Ban, tone: "bad" }, }; const renderLifecycle = (s: LifecycleStatus) => { const { Icon, label, tone } = LIFECYCLE_META[s]; return ; }; const COMPLIANCE_META: Record = { na: { label: U("N/A"), Icon: ShieldAlert, tone: "neutral" }, pending: { label: U("Pending"), Icon: ShieldAlert, tone: "warn" }, approved: { label: U("Approved"), Icon: ShieldCheck, tone: "ok" }, rejected: { label: U("Rejected"), Icon: ShieldX, tone: "bad" }, }; const renderCompliance = (s: ComplianceStatus) => { const { Icon, label, tone } = COMPLIANCE_META[s]; return ; }; const fmtDate = (iso?: string | null) => (iso ? new Date(iso).toLocaleString("th-TH", { hour12: false }) : "-"); const THB = (n?: number | null) => (typeof n === "number" ? `฿${n.toLocaleString()}` : "-"); const num = (n?: number | null) => (typeof n === "number" && Number.isFinite(n) ? n.toString() : "-"); 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: Product[], 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); } /* ======================== Mock Data (ตัวอย่าง) ======================== */ const MOCK_PRODUCTS: Product[] = [ { id: "P-1001", productCode: "AMZ-COLL-BASE", parentProductId: null, sku: "AMZ-COLL-BASE-250G", gtin: "08850001112223", type: "variant", attributesSummary: "Weight=250g|Flavor=Unflavored", brandId: "BRD-0001", brand: "Amrez", categoryId: "CAT-0002", category: "Supplements > Collagen", categoryPath: "Supplements>Collagen", productName: "Amrez Collagen Peptides", variantName: "250g Pouch", descriptionShort: "คอลลาเจนเพปไทด์ ไฮโดรไลซ์", slug: "amrez-collagen-250g", uom: "ชิ้น", weightGrams: 280, dimLcm: 20, dimWcm: 14, dimHcm: 6, netVolumeMl: null, currency: "THB", listPrice: 790, compareAtPrice: 890, priceListsCount: 3, imageCount: 5, docsCount: 3, seoTitle: "คอลลาเจน Amrez 250 กรัม", d2cPublished: true, shopeePublished: true, lazadaPublished: true, tiktokPublished: false, channelsPublished: "D2C|Shopee|Lazada", d2cUrl: "/p/amrez-collagen-250g", shopeeListingId: "SP-556677", lazadaListingId: "LZ-889900", tiktokListingId: null, complianceStatus: "approved", thaiFdaNo: "11-1-12345-5-0001", hazardClass: "Non-hazard", storageInstruction: "เก็บให้พ้นแสงแดด", shelfLifeMonths: 24, ageRestrictionMin: 12, allergens: [], certificationsCount: 2, msdsUrl: null, qaStage: "approved", formulationVersion: "v1.3", ingredientsCount: 8, rdOwner: "Korn P.", packLevels: 2, packPrimaryBarcode: "08850001112223", packCaseBarcode: "18850001112220", packNotes: "12 units/case", manufacturer: "Amrez Foods Co., Ltd.", mfgCountry: "TH", mfgLeadTimeDays: 21, moq: 500, status: "active", publishedAt: "2025-08-15T09:00:00+07:00", createdAt: "2025-07-10T15:21:00+07:00", updatedAt: "2025-10-07T10:10:00+07:00", archivedAt: null, tags: ["top_seller", "online_only"], note: null, }, { id: "P-1002", productCode: "AMZ-SERUM-VC", parentProductId: null, sku: "AMZ-SERUM-VC-30ML", gtin: "08850001113334", type: "simple", attributesSummary: null, brandId: "BRD-0002", brand: "Kathy Labz", categoryId: "CAT-0101", category: "Beauty > Serum", categoryPath: "Beauty>Serum", productName: "Vitamin C Bright Serum 30 ml", variantName: null, descriptionShort: "วิตซี + HA", slug: "vitamin-c-bright-serum-30ml", uom: "ขวด", weightGrams: 120, dimLcm: 14, dimWcm: 5, dimHcm: 5, netVolumeMl: 30, currency: "THB", listPrice: 590, compareAtPrice: null, priceListsCount: 2, imageCount: 4, docsCount: 2, seoTitle: "เซรั่มวิตซี 30ml", d2cPublished: false, shopeePublished: false, lazadaPublished: false, tiktokPublished: false, channelsPublished: "", d2cUrl: null, shopeeListingId: null, lazadaListingId: null, tiktokListingId: null, complianceStatus: "pending", thaiFdaNo: null, hazardClass: "Non-hazard", storageInstruction: "ปิดฝาให้สนิท", shelfLifeMonths: 18, ageRestrictionMin: 12, allergens: ["Fragrance"], certificationsCount: 1, msdsUrl: "https://example.com/msds/serum-vc.pdf", qaStage: "lab", formulationVersion: "v0.9", ingredientsCount: 15, rdOwner: "Bee T.", packLevels: 2, packPrimaryBarcode: "08850001113334", packCaseBarcode: "18850001113331", packNotes: "24 units/case", manufacturer: "Kathy Labz Lab", mfgCountry: "TH", mfgLeadTimeDays: 35, moq: 1000, status: "draft", publishedAt: null, createdAt: "2025-09-01T11:00:00+07:00", updatedAt: "2025-10-08T09:30:00+07:00", archivedAt: null, tags: ["r&d", "new"], note: "รอเลข อย.", }, { id: "P-1003", productCode: "AMZ-GIFT-SET-01", parentProductId: null, sku: "AMZ-GIFT-SET-01", gtin: null, type: "bundle", attributesSummary: "Bundle of: COLL-250G + VC-30ML", brandId: "BRD-0001", brand: "Amrez", categoryId: "CAT-0901", category: "Sets & Bundles", categoryPath: "Sets&Bundles", productName: "Amrez Glow Gift Set", variantName: null, descriptionShort: "ชุดของขวัญผิวโกลว์", slug: "amrez-glow-gift-set", uom: "ชุด", weightGrams: 420, dimLcm: 24, dimWcm: 18, dimHcm: 8, netVolumeMl: null, currency: "THB", listPrice: 1290, compareAtPrice: 1480, priceListsCount: 2, imageCount: 6, docsCount: 1, seoTitle: "ชุดของขวัญผิวโกลว์", d2cPublished: true, shopeePublished: true, lazadaPublished: false, tiktokPublished: true, channelsPublished: "D2C|Shopee|TikTok", d2cUrl: "/p/amrez-glow-gift-set", shopeeListingId: "SP-777888", lazadaListingId: null, tiktokListingId: "TT-445566", complianceStatus: "na", thaiFdaNo: null, hazardClass: "Non-hazard", storageInstruction: "—", shelfLifeMonths: 24, ageRestrictionMin: null, allergens: null, certificationsCount: 0, msdsUrl: null, qaStage: "approved", formulationVersion: null, ingredientsCount: null, rdOwner: "Aom S.", packLevels: 2, packPrimaryBarcode: null, packCaseBarcode: null, packNotes: "ชุดบันเดิล", manufacturer: "Amrez Assembly", mfgCountry: "TH", mfgLeadTimeDays: 10, moq: 200, status: "active", publishedAt: "2025-10-01T08:00:00+07:00", createdAt: "2025-09-20T10:10:00+07:00", updatedAt: "2025-10-08T12:00:00+07:00", archivedAt: null, tags: ["bundle", "gift"], note: null, }, ]; /* ======================== Columns (ขยายชุดใหญ่) ======================== */ const ALL_COLS: Column[] = [ // เอกลักษณ์ & โครงสร้าง { key: "id", labelTH: "ไอดี", groupTH: "เอกลักษณ์ & โครงสร้าง" }, { key: "productCode", labelTH: "รหัสสินค้าแม่", groupTH: "เอกลักษณ์ & โครงสร้าง" }, { key: "parentProductId", labelTH: "รหัสแม่ (ถ้ามี)", groupTH: "เอกลักษณ์ & โครงสร้าง" }, { key: "type", labelTH: "ชนิดสินค้า", groupTH: "เอกลักษณ์ & โครงสร้าง", format: (v) => renderType(v as ProductType) }, { key: "sku", labelTH: "SKU (Variant)", groupTH: "เอกลักษณ์ & โครงสร้าง" }, { key: "gtin", labelTH: "GTIN", groupTH: "เอกลักษณ์ & โครงสร้าง" }, { key: "attributesSummary", labelTH: "แอตทริบิวต์", groupTH: "เอกลักษณ์ & โครงสร้าง" }, // แบรนด์/หมวด { key: "brand", labelTH: "แบรนด์", groupTH: "แบรนด์/หมวด" }, { key: "brandId", labelTH: "รหัสแบรนด์", groupTH: "แบรนด์/หมวด" }, { key: "category", labelTH: "หมวดหมู่", groupTH: "แบรนด์/หมวด" }, { key: "categoryPath", labelTH: "Taxonomy Path", groupTH: "แบรนด์/หมวด" }, { key: "categoryId", labelTH: "รหัสหมวด", groupTH: "แบรนด์/หมวด" }, // คอนเทนต์ { key: "productName", labelTH: "ชื่อสินค้า", groupTH: "คอนเทนต์" }, { key: "variantName", labelTH: "ชื่อรุ่นย่อย", groupTH: "คอนเทนต์" }, { key: "descriptionShort", labelTH: "คำอธิบายสั้น", groupTH: "คอนเทนต์" }, { key: "slug", labelTH: "Slug", groupTH: "คอนเทนต์" }, // หน่วย & ขนาด { key: "uom", labelTH: "หน่วยนับ", groupTH: "หน่วย & ขนาด" }, { key: "weightGrams", labelTH: "น้ำหนัก(g)", groupTH: "หน่วย & ขนาด", align: "right" }, { key: "dimLcm", labelTH: "ยาว(cm)", groupTH: "หน่วย & ขนาด", align: "right" }, { key: "dimWcm", labelTH: "กว้าง(cm)", groupTH: "หน่วย & ขนาด", align: "right" }, { key: "dimHcm", labelTH: "สูง(cm)", groupTH: "หน่วย & ขนาด", align: "right" }, { key: "netVolumeMl", labelTH: "ปริมาตร(ml)", groupTH: "หน่วย & ขนาด", align: "right" }, // ราคา/การตลาด { key: "currency", labelTH: "สกุล", groupTH: "ราคา/การตลาด" }, { key: "listPrice", labelTH: "ราคา", groupTH: "ราคา/การตลาด", align: "right", format: (v) => THB(v as number | null | undefined) }, { key: "compareAtPrice", labelTH: "ราคาเทียบ", groupTH: "ราคา/การตลาด", align: "right", format: (v) => THB(v as number | null | undefined) }, { key: "priceListsCount", labelTH: "จำนวนราคาลิสต์", groupTH: "ราคา/การตลาด", align: "right" }, // สื่อ & เอกสาร/SEO { key: "imageCount", labelTH: "จำนวนสื่อ", groupTH: "สื่อ/เอกสาร/SEO", align: "right", format: (v) => ( {num(v as number)} ) }, { key: "docsCount", labelTH: "จำนวนเอกสาร", groupTH: "สื่อ/เอกสาร/SEO", align: "right", format: (v) => ( {num(v as number)} ) }, { key: "seoTitle", labelTH: "SEO Title", groupTH: "สื่อ/เอกสาร/SEO" }, // ช่องทาง/ลิสติ้ง { key: "channelsPublished", labelTH: "เผยแพร่ที่", groupTH: "ช่องทาง/ลิสติ้ง" }, { key: "d2cPublished", labelTH: "D2C", groupTH: "ช่องทาง/ลิสติ้ง" }, { key: "shopeePublished", labelTH: "Shopee", groupTH: "ช่องทาง/ลิสติ้ง" }, { key: "lazadaPublished", labelTH: "Lazada", groupTH: "ช่องทาง/ลิสติ้ง" }, { key: "tiktokPublished", labelTH: "TikTok", groupTH: "ช่องทาง/ลิสติ้ง" }, { key: "d2cUrl", labelTH: "D2C URL", groupTH: "ช่องทาง/ลิสติ้ง", format: (v) => v ? ( เปิด ) : "-" }, { key: "shopeeListingId", labelTH: "Shopee ID", groupTH: "ช่องทาง/ลิสติ้ง" }, { key: "lazadaListingId", labelTH: "Lazada ID", groupTH: "ช่องทาง/ลิสติ้ง" }, { key: "tiktokListingId", labelTH: "TikTok ID", groupTH: "ช่องทาง/ลิสติ้ง" }, // กฎหมาย/ความสอดคล้อง { key: "complianceStatus", labelTH: "Compliance", groupTH: "กฎหมาย/ความสอดคล้อง", format: (v) => renderCompliance(v as ComplianceStatus) }, { key: "thaiFdaNo", labelTH: "เลข อย.", groupTH: "กฎหมาย/ความสอดคล้อง" }, { key: "hazardClass", labelTH: "Hazard", groupTH: "กฎหมาย/ความสอดคล้อง" }, { key: "storageInstruction", labelTH: "คำแนะนำการเก็บ", groupTH: "กฎหมาย/ความสอดคล้อง" }, { key: "shelfLifeMonths", labelTH: "อายุสินค้า(ด.)", groupTH: "กฎหมาย/ความสอดคล้อง", align: "right" }, { key: "ageRestrictionMin", labelTH: "อายุขั้นต่ำ", groupTH: "กฎหมาย/ความสอดคล้อง", align: "right" }, { key: "allergens", labelTH: "สารก่อภูมิแพ้", groupTH: "กฎหมาย/ความสอดคล้อง", format: (v) => Array.isArray(v) && v.length ? v.join("|") : "-" }, { key: "certificationsCount", labelTH: "จำนวนใบรับรอง", groupTH: "กฎหมาย/ความสอดคล้อง", align: "right" }, { key: "msdsUrl", labelTH: "MSDS", groupTH: "กฎหมาย/ความสอดคล้อง", format: (v) => v ? ( เปิด ) : "-" }, // R&D / สูตร { key: "qaStage", labelTH: "QA Stage", groupTH: "R&D/สูตร" }, { key: "formulationVersion", labelTH: "สูตรเวอร์ชัน", groupTH: "R&D/สูตร" }, { key: "ingredientsCount", labelTH: "จำนวนส่วนผสม", groupTH: "R&D/สูตร", align: "right" }, { key: "rdOwner", labelTH: "ผู้รับผิดชอบ R&D", groupTH: "R&D/สูตร" }, // บรรจุภัณฑ์ { key: "packLevels", labelTH: "ระดับแพ็ก", groupTH: "บรรจุภัณฑ์", align: "right" }, { key: "packPrimaryBarcode", labelTH: "บาร์โค้ดหน่วย", groupTH: "บรรจุภัณฑ์" }, { key: "packCaseBarcode", labelTH: "บาร์โค้ดลัง", groupTH: "บรรจุภัณฑ์" }, { key: "packNotes", labelTH: "บันทึกแพ็ก", groupTH: "บรรจุภัณฑ์" }, // ซัพพลาย/การผลิต { key: "manufacturer", labelTH: "ผู้ผลิต", groupTH: "ซัพพลาย/การผลิต" }, { key: "mfgCountry", labelTH: "ประเทศผลิต", groupTH: "ซัพพลาย/การผลิต" }, { key: "mfgLeadTimeDays", labelTH: "Lead Time(วัน)", groupTH: "ซัพพลาย/การผลิต", align: "right" }, { key: "moq", labelTH: "MOQ", groupTH: "ซัพพลาย/การผลิต", align: "right" }, // วงจรชีวิต { key: "status", labelTH: "สถานะ", groupTH: "วงจรชีวิต", format: (v) => renderLifecycle(v as LifecycleStatus) }, { key: "publishedAt", labelTH: "เผยแพร่เมื่อ", groupTH: "วงจรชีวิต", format: (v) => fmtDate(v as string | null | undefined) }, { key: "createdAt", labelTH: "สร้างเมื่อ", groupTH: "วงจรชีวิต", format: (v) => fmtDate(v as string | null | undefined) }, { key: "updatedAt", labelTH: "แก้ไขล่าสุด", groupTH: "วงจรชีวิต", format: (v) => fmtDate(v as string | null | undefined) }, { key: "archivedAt", labelTH: "เก็บถาวรเมื่อ", groupTH: "วงจรชีวิต", format: (v) => fmtDate(v as string | null | undefined) }, // อื่น ๆ { key: "tags", labelTH: "แท็ก", groupTH: "อื่น ๆ", format: (v) => (Array.isArray(v) && v.length ? v.join("|") : "-") }, { key: "note", labelTH: "โน้ต", groupTH: "อื่น ๆ" }, ]; /* ======================== Visible/Groups ======================== */ const DEFAULT_VISIBLE: ColumnKey[] = [ "productCode","productName","variantName","sku","type", "brand","category","status", "complianceStatus","thaiFdaNo","listPrice", "channelsPublished","imageCount","docsCount", ]; const GROUPS: { titleTH: string; keys: ColumnKey[] }[] = [ { titleTH: "เอกลักษณ์ & โครงสร้าง", keys: ["id","productCode","parentProductId","type","sku","gtin","attributesSummary"] }, { titleTH: "แบรนด์/หมวด", keys: ["brand","brandId","category","categoryPath","categoryId"] }, { titleTH: "คอนเทนต์", keys: ["productName","variantName","descriptionShort","slug"] }, { titleTH: "หน่วย & ขนาด", keys: ["uom","weightGrams","dimLcm","dimWcm","dimHcm","netVolumeMl"] }, { titleTH: "ราคา/การตลาด", keys: ["currency","listPrice","compareAtPrice","priceListsCount"] }, { titleTH: "สื่อ/เอกสาร/SEO", keys: ["imageCount","docsCount","seoTitle"] }, { titleTH: "ช่องทาง/ลิสติ้ง", keys: ["channelsPublished","d2cPublished","shopeePublished","lazadaPublished","tiktokPublished","d2cUrl","shopeeListingId","lazadaListingId","tiktokListingId"] }, { titleTH: "กฎหมาย/ความสอดคล้อง", keys: ["complianceStatus","thaiFdaNo","hazardClass","storageInstruction","shelfLifeMonths","ageRestrictionMin","allergens","certificationsCount","msdsUrl"] }, { titleTH: "R&D/สูตร", keys: ["qaStage","formulationVersion","ingredientsCount","rdOwner"] }, { titleTH: "บรรจุภัณฑ์", keys: ["packLevels","packPrimaryBarcode","packCaseBarcode","packNotes"] }, { titleTH: "ซัพพลาย/การผลิต", keys: ["manufacturer","mfgCountry","mfgLeadTimeDays","moq"] }, { titleTH: "วงจรชีวิต", keys: ["status","publishedAt","createdAt","updatedAt","archivedAt"] }, { titleTH: "อื่น ๆ", keys: ["tags","note"] }, ]; /* ======================== Page ======================== */ const LS_KEY = "products.visibleCols.v2"; export default function ProductsPage() { const [q, setQ] = useState(""); const [ptype, setPtype] = useState<"all" | ProductType>("all"); const [lifecycle, setLifecycle] = useState<"all" | LifecycleStatus>("all"); const [compliance, setCompliance] = useState<"all" | ComplianceStatus>("all"); const [brand, setBrand] = useState<"all" | string>("all"); const [category, setCategory] = useState<"all" | string>("all"); const [channel, setChannel] = useState<"all" | "d2c" | "shopee" | "lazada" | "tiktok" | "none">("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 brands = useMemo(() => Array.from(new Set(MOCK_PRODUCTS.map(p => p.brand))).sort(), []); const categories = useMemo(() => Array.from(new Set(MOCK_PRODUCTS.map(p => p.category))).sort(), []); const rows = useMemo(() => { let arr = [...MOCK_PRODUCTS]; if (ptype !== "all") arr = arr.filter(p => p.type === ptype); if (lifecycle !== "all") arr = arr.filter(p => p.status === lifecycle); if (compliance !== "all") arr = arr.filter(p => p.complianceStatus === compliance); if (brand !== "all") arr = arr.filter(p => p.brand === brand); if (category !== "all") arr = arr.filter(p => p.category === category); if (channel !== "all") { if (channel === "none") { arr = arr.filter(p => !p.d2cPublished && !p.shopeePublished && !p.lazadaPublished && !p.tiktokPublished); } else { const key = (channel + "Published") as keyof Product; arr = arr.filter(p => Boolean(p[key])); } } if (q.trim()) { const s = q.trim().toLowerCase(); arr = arr.filter((p) => [ p.productCode, p.productName, p.variantName, p.sku, p.gtin, p.brand, p.category, p.categoryPath, p.slug, p.thaiFdaNo, p.hazardClass, p.storageInstruction, p.channelsPublished, p.shopeeListingId, p.lazadaListingId, p.tiktokListingId, p.d2cUrl, p.rdOwner, p.manufacturer, p.note, p.attributesSummary, ...(p.tags ?? []) ] .filter((x): x is string => typeof x === "string") .some((x) => x.toLowerCase().includes(s)), ); } arr.sort((a, b) => { const tb = +new Date(b.updatedAt ?? b.createdAt); const ta = +new Date(a.updatedAt ?? a.createdAt); return tb - ta; }); return arr; }, [q, ptype, lifecycle, compliance, brand, category, channel]); 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]); 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]); /* ---------- Filter helpers / styles ---------- */ const Field = ({ label, children, className = "" }: { label: string; children: ReactNode; className?: string }) => (
{children}
); const inputBase = "h-10 w-full rounded-xl border border-neutral-200/80 bg-white/80 px-3 text-sm shadow-sm outline-none placeholder:text-neutral-400 focus:border-neutral-300 focus:ring-2 focus:ring-neutral-900/10"; const selectBase = "appearance-none h-10 w-full rounded-xl border border-neutral-200/80 bg-white/80 px-3 text-sm shadow-sm outline-none focus:border-neutral-300 focus:ring-2 focus:ring-neutral-900/10"; const activeFilters = [ ...(q.trim() ? [{ key: "q", label: "ค้นหา", value: q.trim() }] : []), ...(ptype !== "all" ? [{ key: "ptype", label: "ชนิด", value: TYPE_META[ptype].label }] : []), ...(lifecycle !== "all" ? [{ key: "lifecycle", label: "สถานะ", value: LIFECYCLE_META[lifecycle].label }] : []), ...(compliance !== "all" ? [{ key: "compliance", label: "Compliance", value: COMPLIANCE_META[compliance].label }] : []), ...(brand !== "all" ? [{ key: "brand", label: "แบรนด์", value: brand }] : []), ...(category !== "all" ? [{ key: "category", label: "หมวด", value: category }] : []), ...(channel !== "all" ? [{ key: "channel", label: "ช่องทาง", value: channel === "none" ? "ยังไม่เผยแพร่" : channel.toUpperCase() }] : []), ] as { key: string; label: string; value: string }[]; const clearChip = (key: string) => { switch (key) { case "q": setQ(""); break; case "ptype": setPtype("all"); break; case "lifecycle": setLifecycle("all"); break; case "compliance": setCompliance("all"); break; case "brand": setBrand("all"); break; case "category": setCategory("all"); break; case "channel": setChannel("all"); break; } }; const clearAllFilters = () => { setQ(""); setPtype("all"); setLifecycle("all"); setCompliance("all"); setBrand("all"); setCategory("all"); setChannel("all"); }; return ( <> {/* กว้างหน้า “นิ่ง” */}
{/* Header */}

สินค้า (Product Master)

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

{/* Filters (ใหม่ สะอาด + chips) */}
{/* Search */}
setQ(e.target.value)} className={`${inputBase} pl-9`} />
{/* Selects */}
{/* Chips ของตัวกรองที่ใช้งาน */} {activeFilters.length > 0 && (
{activeFilters.map((c) => ( {c.label}: {c.value} ))}
)}
{/* Table */}
{/* Group header (เส้น/พื้นที่ชัด) */} {topHeaderSegments.map((seg, idx) => ( ))} {/* Column labels */} {visibleCols.map((c) => ( ))} {rows.map((p, rowIdx) => ( {visibleCols.map((c) => { const raw = getProp(p, c.key); const numericKeys = ["weightGrams","dimLcm","dimWcm","dimHcm","netVolumeMl","shelfLifeMonths","ageRestrictionMin","ingredientsCount","priceListsCount","mfgLeadTimeDays","moq","packLevels"]; const content: ReactNode = c.format ? c.format(raw, p) : c.key === "listPrice" || c.key === "compareAtPrice" ? THB(raw as number | null | undefined) : numericKeys.includes(c.key as string) ? num(raw as number | null | undefined) : defaultCell(raw); const align = c.align === "right" ? "text-right" : ""; return ( ); })} ))}
{seg.groupTH}
{c.labelTH}
{c.key === "productCode" ? ( {content} ) : c.key === "tags" && Array.isArray(p.tags) ? (
{p.tags!.length ? p.tags!.map((t) => ( {t} )) : -}
) : ( content )}
{/* Customize dialog */} {openCustomize && (
setOpenCustomize(false)}>
e.stopPropagation()}>

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

เลือก {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 ( ); })}
))}
ระบบจะจำคอลัมน์ที่คุณเลือกไว้ในเบราว์เซอร์นี้
)}
); }