Reorder Orders Table
This commit is contained in:
@@ -431,8 +431,8 @@ function defaultCell(v: unknown): ReactNode {
|
|||||||
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: "orderRef", labelTH: "อ้างอิงภายใน", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
|
||||||
{ key: "channel", labelTH: "ช่องทาง", groupTH: "เอกลักษณ์ & ช่องทาง" },
|
{ key: "channel", 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: "เอกลักษณ์ & ช่องทาง" },
|
||||||
@@ -440,10 +440,10 @@ const ALL_COLS: Column[] = [
|
|||||||
{ 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: "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: "dateDelivered", 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: "ลูกค้า" },
|
||||||
@@ -527,9 +527,9 @@ const DEFAULT_VISIBLE: ColumnKey[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const GROUPS: { titleTH: string; keys: ColumnKey[] }[] = [
|
const GROUPS: { titleTH: string; keys: ColumnKey[] }[] = [
|
||||||
{ titleTH: "เอกลักษณ์ & ช่องทาง", keys: ["id", "orderNo", "orderRef", "channel", "channelOrderId", "marketplaceShopId", "marketplaceShopName"] },
|
{ titleTH: "เอกลักษณ์ & ช่องทาง", keys: ["id", "orderNo", "channel", "orderRef", "channelOrderId", "marketplaceShopId", "marketplaceShopName"] },
|
||||||
{ titleTH: "ผู้เปิดออเดอร์", keys: ["createdByName", "createdByEmail", "createdById"] },
|
{ titleTH: "ผู้เปิดออเดอร์", keys: ["createdByName", "createdByEmail", "createdById"] },
|
||||||
{ titleTH: "วัน–เวลา", keys: ["dateCreated", "datePaid", "datePacked", "dateShipped", "dateDelivered", "dateCancelled"] },
|
{ titleTH: "วัน–เวลา", keys: ["dateCreated", "dateDelivered", "datePaid", "datePacked", "dateShipped", "dateCancelled"] },
|
||||||
{ titleTH: "ลูกค้า", keys: ["customerId", "customerName", "customerEmail", "customerPhone"] },
|
{ titleTH: "ลูกค้า", keys: ["customerId", "customerName", "customerEmail", "customerPhone"] },
|
||||||
{ titleTH: "ที่อยู่จัดส่ง", keys: ["shippingName", "shippingPhone", "shippingAddressLine1", "shippingAddressLine2", "shippingSubdistrict", "shippingDistrict", "shippingProvince", "shippingPostcode", "shippingCountry"] },
|
{ titleTH: "ที่อยู่จัดส่ง", keys: ["shippingName", "shippingPhone", "shippingAddressLine1", "shippingAddressLine2", "shippingSubdistrict", "shippingDistrict", "shippingProvince", "shippingPostcode", "shippingCountry"] },
|
||||||
{ titleTH: "ออกใบเสร็จ/ภาษี", keys: ["billingName", "billingTaxId", "billingAddressLine1", "billingSubdistrict", "billingDistrict", "billingProvince", "billingPostcode", "billingCountry"] },
|
{ titleTH: "ออกใบเสร็จ/ภาษี", keys: ["billingName", "billingTaxId", "billingAddressLine1", "billingSubdistrict", "billingDistrict", "billingProvince", "billingPostcode", "billingCountry"] },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { ReactNode, 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";
|
||||||
@@ -9,7 +9,11 @@ type NavItem = { label: string; href: string; icon?: React.ReactNode; match?: Re
|
|||||||
type Props = { user: UserSession; nav: NavItem[]; children: ReactNode };
|
type Props = { user: UserSession; nav: NavItem[]; children: ReactNode };
|
||||||
|
|
||||||
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 pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
const initials = useMemo(() => {
|
const initials = useMemo(() => {
|
||||||
@@ -22,6 +26,23 @@ export default function AppShell({ user, nav, children }: Props) {
|
|||||||
const isActive = (item: NavItem): boolean =>
|
const isActive = (item: NavItem): boolean =>
|
||||||
Boolean((item.match && item.match.test(pathname)) || pathname === item.href);
|
Boolean((item.match && item.match.test(pathname)) || pathname === item.href);
|
||||||
|
|
||||||
|
// ปิด drawer อัตโนมัติเมื่อเปลี่ยนหน้า
|
||||||
|
useEffect(() => {
|
||||||
|
setMobileOpen(false);
|
||||||
|
}, [pathname]);
|
||||||
|
|
||||||
|
// ล็อกสกอลล์หน้าเมื่อ drawer เปิด (กันเลื่อนพื้นหลัง)
|
||||||
|
useEffect(() => {
|
||||||
|
const el = document.documentElement;
|
||||||
|
if (mobileOpen) {
|
||||||
|
const prev = el.style.overflow;
|
||||||
|
el.style.overflow = "hidden";
|
||||||
|
return () => {
|
||||||
|
el.style.overflow = prev;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}, [mobileOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid min-h-dvh grid-cols-[auto_minmax(0,1fr)] bg-gradient-to-b from-neutral-50 to-white text-neutral-900">
|
<div className="grid min-h-dvh grid-cols-[auto_minmax(0,1fr)] bg-gradient-to-b from-neutral-50 to-white text-neutral-900">
|
||||||
{/* Sidebar (desktop) */}
|
{/* Sidebar (desktop) */}
|
||||||
@@ -91,14 +112,13 @@ export default function AppShell({ user, nav, children }: Props) {
|
|||||||
>
|
>
|
||||||
{item.icon ?? <span className="h-1.5 w-1.5 rounded-full bg-current" />}
|
{item.icon ?? <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>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Subtle footer in sidebar */}
|
{/* Footer in sidebar */}
|
||||||
<div className="px-4 pb-4 pt-2 text-[11px] text-neutral-400">
|
<div className="px-4 pb-4 pt-2 text-[11px] text-neutral-400">
|
||||||
<div className="rounded-xl bg-neutral-50 border border-neutral-200/70 px-3 py-2">v1.0 • {year}</div>
|
<div className="rounded-xl bg-neutral-50 border border-neutral-200/70 px-3 py-2">v1.0 • {year}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -110,14 +130,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 */}
|
||||||
<button
|
<button
|
||||||
onClick={() => setSidebarOpen((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 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-controls="mobile-drawer"
|
||||||
>
|
>
|
||||||
เมนู
|
เมนู
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* (ทางเลือก) ปุ่มเดสก์ท็อป: พับ/กาง sidebar */}
|
||||||
|
<button
|
||||||
|
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"
|
||||||
|
aria-label="Collapse sidebar"
|
||||||
|
title={sidebarOpen ? "Collapse sidebar" : "Expand sidebar"}
|
||||||
|
>
|
||||||
|
{sidebarOpen ? "◀︎" : "▶︎"}
|
||||||
|
</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">
|
||||||
<span className="font-medium text-neutral-800">Dashboard</span>
|
<span className="font-medium text-neutral-800">Dashboard</span>
|
||||||
<span>•</span>
|
<span>•</span>
|
||||||
@@ -154,6 +187,70 @@ export default function AppShell({ user, nav, children }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile drawer + backdrop */}
|
||||||
|
{mobileOpen && (
|
||||||
|
<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)} />
|
||||||
|
{/* 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">
|
||||||
|
{/* Brand */}
|
||||||
|
<div className="h-16 flex items-center px-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="h-9 w-9 rounded-2xl bg-neutral-900 text-white grid place-items-center font-semibold shadow-sm">
|
||||||
|
E
|
||||||
|
</div>
|
||||||
|
<div className="leading-tight">
|
||||||
|
<div className="font-semibold tracking-tight">EOP</div>
|
||||||
|
<div className="text-[11px] text-neutral-500 -mt-0.5">Operations</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mx-4 mb-2 h-px bg-gradient-to-r from-transparent via-neutral-200/70 to-transparent" />
|
||||||
|
|
||||||
|
{/* Tenant pill */}
|
||||||
|
<div className="px-4">
|
||||||
|
<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"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Nav (mobile) */}
|
||||||
|
<nav className="mt-4 space-y-1 px-2 pr-3 pb-6">
|
||||||
|
{nav.map((item) => {
|
||||||
|
const active = isActive(item);
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className={[
|
||||||
|
"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",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
"grid place-items-center rounded-lg",
|
||||||
|
active ? "bg-white/20 text-white" : "text-neutral-500 group-hover:text-neutral-700",
|
||||||
|
"h-8 w-8",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{item.icon ?? <span className="h-1.5 w-1.5 rounded-full bg-current" />}
|
||||||
|
</span>
|
||||||
|
<span className="truncate">{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user