Reorder Orders Table

This commit is contained in:
Thanakarn Klangkasame
2025-10-09 13:30:38 +07:00
parent 515eec2393
commit 784ea62d51
2 changed files with 105 additions and 8 deletions

View File

@@ -431,8 +431,8 @@ function defaultCell(v: unknown): ReactNode {
const ALL_COLS: Column[] = [
{ key: "id", labelTH: "ไอดี", groupTH: "เอกลักษณ์ & ช่องทาง" },
{ key: "orderNo", labelTH: "เลขออเดอร์", groupTH: "เอกลักษณ์ & ช่องทาง" },
{ key: "orderRef", labelTH: "อ้างอิงภายใน", groupTH: "เอกลักษณ์ & ช่องทาง" },
{ key: "channel", labelTH: "ช่องทาง", groupTH: "เอกลักษณ์ & ช่องทาง" },
{ key: "orderRef", labelTH: "อ้างอิงภายใน", groupTH: "เอกลักษณ์ & ช่องทาง" },
{ key: "channelOrderId", labelTH: "เลขออเดอร์ช่องทาง", groupTH: "เอกลักษณ์ & ช่องทาง" },
{ key: "marketplaceShopId", labelTH: "รหัสร้าน", groupTH: "เอกลักษณ์ & ช่องทาง" },
{ key: "marketplaceShopName", labelTH: "ชื่อร้าน", groupTH: "เอกลักษณ์ & ช่องทาง" },
@@ -440,10 +440,10 @@ const ALL_COLS: Column[] = [
{ key: "createdByEmail", labelTH: "อีเมลผู้เปิด", groupTH: "ผู้เปิดออเดอร์" },
{ key: "createdById", labelTH: "รหัสผู้เปิด", groupTH: "ผู้เปิดออเดอร์" },
{ 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: "datePacked", labelTH: "แพ็คของ", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) },
{ key: "dateShipped", labelTH: "ส่งของ", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) },
{ key: "dateDelivered", labelTH: "ถึงปลายทาง", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) },
{ key: "dateCancelled", labelTH: "ยกเลิก", groupTH: "วัน–เวลา", format: (v) => fmtDate(v as string | null | undefined) },
{ key: "customerId", labelTH: "รหัสลูกค้า", groupTH: "ลูกค้า" },
{ key: "customerName", labelTH: "ชื่อลูกค้า", groupTH: "ลูกค้า" },
@@ -527,9 +527,9 @@ const DEFAULT_VISIBLE: 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: ["dateCreated", "datePaid", "datePacked", "dateShipped", "dateDelivered", "dateCancelled"] },
{ titleTH: "วัน–เวลา", keys: ["dateCreated", "dateDelivered", "datePaid", "datePacked", "dateShipped", "dateCancelled"] },
{ titleTH: "ลูกค้า", keys: ["customerId", "customerName", "customerEmail", "customerPhone"] },
{ titleTH: "ที่อยู่จัดส่ง", keys: ["shippingName", "shippingPhone", "shippingAddressLine1", "shippingAddressLine2", "shippingSubdistrict", "shippingDistrict", "shippingProvince", "shippingPostcode", "shippingCountry"] },
{ titleTH: "ออกใบเสร็จ/ภาษี", keys: ["billingName", "billingTaxId", "billingAddressLine1", "billingSubdistrict", "billingDistrict", "billingProvince", "billingPostcode", "billingCountry"] },

View File

@@ -1,6 +1,6 @@
"use client";
import { ReactNode, useMemo, useState } from "react";
import { ReactNode, useEffect, useMemo, useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
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 };
export default function AppShell({ user, nav, children }: Props) {
// เดสก์ท็อป: พับ/กางไซด์บาร์
const [sidebarOpen, setSidebarOpen] = useState(true);
// มือถือ: เปิด/ปิด drawer
const [mobileOpen, setMobileOpen] = useState(false);
const pathname = usePathname();
const initials = useMemo(() => {
@@ -22,6 +26,23 @@ export default function AppShell({ user, nav, children }: Props) {
const isActive = (item: NavItem): boolean =>
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 (
<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) */}
@@ -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" />}
</span>
{sidebarOpen && <span className="truncate">{item.label}</span>}
</Link>
);
})}
</nav>
{/* Subtle footer in sidebar */}
{/* Footer in sidebar */}
<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>
@@ -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">
<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">
{/* ปุ่มมือถือ: เปิด/ปิด drawer */}
<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"
aria-label="Toggle menu"
aria-expanded={mobileOpen}
aria-controls="mobile-drawer"
>
</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">
<span className="font-medium text-neutral-800">Dashboard</span>
<span></span>
@@ -154,6 +187,70 @@ export default function AppShell({ user, nav, children }: Props) {
</div>
</footer>
</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>
);
}