Fix Icon
This commit is contained in:
10
package-lock.json
generated
10
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"lucide-react": "^0.545.0",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
@@ -4594,6 +4595,15 @@
|
|||||||
"loose-envify": "cli.js"
|
"loose-envify": "cli.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lucide-react": {
|
||||||
|
"version": "0.545.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.545.0.tgz",
|
||||||
|
"integrity": "sha512-7r1/yUuflQDSt4f1bpn5ZAocyIxcTyVyBBChSVtBKn5M+392cPmI5YJMWOJKk/HUWGm5wg83chlAZtCcGbEZtw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.19",
|
"version": "0.30.19",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
|
"lucide-react": "^0.545.0",
|
||||||
"next": "15.5.4",
|
"next": "15.5.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
// File: src/app/(protected)/orders/page.tsx
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
import { useEffect, useMemo, useState, type ReactNode } from "react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
ShoppingBag, ShoppingCart, Music2, Globe, MessageSquare,
|
||||||
|
CreditCard, QrCode, HandCoins, Wallet, Landmark,
|
||||||
|
CheckCircle2, Hourglass, XCircle, RotateCcw,
|
||||||
|
Package, Cog, Truck, Ban,
|
||||||
|
ShieldCheck, ShieldAlert, ShieldX,
|
||||||
|
Factory, Building2, Store
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
/* ======================== Types ======================== */
|
||||||
type Channel = "shopee" | "lazada" | "tiktok" | "d2c" | "chat";
|
type Channel = "shopee" | "lazada" | "tiktok" | "d2c" | "chat";
|
||||||
type PaymentStatus = "paid" | "pending" | "failed" | "cod" | "refunded";
|
type PaymentStatus = "paid" | "pending" | "failed" | "cod" | "refunded";
|
||||||
type FulfillmentStatus =
|
type FulfillmentStatus =
|
||||||
@@ -104,6 +113,125 @@ type Column = {
|
|||||||
format?: (v: Order[ColumnKey], row: Order) => ReactNode;
|
format?: (v: Order[ColumnKey], row: Order) => ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ======================== Meta Mapping (Uppercase + Icon) ======================== */
|
||||||
|
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();
|
||||||
|
|
||||||
|
const IconLabel = ({ Icon, label, pillTone }: { Icon: LucideIcon; label: string; pillTone?: Tone }) => {
|
||||||
|
const content = (
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<Icon className="h-4 w-4" aria-hidden />
|
||||||
|
<span className="tracking-wide">{label}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
if (!pillTone) return content;
|
||||||
|
return (
|
||||||
|
<span className={`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[11px] ${pill(pillTone)}`}>
|
||||||
|
<Icon className="h-3.5 w-3.5" aria-hidden />
|
||||||
|
<span className="font-medium">{label}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Channel */
|
||||||
|
const CHANNEL_META = {
|
||||||
|
shopee: { label: U("Shopee"), Icon: ShoppingBag },
|
||||||
|
lazada: { label: U("Lazada"), Icon: ShoppingCart },
|
||||||
|
tiktok: { label: U("TikTok"), Icon: Music2 },
|
||||||
|
d2c: { label: U("D2C"), Icon: Globe },
|
||||||
|
chat: { label: U("Chat"), Icon: MessageSquare },
|
||||||
|
} satisfies Record<Channel, { label: string; Icon: LucideIcon }>;
|
||||||
|
|
||||||
|
const renderChannel = (c: Channel) => {
|
||||||
|
const { Icon, label } = CHANNEL_META[c];
|
||||||
|
return <IconLabel Icon={Icon} label={label} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Payment Method */
|
||||||
|
const PAYMENT_METHOD_META = {
|
||||||
|
card: { label: U("Card"), Icon: CreditCard },
|
||||||
|
bank_qr: { label: U("Bank QR"), Icon: QrCode },
|
||||||
|
cod: { label: U("COD"), Icon: HandCoins },
|
||||||
|
wallet: { label: U("Wallet"), Icon: Wallet },
|
||||||
|
transfer: { label: U("Transfer"), Icon: Landmark },
|
||||||
|
} satisfies Record<Order["paymentMethod"], { label: string; Icon: LucideIcon }>;
|
||||||
|
|
||||||
|
const renderPaymentMethod = (m: Order["paymentMethod"]) => {
|
||||||
|
const { Icon, label } = PAYMENT_METHOD_META[m];
|
||||||
|
return <IconLabel Icon={Icon} label={label} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Payment Status */
|
||||||
|
const PAYMENT_STATUS_META = {
|
||||||
|
paid: { label: U("Paid"), Icon: CheckCircle2, tone: "ok" as Tone },
|
||||||
|
pending: { label: U("Pending"), Icon: Hourglass, tone: "warn" as Tone },
|
||||||
|
failed: { label: U("Failed"), Icon: XCircle, tone: "bad" as Tone },
|
||||||
|
cod: { label: U("COD"), Icon: HandCoins, tone: "warn" as Tone },
|
||||||
|
refunded: { label: U("Refunded"), Icon: RotateCcw, tone: "neutral" as Tone },
|
||||||
|
} satisfies Record<PaymentStatus, { label: string; Icon: LucideIcon; tone: Tone }>;
|
||||||
|
|
||||||
|
const renderPaymentStatus = (s: PaymentStatus) => {
|
||||||
|
const { Icon, label, tone } = PAYMENT_STATUS_META[s];
|
||||||
|
return <IconLabel Icon={Icon} label={label} pillTone={tone} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Fulfillment Status */
|
||||||
|
const FULFILL_META = {
|
||||||
|
unfulfilled: { label: U("Unfulfilled"), Icon: Package, tone: "warn" as Tone },
|
||||||
|
processing: { label: U("Processing"), Icon: Cog, tone: "warn" as Tone },
|
||||||
|
shipped: { label: U("Shipped"), Icon: Truck, tone: "warn" as Tone },
|
||||||
|
delivered: { label: U("Delivered"), Icon: CheckCircle2, tone: "ok" as Tone },
|
||||||
|
returned: { label: U("Returned"), Icon: RotateCcw, tone: "bad" as Tone },
|
||||||
|
cancelled: { label: U("Cancelled"), Icon: Ban, tone: "bad" as Tone },
|
||||||
|
} satisfies Record<FulfillmentStatus, { label: string; Icon: LucideIcon; tone: Tone }>;
|
||||||
|
|
||||||
|
const renderFulfillment = (f: FulfillmentStatus) => {
|
||||||
|
const { Icon, label, tone } = FULFILL_META[f];
|
||||||
|
return <IconLabel Icon={Icon} label={label} pillTone={tone} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* Fraud Status (nullable) */
|
||||||
|
const FRAUD_META: Record<
|
||||||
|
NonNullable<Order["fraudCheckStatus"]>,
|
||||||
|
{ label: string; Icon: LucideIcon; tone: Tone }
|
||||||
|
> = {
|
||||||
|
clear: { label: U("Clear"), Icon: ShieldCheck, tone: "ok" },
|
||||||
|
review: { label: U("Review"), Icon: ShieldAlert, tone: "warn" },
|
||||||
|
hold: { label: U("Hold"), Icon: ShieldX, tone: "bad" },
|
||||||
|
};
|
||||||
|
const renderFraud = (s: Order["fraudCheckStatus"]) =>
|
||||||
|
s ? (
|
||||||
|
<IconLabel Icon={FRAUD_META[s].Icon} label={FRAUD_META[s].label} pillTone={FRAUD_META[s].tone} />
|
||||||
|
) : (
|
||||||
|
<span className={`rounded-full px-2 py-0.5 text-[11px] ${pill("neutral")}`}>N/A</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
/* Warehouse */
|
||||||
|
const WAREHOUSE_META = {
|
||||||
|
"BKK-DC": { label: U("BKK-DC"), Icon: Factory },
|
||||||
|
"CNX-HUB": { label: U("CNX-HUB"), Icon: Building2 },
|
||||||
|
"HKT-MINI": { label: U("HKT-MINI"), Icon: Store },
|
||||||
|
} satisfies Record<Order["warehouseCode"], { label: string; Icon: LucideIcon }>;
|
||||||
|
|
||||||
|
const renderWarehouse = (w: Order["warehouseCode"]) => {
|
||||||
|
const { Icon, label } = WAREHOUSE_META[w];
|
||||||
|
return <IconLabel Icon={Icon} label={label} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
/* For selects (native <option> เรนเดอร์ React icon ไม่ได้ จึงใช้ตัวอักษรอย่างเดียว) */
|
||||||
|
const CHANNELS = Object.keys(CHANNEL_META) as Channel[];
|
||||||
|
const PAYMENT_STATUSES = Object.keys(PAYMENT_STATUS_META) as PaymentStatus[];
|
||||||
|
const FULFILL_STATUSES = Object.keys(FULFILL_META) as FulfillmentStatus[];
|
||||||
|
|
||||||
|
/* ======================== Mock Data ======================== */
|
||||||
const MOCK_ORDERS: Order[] = [
|
const MOCK_ORDERS: Order[] = [
|
||||||
{
|
{
|
||||||
id: "1",
|
id: "1",
|
||||||
@@ -350,46 +478,12 @@ const MOCK_ORDERS: Order[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/* ======================== Utils ======================== */
|
||||||
const LS_KEY = "orders.visibleCols.v2";
|
const LS_KEY = "orders.visibleCols.v2";
|
||||||
|
|
||||||
const THB = (n: number) => `฿${n.toLocaleString()}`;
|
const THB = (n: number) => `฿${n.toLocaleString()}`;
|
||||||
const fmtDate = (iso?: string | null) =>
|
const fmtDate = (iso?: string | null) =>
|
||||||
iso ? new Date(iso).toLocaleString("th-TH", { hour12: false }) : "-";
|
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
|
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
|
||||||
return obj[key];
|
return obj[key];
|
||||||
@@ -397,8 +491,7 @@ function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
|
|||||||
|
|
||||||
function escapeCsv(v: unknown): string {
|
function escapeCsv(v: unknown): string {
|
||||||
if (v === null || v === undefined) return "";
|
if (v === null || v === undefined) return "";
|
||||||
const s =
|
const s = typeof v === "string" ? v : Array.isArray(v) ? v.join("|") : String(v);
|
||||||
typeof v === "string" ? v : Array.isArray(v) ? v.join("|") : String(v);
|
|
||||||
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
return /[",\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -428,27 +521,37 @@ function defaultCell(v: unknown): ReactNode {
|
|||||||
return String(v);
|
return String(v);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ======================== Columns ======================== */
|
||||||
const ALL_COLS: Column[] = [
|
const ALL_COLS: Column[] = [
|
||||||
{ key: "id", labelTH: "ไอดี", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
{ key: "id", labelTH: "ไอดี", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
||||||
{ key: "orderNo", labelTH: "เลขออเดอร์", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
{ key: "orderNo", labelTH: "เลขออเดอร์", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
||||||
{ key: "channel", labelTH: "ช่องทาง", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
{
|
||||||
|
key: "channel",
|
||||||
|
labelTH: "ช่องทาง",
|
||||||
|
groupTH: "เอกลักษณ์ & ช่องทาง",
|
||||||
|
format: (v) => renderChannel(v as Channel),
|
||||||
|
},
|
||||||
{ key: "orderRef", labelTH: "อ้างอิงภายใน", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
{ key: "orderRef", labelTH: "อ้างอิงภายใน", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
||||||
{ key: "channelOrderId", labelTH: "เลขออเดอร์ช่องทาง", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
{ key: "channelOrderId", labelTH: "เลขออเดอร์ช่องทาง", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
||||||
{ key: "marketplaceShopId", labelTH: "รหัสร้าน", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
{ key: "marketplaceShopId", labelTH: "รหัสร้าน", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
||||||
{ key: "marketplaceShopName", labelTH: "ชื่อร้าน", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
{ key: "marketplaceShopName", labelTH: "ชื่อร้าน", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
||||||
|
|
||||||
{ key: "createdByName", labelTH: "ผู้เปิดออเดอร์", groupTH: "ผู้เปิดออเดอร์" },
|
{ key: "createdByName", labelTH: "ผู้เปิดออเดอร์", groupTH: "ผู้เปิดออเดอร์" },
|
||||||
{ key: "createdByEmail", labelTH: "อีเมลผู้เปิด", groupTH: "ผู้เปิดออเดอร์" },
|
{ key: "createdByEmail", labelTH: "อีเมลผู้เปิด", groupTH: "ผู้เปิดออเดอร์" },
|
||||||
{ key: "createdById", labelTH: "รหัสผู้เปิด", groupTH: "ผู้เปิดออเดอร์" },
|
{ key: "createdById", labelTH: "รหัสผู้เปิด", groupTH: "ผู้เปิดออเดอร์" },
|
||||||
|
|
||||||
{ key: "dateCreated", labelTH: "สร้างเมื่อ", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) },
|
{ key: "dateCreated", labelTH: "สร้างเมื่อ", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) },
|
||||||
{ key: "dateDelivered", labelTH: "ถึงปลายทาง", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) },
|
{ key: "dateDelivered", labelTH: "ถึงปลายทาง", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) },
|
||||||
{ key: "datePaid", 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: "datePacked", labelTH: "แพ็คของ", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) },
|
||||||
{ key: "dateShipped", labelTH: "ส่งของ", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) },
|
{ key: "dateShipped", labelTH: "ส่งของ", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) },
|
||||||
{ key: "dateCancelled", 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: "customerId", labelTH: "รหัสลูกค้า", groupTH: "ลูกค้า" },
|
||||||
{ key: "customerName", labelTH: "ชื่อลูกค้า", groupTH: "ลูกค้า" },
|
{ key: "customerName", labelTH: "ชื่อลูกค้า", groupTH: "ลูกค้า" },
|
||||||
{ key: "customerEmail", labelTH: "อีเมลลูกค้า", groupTH: "ลูกค้า" },
|
{ key: "customerEmail", labelTH: "อีเมลลูกค้า", groupTH: "ลูกค้า" },
|
||||||
{ key: "customerPhone", labelTH: "เบอร์ลูกค้า", groupTH: "ลูกค้า" },
|
{ key: "customerPhone", labelTH: "เบอร์ลูกค้า", groupTH: "ลูกค้า" },
|
||||||
|
|
||||||
{ key: "shippingName", labelTH: "ชื่อผู้รับ", groupTH: "ที่อยู่จัดส่ง" },
|
{ key: "shippingName", labelTH: "ชื่อผู้รับ", groupTH: "ที่อยู่จัดส่ง" },
|
||||||
{ key: "shippingPhone", labelTH: "เบอร์ผู้รับ", groupTH: "ที่อยู่จัดส่ง" },
|
{ key: "shippingPhone", labelTH: "เบอร์ผู้รับ", groupTH: "ที่อยู่จัดส่ง" },
|
||||||
{ key: "shippingAddressLine1", labelTH: "ที่อยู่ (บรรทัด 1)", groupTH: "ที่อยู่จัดส่ง" },
|
{ key: "shippingAddressLine1", labelTH: "ที่อยู่ (บรรทัด 1)", groupTH: "ที่อยู่จัดส่ง" },
|
||||||
@@ -458,6 +561,7 @@ const ALL_COLS: Column[] = [
|
|||||||
{ key: "shippingProvince", labelTH: "จังหวัด", groupTH: "ที่อยู่จัดส่ง" },
|
{ key: "shippingProvince", labelTH: "จังหวัด", groupTH: "ที่อยู่จัดส่ง" },
|
||||||
{ key: "shippingPostcode", labelTH: "รหัสไปรษณีย์", groupTH: "ที่อยู่จัดส่ง" },
|
{ key: "shippingPostcode", labelTH: "รหัสไปรษณีย์", groupTH: "ที่อยู่จัดส่ง" },
|
||||||
{ key: "shippingCountry", labelTH: "ประเทศ", groupTH: "ที่อยู่จัดส่ง" },
|
{ key: "shippingCountry", labelTH: "ประเทศ", groupTH: "ที่อยู่จัดส่ง" },
|
||||||
|
|
||||||
{ key: "billingName", labelTH: "ชื่อสำหรับบิล", groupTH: "ออกใบเสร็จ/ภาษี" },
|
{ key: "billingName", labelTH: "ชื่อสำหรับบิล", groupTH: "ออกใบเสร็จ/ภาษี" },
|
||||||
{ key: "billingTaxId", labelTH: "เลขภาษี", groupTH: "ออกใบเสร็จ/ภาษี" },
|
{ key: "billingTaxId", labelTH: "เลขภาษี", groupTH: "ออกใบเสร็จ/ภาษี" },
|
||||||
{ key: "billingAddressLine1", labelTH: "ที่อยู่บิล (บรรทัด 1)", groupTH: "ออกใบเสร็จ/ภาษี" },
|
{ key: "billingAddressLine1", labelTH: "ที่อยู่บิล (บรรทัด 1)", groupTH: "ออกใบเสร็จ/ภาษี" },
|
||||||
@@ -466,11 +570,29 @@ const ALL_COLS: Column[] = [
|
|||||||
{ key: "billingProvince", labelTH: "จังหวัด (บิล)", groupTH: "ออกใบเสร็จ/ภาษี" },
|
{ key: "billingProvince", labelTH: "จังหวัด (บิล)", groupTH: "ออกใบเสร็จ/ภาษี" },
|
||||||
{ key: "billingPostcode", labelTH: "รหัสไปรษณีย์ (บิล)", groupTH: "ออกใบเสร็จ/ภาษี" },
|
{ key: "billingPostcode", labelTH: "รหัสไปรษณีย์ (บิล)", groupTH: "ออกใบเสร็จ/ภาษี" },
|
||||||
{ key: "billingCountry", labelTH: "ประเทศ (บิล)", groupTH: "ออกใบเสร็จ/ภาษี" },
|
{ key: "billingCountry", labelTH: "ประเทศ (บิล)", groupTH: "ออกใบเสร็จ/ภาษี" },
|
||||||
{ key: "paymentMethod", labelTH: "วิธีชำระ", groupTH: "การชำระเงิน" },
|
|
||||||
{ key: "paymentStatus", labelTH: "สถานะชำระ", groupTH: "การชำระเงิน", format: (v) => (<span className={`rounded-full px-2 py-0.5 text-[11px] ${mapPaymentColor(v as PaymentStatus)}`}>{v as string}</span>) },
|
{
|
||||||
|
key: "paymentMethod",
|
||||||
|
labelTH: "วิธีชำระ",
|
||||||
|
groupTH: "การชำระเงิน",
|
||||||
|
format: (v) => renderPaymentMethod(v as Order["paymentMethod"]),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "paymentStatus",
|
||||||
|
labelTH: "สถานะชำระ",
|
||||||
|
groupTH: "การชำระเงิน",
|
||||||
|
format: (v) => renderPaymentStatus(v as PaymentStatus),
|
||||||
|
},
|
||||||
{ key: "paymentProvider", labelTH: "ผู้ให้บริการชำระ", groupTH: "การชำระเงิน" },
|
{ key: "paymentProvider", labelTH: "ผู้ให้บริการชำระ", groupTH: "การชำระเงิน" },
|
||||||
{ key: "paymentTxId", labelTH: "รหัสรายการชำระ", groupTH: "การชำระเงิน" },
|
{ key: "paymentTxId", labelTH: "รหัสรายการชำระ", groupTH: "การชำระเงิน" },
|
||||||
{ key: "paymentFeeTHB", labelTH: "ค่าธรรมเนียม", groupTH: "การชำระเงิน", align: "right", format: (v) => (typeof v === "number" ? THB(v) : "-") },
|
{
|
||||||
|
key: "paymentFeeTHB",
|
||||||
|
labelTH: "ค่าธรรมเนียม",
|
||||||
|
groupTH: "การชำระเงิน",
|
||||||
|
align: "right",
|
||||||
|
format: (v) => (typeof v === "number" ? THB(v) : "-"),
|
||||||
|
},
|
||||||
|
|
||||||
{ key: "currency", labelTH: "สกุล", groupTH: "ราคา/ต้นทุนย่อย" },
|
{ key: "currency", labelTH: "สกุล", groupTH: "ราคา/ต้นทุนย่อย" },
|
||||||
{ key: "exchangeRate", labelTH: "เรท", groupTH: "ราคา/ต้นทุนย่อย", align: "right" },
|
{ key: "exchangeRate", labelTH: "เรท", groupTH: "ราคา/ต้นทุนย่อย", align: "right" },
|
||||||
{ key: "subtotalTHB", labelTH: "ยอดก่อนลด", groupTH: "ราคา/ต้นทุนย่อย", align: "right", format: (v) => THB(Number(v)) },
|
{ key: "subtotalTHB", labelTH: "ยอดก่อนลด", groupTH: "ราคา/ต้นทุนย่อย", align: "right", format: (v) => THB(Number(v)) },
|
||||||
@@ -480,36 +602,81 @@ const ALL_COLS: Column[] = [
|
|||||||
{ key: "packagingFeeTHB", labelTH: "ค่าบรรจุภัณฑ์", 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: "taxTHB", labelTH: "ภาษี", groupTH: "ราคา/ต้นทุนย่อย", align: "right", format: (v) => THB(Number(v)) },
|
||||||
{ key: "totalTHB", 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: "itemsCount", labelTH: "จำนวนชิ้น", groupTH: "สินค้า/โลจิสติกส์", align: "right" },
|
||||||
{ key: "totalWeightGrams", labelTH: "น้ำหนัก(g)", groupTH: "สินค้า/โลจิสติกส์", align: "right" },
|
{ key: "totalWeightGrams", labelTH: "น้ำหนัก(g)", groupTH: "สินค้า/โลจิสติกส์", align: "right" },
|
||||||
{ key: "packageCount", labelTH: "จำนวนพัสดุ", groupTH: "สินค้า/โลจิสติกส์", align: "right" },
|
{ key: "packageCount", labelTH: "จำนวนพัสดุ", groupTH: "สินค้า/โลจิสติกส์", align: "right" },
|
||||||
{ key: "dimensionsCmL", labelTH: "ยาว(cm)", groupTH: "สินค้า/โลจิสติกส์", align: "right" },
|
{ key: "dimensionsCmL", labelTH: "ยาว(cm)", groupTH: "สินค้า/โลจิสติกส์", align: "right" },
|
||||||
{ key: "dimensionsCmW", labelTH: "กว้าง(cm)", groupTH: "สินค้า/โลจิสติกส์", align: "right" },
|
{ key: "dimensionsCmW", labelTH: "กว้าง(cm)", groupTH: "สินค้า/โลจิสติกส์", align: "right" },
|
||||||
{ key: "dimensionsCmH", labelTH: "สูง(cm)", groupTH: "สินค้า/โลจิสติกส์", align: "right" },
|
{ key: "dimensionsCmH", labelTH: "สูง(cm)", groupTH: "สินค้า/โลจิสติกส์", align: "right" },
|
||||||
{ key: "fulfillStatus", labelTH: "สถานะจัดส่ง", groupTH: "สถานะจัดส่ง", format: (v) => (<span className={`rounded-full px-2 py-0.5 text-[11px] ${mapFulfillmentColor(v as FulfillmentStatus)}`}>{v as string}</span>) },
|
|
||||||
|
{
|
||||||
|
key: "fulfillStatus",
|
||||||
|
labelTH: "สถานะจัดส่ง",
|
||||||
|
groupTH: "สถานะจัดส่ง",
|
||||||
|
format: (v) => renderFulfillment(v as FulfillmentStatus),
|
||||||
|
},
|
||||||
{ key: "shipBy", labelTH: "กำหนดส่งภายใน", groupTH: "สถานะจัดส่ง", format: (v) => fmtDate(v as string | null | undefined) },
|
{ key: "shipBy", labelTH: "กำหนดส่งภายใน", groupTH: "สถานะจัดส่ง", format: (v) => fmtDate(v as string | null | undefined) },
|
||||||
{ key: "carrier", labelTH: "ขนส่ง", groupTH: "สถานะจัดส่ง" },
|
{ key: "carrier", labelTH: "ขนส่ง", groupTH: "สถานะจัดส่ง" },
|
||||||
{ key: "serviceLevel", labelTH: "บริการ", groupTH: "สถานะจัดส่ง" },
|
{ key: "serviceLevel", labelTH: "บริการ", groupTH: "สถานะจัดส่ง" },
|
||||||
{ key: "trackingNo", labelTH: "เลขติดตาม", groupTH: "สถานะจัดส่ง" },
|
{ key: "trackingNo", labelTH: "เลขติดตาม", groupTH: "สถานะจัดส่ง" },
|
||||||
{ key: "warehouseCode", labelTH: "คลังสินค้า", groupTH: "โกดัง/ปฏิบัติการ" },
|
|
||||||
|
{
|
||||||
|
key: "warehouseCode",
|
||||||
|
labelTH: "คลังสินค้า",
|
||||||
|
groupTH: "โกดัง/ปฏิบัติการ",
|
||||||
|
format: (v) => renderWarehouse(v as Order["warehouseCode"]),
|
||||||
|
},
|
||||||
{ key: "pickListId", labelTH: "ใบหยิบ", groupTH: "โกดัง/ปฏิบัติการ" },
|
{ key: "pickListId", labelTH: "ใบหยิบ", groupTH: "โกดัง/ปฏิบัติการ" },
|
||||||
{ key: "waveId", labelTH: "รหัสเวฟ", groupTH: "โกดัง/ปฏิบัติการ" },
|
{ key: "waveId", labelTH: "รหัสเวฟ", groupTH: "โกดัง/ปฏิบัติการ" },
|
||||||
{ key: "binFrom", labelTH: "หยิบจากช่อง", groupTH: "โกดัง/ปฏิบัติการ" },
|
{ key: "binFrom", labelTH: "หยิบจากช่อง", groupTH: "โกดัง/ปฏิบัติการ" },
|
||||||
{ key: "binTo", 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: "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: "rmaReason", labelTH: "สาเหตุเคลม", groupTH: "ธง/โน้ต/ความเสี่ยง" },
|
||||||
{ key: "noteSeller", labelTH: "โน้ตผู้ขาย", groupTH: "ธง/โน้ต/ความเสี่ยง" },
|
{ key: "noteSeller", labelTH: "โน้ตผู้ขาย", groupTH: "ธง/โน้ต/ความเสี่ยง" },
|
||||||
{ key: "noteBuyer", 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: "slaShipOnTime",
|
||||||
{ key: "fragile", labelTH: "แตกหักง่าย", groupTH: "ธง/โน้ต/ความเสี่ยง", format: (v) => (typeof v === "boolean" ? (v ? "YES" : "NO") : "NO") },
|
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: "priorityLevel", labelTH: "ลำดับความสำคัญ", groupTH: "ธง/โน้ต/ความเสี่ยง", align: "right" },
|
||||||
{ key: "riskScore", labelTH: "คะแนนเสี่ยง", groupTH: "ธง/โน้ต/ความเสี่ยง", align: "right" },
|
{ key: "riskScore", labelTH: "คะแนนเสี่ยง", groupTH: "ธง/โน้ต/ความเสี่ยง", align: "right" },
|
||||||
{ key: "holdReason", labelTH: "เหตุผลพักออเดอร์", groupTH: "ธง/โน้ต/ความเสี่ยง" },
|
{ key: "holdReason", labelTH: "เหตุผลพักออเดอร์", groupTH: "ธง/โน้ต/ความเสี่ยง" },
|
||||||
{ key: "fraudCheckStatus", labelTH: "สถานะ Fraud", groupTH: "ธง/โน้ต/ความเสี่ยง" },
|
{
|
||||||
|
key: "fraudCheckStatus",
|
||||||
|
labelTH: "สถานะ Fraud",
|
||||||
|
groupTH: "ธง/โน้ต/ความเสี่ยง",
|
||||||
|
format: (v) => renderFraud(v as Order["fraudCheckStatus"]),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/* ======================== Visible/Groups ======================== */
|
||||||
const DEFAULT_VISIBLE: ColumnKey[] = [
|
const DEFAULT_VISIBLE: ColumnKey[] = [
|
||||||
"orderNo",
|
"orderNo",
|
||||||
"dateCreated",
|
"dateCreated",
|
||||||
@@ -541,6 +708,7 @@ const GROUPS: { titleTH: string; keys: ColumnKey[] }[] = [
|
|||||||
{ titleTH: "ธง/โน้ต/ความเสี่ยง", keys: ["tags", "rmaFlag", "rmaReason", "noteSeller", "noteBuyer", "slaShipOnTime", "giftWrap", "fragile", "priorityLevel", "riskScore", "holdReason", "fraudCheckStatus"] },
|
{ titleTH: "ธง/โน้ต/ความเสี่ยง", keys: ["tags", "rmaFlag", "rmaReason", "noteSeller", "noteBuyer", "slaShipOnTime", "giftWrap", "fragile", "priorityLevel", "riskScore", "holdReason", "fraudCheckStatus"] },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/* ======================== Page ======================== */
|
||||||
export default function OrdersPage() {
|
export default function OrdersPage() {
|
||||||
const [q, setQ] = useState("");
|
const [q, setQ] = useState("");
|
||||||
const [channel, setChannel] = useState<"all" | Channel>("all");
|
const [channel, setChannel] = useState<"all" | Channel>("all");
|
||||||
@@ -562,6 +730,7 @@ export default function OrdersPage() {
|
|||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const persistVisible = (keys: ColumnKey[]) => {
|
const persistVisible = (keys: ColumnKey[]) => {
|
||||||
setVisibleKeys(keys);
|
setVisibleKeys(keys);
|
||||||
try {
|
try {
|
||||||
@@ -620,13 +789,10 @@ export default function OrdersPage() {
|
|||||||
<>
|
<>
|
||||||
{/* ทำให้กว้างหน้า "นิ่งเท่ากัน" เสมอ โดยจองที่สกรอลล์บาร์ */}
|
{/* ทำให้กว้างหน้า "นิ่งเท่ากัน" เสมอ โดยจองที่สกรอลล์บาร์ */}
|
||||||
<style jsx global>{`
|
<style jsx global>{`
|
||||||
html { scrollbar-gutter: stable both-edges; }
|
html { scrollbar-gutter: stable both-edges; }
|
||||||
@supports not (scrollbar-gutter: stable both-edges) {
|
@supports not (scrollbar-gutter: stable both-edges) { html { overflow-y: scroll; } }
|
||||||
html { overflow-y: scroll; }
|
`}</style>
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
|
|
||||||
{/* ผูกหน้าเข้ากับกรอบคงที่ */}
|
|
||||||
<div className="mx-auto w-full max-w-7xl px-6 space-y-6">
|
<div className="mx-auto w-full max-w-7xl px-6 space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
|
||||||
@@ -666,6 +832,7 @@ export default function OrdersPage() {
|
|||||||
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"
|
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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<select
|
<select
|
||||||
value={channel}
|
value={channel}
|
||||||
@@ -673,13 +840,14 @@ export default function OrdersPage() {
|
|||||||
className="w-full rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm outline-none"
|
className="w-full rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm outline-none"
|
||||||
>
|
>
|
||||||
<option value="all">ทุกช่องทาง</option>
|
<option value="all">ทุกช่องทาง</option>
|
||||||
<option value="shopee">Shopee</option>
|
{CHANNELS.map((c) => (
|
||||||
<option value="lazada">Lazada</option>
|
<option key={c} value={c}>
|
||||||
<option value="tiktok">TikTok</option>
|
{CHANNEL_META[c].label}
|
||||||
<option value="d2c">เว็บไซต์ (D2C)</option>
|
</option>
|
||||||
<option value="chat">แชต</option>
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<select
|
<select
|
||||||
value={payment}
|
value={payment}
|
||||||
@@ -687,13 +855,14 @@ export default function OrdersPage() {
|
|||||||
className="w-full rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm outline-none"
|
className="w-full rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm outline-none"
|
||||||
>
|
>
|
||||||
<option value="all">การชำระเงิน: ทั้งหมด</option>
|
<option value="all">การชำระเงิน: ทั้งหมด</option>
|
||||||
<option value="paid">ชำระแล้ว (paid)</option>
|
{PAYMENT_STATUSES.map((ps) => (
|
||||||
<option value="pending">รอชำระ (pending)</option>
|
<option key={ps} value={ps}>
|
||||||
<option value="failed">ล้มเหลว (failed)</option>
|
{PAYMENT_STATUS_META[ps].label}
|
||||||
<option value="cod">COD</option>
|
</option>
|
||||||
<option value="refunded">คืนเงิน (refunded)</option>
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="sm:col-span-2">
|
<div className="sm:col-span-2">
|
||||||
<select
|
<select
|
||||||
value={fulfillment}
|
value={fulfillment}
|
||||||
@@ -701,18 +870,17 @@ export default function OrdersPage() {
|
|||||||
className="w-full rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm outline-none"
|
className="w-full rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm outline-none"
|
||||||
>
|
>
|
||||||
<option value="all">จัดส่ง: ทั้งหมด</option>
|
<option value="all">จัดส่ง: ทั้งหมด</option>
|
||||||
<option value="unfulfilled">ยังไม่จัดส่ง (unfulfilled)</option>
|
{FULFILL_STATUSES.map((fs) => (
|
||||||
<option value="processing">กำลังดำเนินการ (processing)</option>
|
<option key={fs} value={fs}>
|
||||||
<option value="shipped">ส่งแล้ว (shipped)</option>
|
{FULFILL_META[fs].label}
|
||||||
<option value="delivered">ถึงปลายทาง (delivered)</option>
|
</option>
|
||||||
<option value="returned">ตีกลับ (returned)</option>
|
))}
|
||||||
<option value="cancelled">ยกเลิก (cancelled)</option>
|
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Table — เก็บเนื้อหาทั้งหมดด้วยสกรอลล์แนวนอน และสกรอลล์บาร์ชิดขอบการ์ด */}
|
{/* Table */}
|
||||||
<section className="rounded-3xl border border-neutral-200/70 bg-white/80 p-5 shadow-[0_10px_30px_-12px_rgba(0,0,0,0.08)]">
|
<section className="rounded-3xl border border-neutral-200/70 bg-white/80 p-5 shadow-[0_10px_30px_-12px_rgba(0,0,0,0.08)]">
|
||||||
<div className="-mx-5 overflow-x-auto">
|
<div className="-mx-5 overflow-x-auto">
|
||||||
<div className="px-5">
|
<div className="px-5">
|
||||||
@@ -786,6 +954,7 @@ export default function OrdersPage() {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{/* Customize dialog */}
|
||||||
{openCustomize && (
|
{openCustomize && (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-40 flex items-end justify-center bg-black/40 p-3 sm:items-center"
|
className="fixed inset-0 z-40 flex items-end justify-center bg-black/40 p-3 sm:items-center"
|
||||||
|
|||||||
@@ -1,17 +1,78 @@
|
|||||||
|
// src/app/component/layout/AppShell.tsx
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ReactNode, useEffect, useMemo, useState } from "react";
|
import { ReactNode, useEffect, useMemo, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import type { UserSession } from "@/types/auth";
|
import type { UserSession } from "@/types/auth";
|
||||||
|
import type { NavItem as ServerNavItem, IconKey } from "@/lib/nav";
|
||||||
|
|
||||||
type NavItem = { label: string; href: string; icon?: React.ReactNode; match?: RegExp };
|
import type { LucideIcon } from "lucide-react";
|
||||||
type Props = { user: UserSession; nav: NavItem[]; children: ReactNode };
|
import {
|
||||||
|
// groups
|
||||||
|
Home, Settings, Megaphone, Warehouse as WarehouseIcon, Wrench, ShoppingCart,
|
||||||
|
Bell, BarChart3, CircleDollarSign,
|
||||||
|
// pages/common
|
||||||
|
LayoutDashboard, Users, UserRound, PhoneCall, MessageSquare, Headset,
|
||||||
|
BookOpen, Newspaper, Bot,
|
||||||
|
// sales & catalog
|
||||||
|
Package, Boxes, Tag, FileText, Repeat,
|
||||||
|
// services
|
||||||
|
CalendarClock, Route, Calendar, UserCog, ClipboardList, Timer, FileSignature, Box, BadgeCheck,
|
||||||
|
// procurement & warehouse
|
||||||
|
Store, Inbox, Truck, ClipboardCheck,
|
||||||
|
// marketing
|
||||||
|
BadgePercent, Gift,
|
||||||
|
// finance
|
||||||
|
Receipt, CreditCard, Calculator, PiggyBank, ListChecks,
|
||||||
|
// admin/integrations
|
||||||
|
PlugZap, Webhook, KeyRound, FileSearch, Building2, Activity, CodeSquare, Smile,
|
||||||
|
// chrome
|
||||||
|
Menu, PanelLeftOpen, PanelLeftClose,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
user: UserSession;
|
||||||
|
nav: ServerNavItem[];
|
||||||
|
children: ReactNode; // <<<< เพิ่ม children ลงใน Props
|
||||||
|
};
|
||||||
|
|
||||||
|
// map string key -> actual Lucide icon
|
||||||
|
const ICONS: Record<IconKey, LucideIcon> = {
|
||||||
|
// groups
|
||||||
|
home: Home, sales: ShoppingCart, services: Wrench, procurement: Store, warehouse: WarehouseIcon,
|
||||||
|
support: Headset, marketing: Megaphone, finance: CircleDollarSign, analytics: BarChart3, admin: Settings,
|
||||||
|
// common
|
||||||
|
dashboard: LayoutDashboard, myTasks: ClipboardCheck, notifications: Bell,
|
||||||
|
// sales & catalog
|
||||||
|
customers: Users, contacts: UserRound, leads: PhoneCall, orders: ShoppingCart, quotes: FileText,
|
||||||
|
subscriptions: Repeat, products: Package, bundles: Boxes, pricing: Tag, returns: Repeat,
|
||||||
|
// services
|
||||||
|
serviceCatalog: Tag, servicePricing: Tag, serviceOrders: Wrench, dispatchBoard: Route,
|
||||||
|
schedule: Calendar, resources: UserCog, projects: ClipboardList, timesheets: Timer,
|
||||||
|
contracts: FileSignature, installedBase: Box, warrantyClaims: BadgeCheck,
|
||||||
|
// procurement & warehouse
|
||||||
|
suppliers: Store, purchaseOrders: ClipboardCheck, grn: Inbox,
|
||||||
|
inventory: Boxes, receiving: Inbox, picking: ClipboardList, packing: Package, shipments: Truck,
|
||||||
|
// support & knowledge
|
||||||
|
tickets: MessageSquare, sla: Timer, csat: Smile, aiChat: Bot, knowledgeBase: BookOpen,
|
||||||
|
// marketing
|
||||||
|
campaigns: Megaphone, coupons: BadgePercent, loyalty: Gift, news: Newspaper, pages: FileText,
|
||||||
|
// finance
|
||||||
|
invoices: Receipt, payments: CreditCard, refunds: Repeat, generalLedger: PiggyBank, reconciliation: ListChecks,
|
||||||
|
// analytics
|
||||||
|
reports: BarChart3, dashboards: LayoutDashboard,
|
||||||
|
// admin & system
|
||||||
|
users: Users, rolesPermissions: UserCog, tenants: Building2, settings: Settings,
|
||||||
|
flashExpress: Truck, tiktokShop: PlugZap, psp: CreditCard, webhooks: Webhook, apiKeys: KeyRound,
|
||||||
|
compliance: CircleDollarSign, documents: FileText, eSign: FileSignature, auditTrail: FileSearch,
|
||||||
|
systemHealth: Activity, logs: ListChecks, developerSandbox: CodeSquare,
|
||||||
|
// misc
|
||||||
|
kms: BookOpen, chat: Bot,
|
||||||
|
};
|
||||||
|
|
||||||
export default function AppShell({ user, nav, children }: Props) {
|
export default function AppShell({ user, nav, children }: Props) {
|
||||||
// เดสก์ท็อป: พับ/กางไซด์บาร์
|
|
||||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||||
// มือถือ: เปิด/ปิด drawer
|
|
||||||
const [mobileOpen, setMobileOpen] = useState(false);
|
const [mobileOpen, setMobileOpen] = useState(false);
|
||||||
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
@@ -23,23 +84,23 @@ export default function AppShell({ user, nav, children }: Props) {
|
|||||||
}, [user?.email]);
|
}, [user?.email]);
|
||||||
|
|
||||||
const year = new Date().getFullYear();
|
const year = new Date().getFullYear();
|
||||||
const isActive = (item: NavItem): boolean =>
|
|
||||||
Boolean((item.match && item.match.test(pathname)) || pathname === item.href);
|
|
||||||
|
|
||||||
// ปิด drawer อัตโนมัติเมื่อเปลี่ยนหน้า
|
const isActive = (item: ServerNavItem): boolean => {
|
||||||
useEffect(() => {
|
if (item.match) {
|
||||||
setMobileOpen(false);
|
try { return new RegExp(item.match).test(pathname); }
|
||||||
}, [pathname]);
|
catch { return pathname.startsWith(item.match); }
|
||||||
|
}
|
||||||
|
return pathname === item.href;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => { setMobileOpen(false); }, [pathname]);
|
||||||
|
|
||||||
// ล็อกสกอลล์หน้าเมื่อ drawer เปิด (กันเลื่อนพื้นหลัง)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = document.documentElement;
|
const el = document.documentElement;
|
||||||
if (mobileOpen) {
|
if (mobileOpen) {
|
||||||
const prev = el.style.overflow;
|
const prev = el.style.overflow;
|
||||||
el.style.overflow = "hidden";
|
el.style.overflow = "hidden";
|
||||||
return () => {
|
return () => { el.style.overflow = prev; };
|
||||||
el.style.overflow = prev;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}, [mobileOpen]);
|
}, [mobileOpen]);
|
||||||
|
|
||||||
@@ -75,10 +136,7 @@ export default function AppShell({ user, nav, children }: Props) {
|
|||||||
{/* Tenant pill */}
|
{/* Tenant pill */}
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<div
|
<div
|
||||||
className={[
|
className="rounded-2xl border border-neutral-200/70 bg-white/70 shadow-sm backdrop-blur-sm px-3 py-2 flex items-center gap-2"
|
||||||
"rounded-2xl border border-neutral-200/70 bg-white/70 shadow-sm backdrop-blur-sm",
|
|
||||||
"px-3 py-2 flex items-center gap-2",
|
|
||||||
].join(" ")}
|
|
||||||
title="Current tenant"
|
title="Current tenant"
|
||||||
>
|
>
|
||||||
<span className="inline-block h-2.5 w-2.5 rounded-full bg-emerald-500/90 shadow-[0_0_0_3px_rgba(16,185,129,0.12)]" />
|
<span className="inline-block h-2.5 w-2.5 rounded-full bg-emerald-500/90 shadow-[0_0_0_3px_rgba(16,185,129,0.12)]" />
|
||||||
@@ -94,6 +152,7 @@ export default function AppShell({ user, nav, children }: Props) {
|
|||||||
<nav className="mt-4 flex-1 overflow-y-auto space-y-1 px-2 pr-3">
|
<nav className="mt-4 flex-1 overflow-y-auto space-y-1 px-2 pr-3">
|
||||||
{nav.map((item) => {
|
{nav.map((item) => {
|
||||||
const active = isActive(item);
|
const active = isActive(item);
|
||||||
|
const Icon = item.icon ? ICONS[item.icon] : null;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
@@ -110,7 +169,7 @@ export default function AppShell({ user, nav, children }: Props) {
|
|||||||
"h-8 w-8",
|
"h-8 w-8",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{item.icon ?? <span className="h-1.5 w-1.5 rounded-full bg-current" />}
|
{Icon ? <Icon className="h-4 w-4" aria-hidden /> : <span className="h-1.5 w-1.5 rounded-full bg-current" />}
|
||||||
</span>
|
</span>
|
||||||
{sidebarOpen && <span className="truncate">{item.label}</span>}
|
{sidebarOpen && <span className="truncate">{item.label}</span>}
|
||||||
</Link>
|
</Link>
|
||||||
@@ -130,25 +189,27 @@ export default function AppShell({ user, nav, children }: Props) {
|
|||||||
<header className="sticky top-0 z-40 border-b border-neutral-200/70 bg-white/70 backdrop-blur">
|
<header className="sticky top-0 z-40 border-b border-neutral-200/70 bg-white/70 backdrop-blur">
|
||||||
<div className="mx-auto flex h-16 w-full max-w-[1280px] items-center justify-between px-4">
|
<div className="mx-auto flex h-16 w-full max-w-[1280px] items-center justify-between px-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{/* ปุ่มมือถือ: เปิด/ปิด drawer */}
|
{/* Mobile drawer toggle */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setMobileOpen((v) => !v)}
|
onClick={() => setMobileOpen((v) => !v)}
|
||||||
className="md:hidden rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm hover:bg-neutral-100/80"
|
className="md:hidden inline-flex items-center gap-2 rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm hover:bg-neutral-100/80"
|
||||||
aria-label="Toggle menu"
|
aria-label="Toggle menu"
|
||||||
aria-expanded={mobileOpen}
|
aria-expanded={mobileOpen}
|
||||||
aria-controls="mobile-drawer"
|
aria-controls="mobile-drawer"
|
||||||
>
|
>
|
||||||
|
<Menu className="h-4 w-4" aria-hidden />
|
||||||
เมนู
|
เมนู
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* (ทางเลือก) ปุ่มเดสก์ท็อป: พับ/กาง sidebar */}
|
{/* Desktop collapse toggle */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setSidebarOpen((v) => !v)}
|
onClick={() => setSidebarOpen((v) => !v)}
|
||||||
className="hidden md:inline-flex rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-xs text-neutral-600 shadow-sm hover:bg-neutral-100/80"
|
className="hidden md:inline-flex items-center gap-2 rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-xs text-neutral-600 shadow-sm hover:bg-neutral-100/80"
|
||||||
aria-label="Collapse sidebar"
|
aria-label={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
|
||||||
title={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
|
title={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
|
||||||
>
|
>
|
||||||
{sidebarOpen ? "◀︎" : "▶︎"}
|
{sidebarOpen ? <PanelLeftClose className="h-4 w-4" aria-hidden /> : <PanelLeftOpen className="h-4 w-4" aria-hidden />}
|
||||||
|
{sidebarOpen ? "ซ่อน" : "แสดง"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="hidden items-center gap-2 text-sm text-neutral-500 sm:flex">
|
<div className="hidden items-center gap-2 text-sm text-neutral-500 sm:flex">
|
||||||
@@ -191,9 +252,7 @@ export default function AppShell({ user, nav, children }: Props) {
|
|||||||
{/* Mobile drawer + backdrop */}
|
{/* Mobile drawer + backdrop */}
|
||||||
{mobileOpen && (
|
{mobileOpen && (
|
||||||
<div className="fixed inset-0 z-[60] md:hidden" role="dialog" aria-modal="true" id="mobile-drawer">
|
<div className="fixed inset-0 z-[60] md:hidden" role="dialog" aria-modal="true" id="mobile-drawer">
|
||||||
{/* Backdrop */}
|
<div className="absolute inset-0 bg-black/40" onClick={() => setMobileOpen(false)} aria-hidden />
|
||||||
<div className="absolute inset-0 bg-black/40" onClick={() => setMobileOpen(false)} />
|
|
||||||
{/* Panel */}
|
|
||||||
<div className="absolute left-0 top-0 h-dvh w-72 max-w-[85vw] bg-white shadow-xl border-r border-neutral-200/70 overflow-y-auto">
|
<div className="absolute left-0 top-0 h-dvh w-72 max-w-[85vw] bg-white shadow-xl border-r border-neutral-200/70 overflow-y-auto">
|
||||||
{/* Brand */}
|
{/* Brand */}
|
||||||
<div className="h-16 flex items-center px-4">
|
<div className="h-16 flex items-center px-4">
|
||||||
@@ -212,10 +271,7 @@ export default function AppShell({ user, nav, children }: Props) {
|
|||||||
|
|
||||||
{/* Tenant pill */}
|
{/* Tenant pill */}
|
||||||
<div className="px-4">
|
<div className="px-4">
|
||||||
<div
|
<div className="rounded-2xl border border-neutral-200/70 bg-white/70 shadow-sm backdrop-blur-sm px-3 py-2 flex items-center gap-2" title="Current tenant">
|
||||||
className="rounded-2xl border border-neutral-200/70 bg-white/70 shadow-sm backdrop-blur-sm px-3 py-2 flex items-center gap-2"
|
|
||||||
title="Current tenant"
|
|
||||||
>
|
|
||||||
<span className="inline-block h-2.5 w-2.5 rounded-full bg-emerald-500/90 shadow-[0_0_0_3px_rgba(16,185,129,0.12)]" />
|
<span className="inline-block h-2.5 w-2.5 rounded-full bg-emerald-500/90 shadow-[0_0_0_3px_rgba(16,185,129,0.12)]" />
|
||||||
<span className="truncate text-xs text-neutral-700">{user?.tenantKey ?? "—"}</span>
|
<span className="truncate text-xs text-neutral-700">{user?.tenantKey ?? "—"}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -225,10 +281,12 @@ export default function AppShell({ user, nav, children }: Props) {
|
|||||||
<nav className="mt-4 space-y-1 px-2 pr-3 pb-6">
|
<nav className="mt-4 space-y-1 px-2 pr-3 pb-6">
|
||||||
{nav.map((item) => {
|
{nav.map((item) => {
|
||||||
const active = isActive(item);
|
const active = isActive(item);
|
||||||
|
const Icon = item.icon ? ICONS[item.icon] : null;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
key={item.href}
|
||||||
href={item.href}
|
href={item.href}
|
||||||
|
onClick={() => setMobileOpen(false)}
|
||||||
className={[
|
className={[
|
||||||
"group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm transition-all",
|
"group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm transition-all",
|
||||||
active ? "bg-neutral-900 text-white shadow-sm" : "text-neutral-700 hover:bg-neutral-100/80",
|
active ? "bg-neutral-900 text-white shadow-sm" : "text-neutral-700 hover:bg-neutral-100/80",
|
||||||
@@ -241,7 +299,7 @@ export default function AppShell({ user, nav, children }: Props) {
|
|||||||
"h-8 w-8",
|
"h-8 w-8",
|
||||||
].join(" ")}
|
].join(" ")}
|
||||||
>
|
>
|
||||||
{item.icon ?? <span className="h-1.5 w-1.5 rounded-full bg-current" />}
|
{Icon ? <Icon className="h-4 w-4" aria-hidden /> : <span className="h-1.5 w-1.5 rounded-full bg-current" />}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate">{item.label}</span>
|
<span className="truncate">{item.label}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
451
src/lib/nav.ts
451
src/lib/nav.ts
@@ -1,165 +1,131 @@
|
|||||||
// nav.ts — รวมทุกอย่างไว้ไฟล์เดียว (อัปเดตให้ครอบคลุม “บริการ” ด้วย)
|
/* ---------- Types ---------- */
|
||||||
|
|
||||||
// ---------- Types ----------
|
|
||||||
export type Role =
|
export type Role =
|
||||||
| "owner"
|
| "owner" | "admin" | "ops" | "sales" | "finance" | "warehouse"
|
||||||
| "admin"
|
| "procurement" | "support" | "marketing" | "hr";
|
||||||
| "ops"
|
|
||||||
| "sales"
|
|
||||||
| "finance"
|
|
||||||
| "warehouse"
|
|
||||||
| "procurement"
|
|
||||||
| "support"
|
|
||||||
| "marketing"
|
|
||||||
| "hr";
|
|
||||||
|
|
||||||
export type ModuleKey =
|
export type ModuleKey =
|
||||||
// Core commerce
|
// Core commerce
|
||||||
| "CRM"
|
| "CRM" | "OMS" | "WMS" | "Returns" | "Billing" | "Accounting" | "Loyalty" | "Marketing"
|
||||||
| "OMS"
|
| "Reports" | "Audit" | "Integrations" | "CMS" | "KMS" | "AIChat" | "Appointments"
|
||||||
| "WMS"
|
// Service business
|
||||||
| "Returns"
|
| "ServiceCatalog" | "ServiceOrders" | "ServiceProjects" | "Timesheets" | "Contracts"
|
||||||
| "Billing"
|
| "InstalledBase" | "Warranty"
|
||||||
| "Accounting"
|
// Enablement & Compliance
|
||||||
| "Loyalty"
|
| "Documents" | "ESign" | "Compliance";
|
||||||
| "Marketing"
|
|
||||||
| "Reports"
|
|
||||||
| "Audit"
|
|
||||||
| "Integrations"
|
|
||||||
| "CMS"
|
|
||||||
| "KMS"
|
|
||||||
| "AIChat"
|
|
||||||
| "Appointments"
|
|
||||||
// Service business (ใหม่)
|
|
||||||
| "ServiceCatalog" // ขายบริการ/แพ็กเกจ/ราคา
|
|
||||||
| "ServiceOrders" // Work Orders / Service Orders
|
|
||||||
| "ServiceProjects" // โครงการบริการ/PSA
|
|
||||||
| "Timesheets" // ลงเวลา/ต้นทุนแรงงาน
|
|
||||||
| "Contracts" // สัญญา/Entitlements/SLAs
|
|
||||||
| "InstalledBase" // สินทรัพย์ลูกค้า/Serial/ประวัติซ่อม
|
|
||||||
| "Warranty" // การรับประกัน/CLAIM
|
|
||||||
// Enablement & Compliance (แนะนำให้มี)
|
|
||||||
| "Documents" // เอกสาร/เทมเพลต
|
|
||||||
| "ESign" // ลายเซ็นอิเล็กทรอนิกส์
|
|
||||||
| "Compliance"; // PDPA/Consent/Retention/DLP
|
|
||||||
|
|
||||||
export type NavItem = { label: string; href: string };
|
/** ชื่อไอคอนเป็น string serializable เท่านั้น */
|
||||||
|
export type IconKey =
|
||||||
|
// groups
|
||||||
|
| "home" | "sales" | "services" | "procurement" | "warehouse" | "support"
|
||||||
|
| "marketing" | "finance" | "analytics" | "admin"
|
||||||
|
// common pages
|
||||||
|
| "dashboard" | "myTasks" | "notifications"
|
||||||
|
// sales & crm & catalog
|
||||||
|
| "customers" | "contacts" | "leads" | "orders" | "quotes" | "subscriptions"
|
||||||
|
| "products" | "bundles" | "pricing" | "returns"
|
||||||
|
// services
|
||||||
|
| "serviceCatalog" | "servicePricing" | "serviceOrders" | "dispatchBoard" | "schedule"
|
||||||
|
| "resources" | "projects" | "timesheets" | "contracts" | "installedBase" | "warrantyClaims"
|
||||||
|
// procurement & warehouse
|
||||||
|
| "suppliers" | "purchaseOrders" | "grn" | "inventory" | "receiving" | "picking" | "packing" | "shipments"
|
||||||
|
// support, knowledge, ai
|
||||||
|
| "tickets" | "sla" | "csat" | "aiChat" | "knowledgeBase"
|
||||||
|
// marketing
|
||||||
|
| "campaigns" | "coupons" | "loyalty" | "news" | "pages"
|
||||||
|
// finance
|
||||||
|
| "invoices" | "payments" | "refunds" | "generalLedger" | "reconciliation"
|
||||||
|
// analytics
|
||||||
|
| "reports" | "dashboards"
|
||||||
|
// admin & system
|
||||||
|
| "users" | "rolesPermissions" | "tenants" | "settings"
|
||||||
|
| "flashExpress" | "tiktokShop" | "psp" | "webhooks" | "apiKeys"
|
||||||
|
| "compliance" | "documents" | "eSign" | "auditTrail" | "systemHealth" | "logs" | "developerSandbox"
|
||||||
|
// misc
|
||||||
|
| "kms" | "chat";
|
||||||
|
|
||||||
|
export type NavItem = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
icon?: IconKey; // ชื่อไอคอน
|
||||||
|
match?: string; // regex เป็น string เช่น "^/orders"
|
||||||
|
};
|
||||||
|
|
||||||
export type NavTreeItem = {
|
export type NavTreeItem = {
|
||||||
label: string;
|
label: string;
|
||||||
href?: string; // กลุ่มใหญ่ไม่ต้องมี href ก็ได้
|
href?: string;
|
||||||
children?: NavTreeItem[]; // เมนูย่อย
|
icon?: IconKey;
|
||||||
hidden?: boolean; // สำหรับซ่อนตามสิทธิ์/โมดูล
|
children?: NavTreeItem[];
|
||||||
|
hidden?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- Helpers (เดิม) ----------
|
/* ---------- Helpers ---------- */
|
||||||
const hasAnyRole = (roles: string[], list: Role[]) =>
|
const hasAnyRole = (roles: string[], list: Role[]) => list.some((r) => roles.includes(r));
|
||||||
list.some((r) => roles.includes(r));
|
|
||||||
|
|
||||||
const modEnabled = (mods: ModuleKey[], key: ModuleKey) => mods.includes(key);
|
const modEnabled = (mods: ModuleKey[], key: ModuleKey) => mods.includes(key);
|
||||||
|
|
||||||
// ให้ generic รับได้ทั้งที่มี/ไม่มี label เพื่อกัน error
|
|
||||||
type NavLike = { href?: string | null; label?: string | null };
|
type NavLike = { href?: string | null; label?: string | null };
|
||||||
|
|
||||||
const uniqByHref = <T extends NavLike>(items: T[]) => {
|
const uniqByHref = <T extends NavLike>(items: T[]) => {
|
||||||
const seen = new Set<string>();
|
const seen = new Set<string>();
|
||||||
return items.filter((i, idx) => {
|
return items.filter((i, idx) => {
|
||||||
const key =
|
const key = (i.href ?? undefined) || (i.label ? `__group__${i.label}` : `__idx__${idx}`);
|
||||||
(i.href ?? undefined) ||
|
|
||||||
(i.label ? `__group__${i.label}` : `__idx__${idx}`);
|
|
||||||
if (seen.has(key)) return false;
|
if (seen.has(key)) return false;
|
||||||
seen.add(key);
|
seen.add(key);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------- Flat nav ----------
|
/* ---------- Flat nav (Topbar/Mobile) ---------- */
|
||||||
/**
|
|
||||||
* สร้างเมนูแบบแบน (NavItem[]) สำหรับ topbar/mobile drawer
|
|
||||||
* - โหมดปกติ: เช็ค roles + modules
|
|
||||||
* - โหมดโชว์ทั้งหมด: opts.showAll = true (ไม่เช็คอะไรเลย)
|
|
||||||
*/
|
|
||||||
export function buildNavForRoles(
|
export function buildNavForRoles(
|
||||||
roles: string[],
|
roles: string[],
|
||||||
modules: ModuleKey[] = [],
|
modules: ModuleKey[] = [],
|
||||||
opts: { showAll?: boolean } = {}
|
opts: { showAll?: boolean } = {}
|
||||||
): NavItem[] {
|
): NavItem[] {
|
||||||
const showAll = !!opts.showAll;
|
const showAll = !!opts.showAll;
|
||||||
|
|
||||||
// bypass เมื่อ showAll = true
|
|
||||||
const has = (list: Role[]) => (showAll ? true : hasAnyRole(roles, list));
|
const has = (list: Role[]) => (showAll ? true : hasAnyRole(roles, list));
|
||||||
const mod = (key: ModuleKey) => (showAll ? true : modEnabled(modules, key));
|
const mod = (key: ModuleKey) => (showAll ? true : modEnabled(modules, key));
|
||||||
|
|
||||||
const base: NavItem[] = [{ label: "Dashboard", href: "/dashboard" }];
|
const base: NavItem[] = [
|
||||||
|
{ label: "Dashboard", href: "/dashboard", icon: "dashboard", match: "^/dashboard" },
|
||||||
|
];
|
||||||
|
|
||||||
// Sales quick links
|
|
||||||
if (mod("OMS") && has(["sales", "ops", "admin", "owner"])) {
|
if (mod("OMS") && has(["sales", "ops", "admin", "owner"])) {
|
||||||
base.push(
|
base.push(
|
||||||
{ label: "Orders", href: "/orders" },
|
{ label: "Orders", href: "/orders", icon: "orders", match: "^/orders" },
|
||||||
{ label: "Products", href: "/products" }
|
{ label: "Products", href: "/products", icon: "products", match: "^/products" },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (mod("CRM") && has(["sales", "support", "marketing", "admin", "owner"])) {
|
if (mod("CRM") && has(["sales", "support", "marketing", "admin", "owner"])) {
|
||||||
base.push({ label: "Customers", href: "/crm/customers" });
|
base.push({ label: "Customers", href: "/crm/customers", icon: "customers", match: "^/crm" });
|
||||||
}
|
}
|
||||||
|
if ((mod("ServiceOrders") || mod("Appointments") || mod("ServiceCatalog")) && has(["ops", "support", "admin", "owner"])) {
|
||||||
// Services quick links (ใหม่)
|
base.push({ label: "Services", href: "/services", icon: "services", match: "^/services" });
|
||||||
if (
|
|
||||||
(mod("ServiceOrders") || mod("Appointments") || mod("ServiceCatalog")) &&
|
|
||||||
has(["ops", "support", "admin", "owner"])
|
|
||||||
) {
|
|
||||||
base.push({ label: "Services", href: "/services" });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ops/Warehouse
|
|
||||||
if (mod("WMS") && has(["warehouse", "ops", "admin", "owner"])) {
|
if (mod("WMS") && has(["warehouse", "ops", "admin", "owner"])) {
|
||||||
base.push({ label: "Inventory", href: "/inventory" });
|
base.push({ label: "Inventory", href: "/inventory", icon: "inventory", match: "^/inventory" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Marketing
|
|
||||||
if (mod("Loyalty") && has(["marketing", "ops", "admin", "owner"])) {
|
if (mod("Loyalty") && has(["marketing", "ops", "admin", "owner"])) {
|
||||||
base.push({ label: "Loyalty", href: "/loyalty" });
|
base.push({ label: "Loyalty", href: "/loyalty", icon: "loyalty", match: "^/loyalty" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Support
|
|
||||||
if (has(["support", "admin", "owner"])) {
|
if (has(["support", "admin", "owner"])) {
|
||||||
base.push({ label: "Tickets", href: "/support/tickets" });
|
base.push({ label: "Tickets", href: "/support/tickets", icon: "tickets", match: "^/support" });
|
||||||
}
|
}
|
||||||
|
if (mod("KMS")) base.push({ label: "KMS", href: "/kms", icon: "kms", match: "^/kms" });
|
||||||
|
if (mod("AIChat")) base.push({ label: "Chat", href: "/chat", icon: "chat", match: "^/chat" });
|
||||||
|
|
||||||
// Knowledge/Chat
|
|
||||||
if (mod("KMS")) base.push({ label: "KMS", href: "/kms" });
|
|
||||||
if (mod("AIChat")) base.push({ label: "Chat", href: "/chat" });
|
|
||||||
|
|
||||||
// Finance
|
|
||||||
if ((mod("Billing") || mod("Accounting")) && has(["finance", "admin", "owner"])) {
|
if ((mod("Billing") || mod("Accounting")) && has(["finance", "admin", "owner"])) {
|
||||||
base.push({ label: "Finance", href: "/finance" });
|
base.push({ label: "Finance", href: "/finance", icon: "finance", match: "^/(finance|accounting)" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Admin
|
|
||||||
if (has(["admin", "owner"])) {
|
if (has(["admin", "owner"])) {
|
||||||
base.push(
|
base.push(
|
||||||
{ label: "Users", href: "/settings/users" },
|
{ label: "Users", href: "/settings/users", icon: "users", match: "^/settings/users" },
|
||||||
{ label: "Settings", href: "/settings" }
|
{ label: "Settings", href: "/settings", icon: "settings", match: "^/settings(?!/users)" },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return uniqByHref(base);
|
return uniqByHref(base);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Sidebar (grouped tree) ----------
|
/* ---------- Sidebar tree (Grouped) ---------- */
|
||||||
/**
|
|
||||||
* Top level (≤ 9 หมวด):
|
|
||||||
* 1) Home
|
|
||||||
* 2) Sales — รวม CRM + Catalog + Orders + Returns
|
|
||||||
* 3) Services — Service Catalog + Work Orders + Schedule/Dispatch + Projects + Timesheets + Contracts + Assets + Warranty
|
|
||||||
* 4) Procurement
|
|
||||||
* 5) Warehouse
|
|
||||||
* 6) Support — Tickets/SLA/CSAT + (shortcut) AI Chat/KMS
|
|
||||||
* 7) Marketing — Campaigns/Coupons + Loyalty + CMS
|
|
||||||
* 8) Finance — Billing + Accounting
|
|
||||||
* 9) Analytics — Reports + Dashboards
|
|
||||||
* + Admin & Settings (Integrations, System, Audit, Compliance, Documents/ESign)
|
|
||||||
*/
|
|
||||||
export function buildSidebarTreeForRoles(
|
export function buildSidebarTreeForRoles(
|
||||||
roles: string[],
|
roles: string[],
|
||||||
modules: ModuleKey[] = [],
|
modules: ModuleKey[] = [],
|
||||||
@@ -174,251 +140,186 @@ export function buildSidebarTreeForRoles(
|
|||||||
// 1) Home
|
// 1) Home
|
||||||
{
|
{
|
||||||
label: "Home",
|
label: "Home",
|
||||||
|
icon: "home",
|
||||||
children: uniqByHref<NavTreeItem>([
|
children: uniqByHref<NavTreeItem>([
|
||||||
{ label: "Dashboard", href: "/dashboard" },
|
{ label: "Dashboard", href: "/dashboard", icon: "dashboard" },
|
||||||
{ label: "My Tasks", href: "/tasks" },
|
{ label: "My Tasks", href: "/tasks", icon: "myTasks" },
|
||||||
{ label: "Notifications", href: "/notifications" },
|
{ label: "Notifications", href: "/notifications", icon: "notifications" },
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|
||||||
// 2) Sales — รวม CRM + Catalog + Orders + Returns
|
// 2) Sales
|
||||||
{
|
{
|
||||||
label: "Sales",
|
label: "Sales",
|
||||||
hidden: hide(
|
icon: "sales",
|
||||||
!(
|
hidden: hide(!((mod("OMS") || mod("CRM") || mod("Returns")) && has(["sales","ops","admin","owner","marketing","support"]))),
|
||||||
(mod("OMS") || mod("CRM") || mod("Returns")) &&
|
|
||||||
has(["sales", "ops", "admin", "owner", "marketing", "support"])
|
|
||||||
)
|
|
||||||
),
|
|
||||||
children: uniqByHref<NavTreeItem>([
|
children: uniqByHref<NavTreeItem>([
|
||||||
// CRM
|
{ label: "Customers", href: "/crm/customers", hidden: hide(!mod("CRM")), icon: "customers" },
|
||||||
{ label: "Customers", href: "/crm/customers", hidden: hide(!mod("CRM")) },
|
{ label: "Contacts", href: "/crm/contacts", hidden: hide(!mod("CRM")), icon: "contacts" },
|
||||||
{ label: "Contacts", href: "/crm/contacts", hidden: hide(!mod("CRM")) },
|
{ label: "Leads", href: "/crm/leads", hidden: hide(!mod("CRM")), icon: "leads" },
|
||||||
{ label: "Leads", href: "/crm/leads", hidden: hide(!mod("CRM")) },
|
|
||||||
// Sales core
|
{ label: "Orders", href: "/orders", hidden: hide(!mod("OMS")), icon: "orders" },
|
||||||
{ label: "Orders", href: "/orders", hidden: hide(!mod("OMS")) },
|
{ label: "Quotes", href: "/sales/quotes", hidden: hide(!mod("OMS")), icon: "quotes" },
|
||||||
{ label: "Quotes", href: "/sales/quotes", hidden: hide(!mod("OMS")) },
|
{ label: "Subscriptions", href: "/sales/subscriptions", hidden: hide(!mod("OMS")), icon: "subscriptions" },
|
||||||
{ label: "Subscriptions", href: "/sales/subscriptions", hidden: hide(!mod("OMS")) },
|
|
||||||
// Catalog
|
{ label: "Products", href: "/products", hidden: hide(!mod("OMS")), icon: "products" },
|
||||||
{ label: "Products", href: "/products", hidden: hide(!mod("OMS")) },
|
{ label: "Bundles", href: "/products/bundles", hidden: hide(!mod("OMS")), icon: "bundles" },
|
||||||
{ label: "Bundles", href: "/products/bundles", hidden: hide(!mod("OMS")) },
|
{ label: "Pricing", href: "/products/pricing", hidden: hide(!mod("OMS")), icon: "pricing" },
|
||||||
{ label: "Pricing", href: "/products/pricing", hidden: hide(!mod("OMS")) },
|
|
||||||
// Returns
|
{ label: "Returns", href: "/returns", hidden: hide(!mod("Returns")), icon: "returns" },
|
||||||
{ label: "Returns", href: "/returns", hidden: hide(!mod("Returns")) },
|
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|
||||||
// 3) Services — บริการปลายทางถึงปลายทาง
|
// 3) Services
|
||||||
{
|
{
|
||||||
label: "Services",
|
label: "Services",
|
||||||
hidden: hide(
|
icon: "services",
|
||||||
!(
|
hidden: hide(!((mod("ServiceCatalog") || mod("ServiceOrders") || mod("ServiceProjects") || mod("Appointments") || mod("Contracts") || mod("Timesheets") || mod("InstalledBase") || mod("Warranty")) && has(["ops","support","admin","owner","hr","finance"]))),
|
||||||
(mod("ServiceCatalog") ||
|
|
||||||
mod("ServiceOrders") ||
|
|
||||||
mod("ServiceProjects") ||
|
|
||||||
mod("Appointments") ||
|
|
||||||
mod("Contracts") ||
|
|
||||||
mod("Timesheets") ||
|
|
||||||
mod("InstalledBase") ||
|
|
||||||
mod("Warranty")) &&
|
|
||||||
has(["ops", "support", "admin", "owner", "hr", "finance"])
|
|
||||||
)
|
|
||||||
),
|
|
||||||
children: uniqByHref<NavTreeItem>([
|
children: uniqByHref<NavTreeItem>([
|
||||||
// ขายบริการ/แพ็กเกจ
|
{ label: "Service Catalog", href: "/services/catalog", hidden: hide(!mod("ServiceCatalog")), icon: "serviceCatalog" },
|
||||||
{ label: "Service Catalog", href: "/services/catalog", hidden: hide(!mod("ServiceCatalog")) },
|
{ label: "Service Pricing", href: "/services/pricing", hidden: hide(!mod("ServiceCatalog")), icon: "servicePricing" },
|
||||||
{ label: "Service Pricing", href: "/services/pricing", hidden: hide(!mod("ServiceCatalog")) },
|
|
||||||
// ดำเนินงานบริการ
|
{ label: "Service Orders", href: "/services/orders", hidden: hide(!mod("ServiceOrders")), icon: "serviceOrders" },
|
||||||
{ label: "Service Orders", href: "/services/orders", hidden: hide(!mod("ServiceOrders")) },
|
{ label: "Dispatch Board", href: "/services/dispatch", hidden: hide(!mod("ServiceOrders") && !mod("Appointments")), icon: "dispatchBoard" },
|
||||||
{ label: "Dispatch Board", href: "/services/dispatch", hidden: hide(!mod("ServiceOrders") && !mod("Appointments")) },
|
{ label: "Schedule", href: "/appointments/schedule", hidden: hide(!mod("Appointments")), icon: "schedule" },
|
||||||
{ label: "Schedule", href: "/appointments/schedule", hidden: hide(!mod("Appointments")) },
|
{ label: "Resources", href: "/appointments/resources", hidden: hide(!mod("Appointments")), icon: "resources" },
|
||||||
{ label: "Resources", href: "/appointments/resources", hidden: hide(!mod("Appointments")) },
|
|
||||||
// โครงการบริการ/บุคลากร
|
{ label: "Projects", href: "/services/projects", hidden: hide(!mod("ServiceProjects")), icon: "projects" },
|
||||||
{ label: "Projects", href: "/services/projects", hidden: hide(!mod("ServiceProjects")) },
|
{ label: "Timesheets", href: "/services/timesheets", hidden: hide(!mod("Timesheets")), icon: "timesheets" },
|
||||||
{ label: "Timesheets", href: "/services/timesheets", hidden: hide(!mod("Timesheets")) },
|
|
||||||
// สัญญา/สิทธิ์/บริการตาม SLA
|
{ label: "Contracts & SLAs", href: "/services/contracts", hidden: hide(!mod("Contracts")), icon: "contracts" },
|
||||||
{ label: "Contracts & SLAs", href: "/services/contracts", hidden: hide(!mod("Contracts")) },
|
{ label: "Installed Base", href: "/services/installed-base", hidden: hide(!mod("InstalledBase")), icon: "installedBase" },
|
||||||
// สินทรัพย์ลูกค้า/ประวัติซ่อม
|
{ label: "Warranty Claims", href: "/services/warranty", hidden: hide(!mod("Warranty")), icon: "warrantyClaims" },
|
||||||
{ label: "Installed Base", href: "/services/installed-base", hidden: hide(!mod("InstalledBase")) },
|
|
||||||
{ label: "Warranty Claims", href: "/services/warranty", hidden: hide(!mod("Warranty")) },
|
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|
||||||
// 4) Procurement
|
// 4) Procurement
|
||||||
{
|
{
|
||||||
label: "Procurement",
|
label: "Procurement",
|
||||||
hidden: hide(!has(["procurement", "ops", "admin", "owner"])),
|
icon: "procurement",
|
||||||
|
hidden: hide(!has(["procurement","ops","admin","owner"])),
|
||||||
children: uniqByHref<NavTreeItem>([
|
children: uniqByHref<NavTreeItem>([
|
||||||
{ label: "Suppliers", href: "/procurement/suppliers" },
|
{ label: "Suppliers", href: "/procurement/suppliers", icon: "suppliers" },
|
||||||
{ label: "Purchase Orders", href: "/procurement/purchase-orders" },
|
{ label: "Purchase Orders", href: "/procurement/purchase-orders", icon: "purchaseOrders" },
|
||||||
{ label: "GRN (Receiving)", href: "/procurement/grn" },
|
{ label: "GRN (Receiving)", href: "/procurement/grn", icon: "grn" },
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|
||||||
// 5) Warehouse
|
// 5) Warehouse
|
||||||
{
|
{
|
||||||
label: "Warehouse",
|
label: "Warehouse",
|
||||||
hidden: hide(!mod("WMS") || !has(["warehouse", "ops", "admin", "owner"])),
|
icon: "warehouse",
|
||||||
|
hidden: hide(!mod("WMS") || !has(["warehouse","ops","admin","owner"])),
|
||||||
children: uniqByHref<NavTreeItem>([
|
children: uniqByHref<NavTreeItem>([
|
||||||
{ label: "Inventory", href: "/inventory" },
|
{ label: "Inventory", href: "/inventory", icon: "inventory" },
|
||||||
{ label: "Receiving", href: "/warehouse/receiving" },
|
{ label: "Receiving", href: "/warehouse/receiving", icon: "receiving" },
|
||||||
{ label: "Picking", href: "/warehouse/picking" },
|
{ label: "Picking", href: "/warehouse/picking", icon: "picking" },
|
||||||
{ label: "Packing", href: "/warehouse/packing" },
|
{ label: "Packing", href: "/warehouse/packing", icon: "packing" },
|
||||||
{ label: "Shipments", href: "/warehouse/shipments" },
|
{ label: "Shipments", href: "/warehouse/shipments", icon: "shipments" },
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|
||||||
// 6) Support — เพิ่ม shortcut ไป Knowledge/AI Chat (ถ้ามี)
|
// 6) Support
|
||||||
{
|
{
|
||||||
label: "Support",
|
label: "Support",
|
||||||
hidden: hide(!has(["support", "admin", "owner", "ops"])),
|
icon: "support",
|
||||||
|
hidden: hide(!has(["support","admin","owner","ops"])),
|
||||||
children: uniqByHref<NavTreeItem>([
|
children: uniqByHref<NavTreeItem>([
|
||||||
{ label: "Tickets", href: "/support/tickets" },
|
{ label: "Tickets", href: "/support/tickets", icon: "tickets" },
|
||||||
{ label: "SLA", href: "/support/sla" },
|
{ label: "SLA", href: "/support/sla", icon: "sla" },
|
||||||
{ label: "CSAT", href: "/support/csat" },
|
{ label: "CSAT", href: "/support/csat", icon: "csat" },
|
||||||
{ label: "AI Chat", href: "/chat", hidden: hide(!mod("AIChat")) },
|
{ label: "AI Chat", href: "/chat", hidden: hide(!mod("AIChat")), icon: "aiChat" },
|
||||||
{ label: "Knowledge Base", href: "/kms", hidden: hide(!mod("KMS")) },
|
{ label: "Knowledge Base", href: "/kms", hidden: hide(!mod("KMS")), icon: "knowledgeBase" },
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|
||||||
// 7) Marketing — รวม Loyalty + CMS/Content
|
// 7) Marketing
|
||||||
{
|
{
|
||||||
label: "Marketing",
|
label: "Marketing",
|
||||||
hidden: hide(!has(["marketing", "admin", "owner"])),
|
icon: "marketing",
|
||||||
|
hidden: hide(!has(["marketing","admin","owner"])),
|
||||||
children: uniqByHref<NavTreeItem>([
|
children: uniqByHref<NavTreeItem>([
|
||||||
{ label: "Campaigns", href: "/marketing/campaigns", hidden: hide(!mod("Marketing")) },
|
{ label: "Campaigns", href: "/marketing/campaigns", hidden: hide(!mod("Marketing")), icon: "campaigns" },
|
||||||
{ label: "Coupons", href: "/marketing/coupons", hidden: hide(!mod("Marketing")) },
|
{ label: "Coupons", href: "/marketing/coupons", hidden: hide(!mod("Marketing")), icon: "coupons" },
|
||||||
{ label: "Loyalty", href: "/loyalty", hidden: hide(!mod("Loyalty")) },
|
{ label: "Loyalty", href: "/loyalty", hidden: hide(!mod("Loyalty")), icon: "loyalty" },
|
||||||
{ label: "News", href: "/cms/news", hidden: hide(!mod("CMS")) },
|
{ label: "News", href: "/cms/news", hidden: hide(!mod("CMS")), icon: "news" },
|
||||||
{ label: "Pages", href: "/cms/pages", hidden: hide(!mod("CMS")) },
|
{ label: "Pages", href: "/cms/pages", hidden: hide(!mod("CMS")), icon: "pages" },
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|
||||||
// 8) Finance
|
// 8) Finance
|
||||||
{
|
{
|
||||||
label: "Finance",
|
label: "Finance",
|
||||||
hidden: hide(
|
icon: "finance",
|
||||||
!((mod("Billing") || mod("Accounting")) && has(["finance", "admin", "owner"]))
|
hidden: hide(!((mod("Billing") || mod("Accounting")) && has(["finance","admin","owner"]))),
|
||||||
),
|
|
||||||
children: uniqByHref<NavTreeItem>([
|
children: uniqByHref<NavTreeItem>([
|
||||||
{ label: "Invoices", href: "/finance/invoices", hidden: hide(!mod("Billing")) },
|
{ label: "Invoices", href: "/finance/invoices", hidden: hide(!mod("Billing")), icon: "invoices" },
|
||||||
{ label: "Payments", href: "/finance/payments", hidden: hide(!mod("Billing")) },
|
{ label: "Payments", href: "/finance/payments", hidden: hide(!mod("Billing")), icon: "payments" },
|
||||||
{ label: "Refunds", href: "/finance/refunds", hidden: hide(!mod("Billing")) },
|
{ label: "Refunds", href: "/finance/refunds", hidden: hide(!mod("Billing")), icon: "refunds" },
|
||||||
{ label: "General Ledger", href: "/accounting/gl", hidden: hide(!mod("Accounting")) },
|
{ label: "General Ledger", href: "/accounting/gl", hidden: hide(!mod("Accounting")), icon: "generalLedger" },
|
||||||
{ label: "Reconciliation", href: "/accounting/recon", hidden: hide(!mod("Accounting")) },
|
{ label: "Reconciliation", href: "/accounting/recon", hidden: hide(!mod("Accounting")), icon: "reconciliation" },
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|
||||||
// 9) Analytics
|
// 9) Analytics
|
||||||
{
|
{
|
||||||
label: "Analytics",
|
label: "Analytics",
|
||||||
hidden: hide(!mod("Reports") || !has(["admin", "owner", "ops", "sales", "finance"])),
|
icon: "analytics",
|
||||||
|
hidden: hide(!mod("Reports") || !has(["admin","owner","ops","sales","finance"])),
|
||||||
children: uniqByHref<NavTreeItem>([
|
children: uniqByHref<NavTreeItem>([
|
||||||
{ label: "Reports", href: "/reports" },
|
{ label: "Reports", href: "/reports", icon: "reports" },
|
||||||
{ label: "Dashboards", href: "/reports/dashboards" },
|
{ label: "Dashboards", href: "/reports/dashboards", icon: "dashboards" },
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Admin & Settings (รวม Integrations/Compliance/Documents/Audit/System)
|
// Admin & Settings
|
||||||
{
|
{
|
||||||
label: "Admin",
|
label: "Admin",
|
||||||
hidden: hide(!has(["admin", "owner"])),
|
icon: "admin",
|
||||||
|
hidden: hide(!has(["admin","owner"])),
|
||||||
children: uniqByHref<NavTreeItem>([
|
children: uniqByHref<NavTreeItem>([
|
||||||
// Identity & Tenant
|
{ label: "Users", href: "/settings/users", icon: "users" },
|
||||||
{ label: "Users", href: "/settings/users" },
|
{ label: "Roles & Permissions", href: "/settings/roles", icon: "rolesPermissions" },
|
||||||
{ label: "Roles & Permissions", href: "/settings/roles" },
|
{ label: "Tenants", href: "/settings/tenants", icon: "tenants" },
|
||||||
{ label: "Tenants", href: "/settings/tenants" },
|
{ label: "Settings", href: "/settings", icon: "settings" },
|
||||||
{ label: "Settings", href: "/settings" },
|
|
||||||
|
|
||||||
// Integrations
|
{ label: "Flash Express", href: "/integrations/flash-express", hidden: hide(!mod("Integrations")), icon: "flashExpress" },
|
||||||
{ label: "Flash Express", href: "/integrations/flash-express", hidden: hide(!mod("Integrations")) },
|
{ label: "TikTok Shop", href: "/integrations/tiktok-shop", hidden: hide(!mod("Integrations")), icon: "tiktokShop" },
|
||||||
{ label: "TikTok Shop", href: "/integrations/tiktok-shop", hidden: hide(!mod("Integrations")) },
|
{ label: "PSP / QR", href: "/integrations/payments", hidden: hide(!mod("Integrations")), icon: "psp" },
|
||||||
{ label: "PSP / QR", href: "/integrations/payments", hidden: hide(!mod("Integrations")) },
|
{ label: "Webhooks", href: "/integrations/webhooks", hidden: hide(!mod("Integrations")), icon: "webhooks" },
|
||||||
{ label: "Webhooks", href: "/integrations/webhooks", hidden: hide(!mod("Integrations")) },
|
{ label: "API Keys", href: "/integrations/api-keys", hidden: hide(!mod("Integrations")), icon: "apiKeys" },
|
||||||
{ label: "API Keys", href: "/integrations/api-keys", hidden: hide(!mod("Integrations")) },
|
|
||||||
|
|
||||||
// Compliance & Docs
|
{ label: "Compliance", href: "/compliance", hidden: hide(!mod("Compliance")), icon: "compliance" },
|
||||||
{ label: "Compliance", href: "/compliance", hidden: hide(!mod("Compliance")) },
|
{ label: "Documents", href: "/documents", hidden: hide(!mod("Documents")), icon: "documents" },
|
||||||
{ label: "Documents", href: "/documents", hidden: hide(!mod("Documents")) },
|
{ label: "eSign", href: "/documents/esign", hidden: hide(!mod("ESign")), icon: "eSign" },
|
||||||
{ label: "eSign", href: "/documents/esign", hidden: hide(!mod("ESign")) },
|
|
||||||
|
|
||||||
// System
|
{ label: "Audit Trail", href: "/system/audit", hidden: hide(!mod("Audit")), icon: "auditTrail" },
|
||||||
{ label: "Audit Trail", href: "/system/audit", hidden: hide(!mod("Audit")) },
|
{ label: "System Health", href: "/system/health", icon: "systemHealth" },
|
||||||
{ label: "System Health", href: "/system/health" },
|
{ label: "Logs", href: "/system/logs", icon: "logs" },
|
||||||
{ label: "Logs", href: "/system/logs" },
|
{ label: "Developer Sandbox", href: "/developer/sandbox", icon: "developerSandbox" },
|
||||||
{ label: "Developer Sandbox", href: "/developer/sandbox" },
|
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// กรองกลุ่มว่าง + child ที่ hidden
|
|
||||||
const cleaned = tree
|
const cleaned = tree
|
||||||
.map((g) =>
|
.map((g) => (!g.children?.length ? g : { ...g, children: g.children.filter((c) => !c.hidden) }))
|
||||||
!g.children?.length ? g : { ...g, children: g.children.filter((c) => !c.hidden) }
|
|
||||||
)
|
|
||||||
.filter((g) => !g.hidden && (g.children?.length ?? 0) > 0);
|
.filter((g) => !g.hidden && (g.children?.length ?? 0) > 0);
|
||||||
|
|
||||||
return cleaned;
|
return cleaned;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- Presets & shortcuts ----------
|
/* ---------- Presets ---------- */
|
||||||
export const DEFAULT_MODULES: ModuleKey[] = [
|
export const DEFAULT_MODULES: ModuleKey[] = [
|
||||||
// Core
|
// Core
|
||||||
"CRM",
|
"CRM","OMS","WMS","Returns","Billing","Accounting","Loyalty","Marketing",
|
||||||
"OMS",
|
"KMS","AIChat","Appointments","CMS","Integrations","Reports","Audit",
|
||||||
"WMS",
|
// Service
|
||||||
"Returns",
|
"ServiceCatalog","ServiceOrders","ServiceProjects","Timesheets","Contracts","InstalledBase","Warranty",
|
||||||
"Billing",
|
|
||||||
"Accounting",
|
|
||||||
"Loyalty",
|
|
||||||
"Marketing",
|
|
||||||
"KMS",
|
|
||||||
"AIChat",
|
|
||||||
"Appointments",
|
|
||||||
"CMS",
|
|
||||||
"Integrations",
|
|
||||||
"Reports",
|
|
||||||
"Audit",
|
|
||||||
// Service business (ใหม่)
|
|
||||||
"ServiceCatalog",
|
|
||||||
"ServiceOrders",
|
|
||||||
"ServiceProjects",
|
|
||||||
"Timesheets",
|
|
||||||
"Contracts",
|
|
||||||
"InstalledBase",
|
|
||||||
"Warranty",
|
|
||||||
// Enablement & Compliance
|
// Enablement & Compliance
|
||||||
"Documents",
|
"Documents","ESign","Compliance",
|
||||||
"ESign",
|
|
||||||
"Compliance",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export const ALL_ROLES: Role[] = [
|
export const ALL_ROLES: Role[] = ["owner","admin","ops","sales","finance","warehouse","procurement","support","marketing","hr"];
|
||||||
"owner",
|
|
||||||
"admin",
|
|
||||||
"ops",
|
|
||||||
"sales",
|
|
||||||
"finance",
|
|
||||||
"warehouse",
|
|
||||||
"procurement",
|
|
||||||
"support",
|
|
||||||
"marketing",
|
|
||||||
"hr",
|
|
||||||
];
|
|
||||||
|
|
||||||
// โหมดโชว์ครบ แบบไม่ต้องส่ง params
|
export const buildNavShowAll = () => buildNavForRoles(ALL_ROLES, DEFAULT_MODULES, { showAll: true });
|
||||||
export const buildNavShowAll = () =>
|
export const buildSidebarShowAll = () => buildSidebarTreeForRoles(ALL_ROLES, DEFAULT_MODULES, { showAll: true });
|
||||||
buildNavForRoles(ALL_ROLES, DEFAULT_MODULES, { showAll: true });
|
|
||||||
|
|
||||||
export const buildSidebarShowAll = () =>
|
|
||||||
buildSidebarTreeForRoles(ALL_ROLES, DEFAULT_MODULES, { showAll: true });
|
|
||||||
|
|
||||||
/*
|
|
||||||
Usage:
|
|
||||||
const topNav = buildNavForRoles(user.roles, enabledModules);
|
|
||||||
const sideTree = buildSidebarTreeForRoles(user.roles, enabledModules);
|
|
||||||
|
|
||||||
// โหมดโชว์ครบ (debug/demo):
|
|
||||||
const topAll = buildNavShowAll();
|
|
||||||
const sideAll = buildSidebarShowAll();
|
|
||||||
*/
|
|
||||||
|
|||||||
Reference in New Issue
Block a user