Fix Layout

This commit is contained in:
Thanakarn Klangkasame
2025-10-08 15:49:12 +07:00
parent 1df1338cb8
commit f8b661b466
2 changed files with 276 additions and 259 deletions

View File

@@ -245,7 +245,7 @@ const MOCK_ORDERS: Order[] = [
dimensionsCmW: 12,
dimensionsCmH: 8,
fulfillStatus: "unfulfilled",
shipBy: "2025-10-09T18:00+07:00",
shipBy: "2025-10-09T18:00:00+07:00",
carrier: "Flash",
serviceLevel: "COD-Standard",
trackingNo: null,
@@ -409,9 +409,7 @@ function downloadCsv(filename: string, rows: Order[], cols: Column[]) {
const csv =
headers.join(",") +
"\n" +
rows
.map((r) => keys.map((k) => escapeCsv(getProp(r, k))).join(","))
.join("\n");
rows.map((r) => keys.map((k) => escapeCsv(getProp(r, k))).join(",")).join("\n");
const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
@@ -619,281 +617,294 @@ export default function OrdersPage() {
}, [visibleCols]);
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<h1 className="text-3xl font-extrabold tracking-tight"></h1>
<p className="mt-1 text-sm text-neutral-500">
{visibleCols.length} {rows.length}
</p>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setOpenCustomize(true)}
className="rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-1.5 text-sm shadow-sm hover:bg-neutral-100/80"
>
</button>
<button
onClick={() => downloadCsv("orders_visible.csv", rows, visibleCols)}
className="rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-1.5 text-sm shadow-sm hover:bg-neutral-100/80"
>
CSV ()
</button>
<button className="rounded-xl bg-neutral-900 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-neutral-800">
</button>
</div>
</div>
<>
{/* ทำให้กว้างหน้า "นิ่งเท่ากัน" เสมอ โดยจองที่สกรอลล์บาร์ */}
<style jsx global>{`
html { scrollbar-gutter: stable both-edges; }
@supports not (scrollbar-gutter: stable both-edges) {
html { overflow-y: scroll; }
}
`}</style>
{/* Filters — ใช้ p-5 ให้เท่ากันทุกการ์ด */}
<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="grid gap-3 sm:grid-cols-12">
<div className="sm:col-span-6">
<input
placeholder="ค้นหา: เลขออเดอร์ / อ้างอิง / ลูกค้า / เบอร์ / อีเมล / ผู้เปิด / เลขภาษี / Tracking…"
value={q}
onChange={(e) => setQ(e.target.value)}
className="w-full rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm outline-none placeholder:text-neutral-400 focus:border-neutral-300"
/>
{/* ผูกหน้าเข้ากับกรอบคงที่ */}
<div className="mx-auto w-full max-w-7xl px-6 space-y-6">
{/* Header */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<h1 className="text-3xl font-extrabold tracking-tight"></h1>
<p className="mt-1 text-sm text-neutral-500">
{visibleCols.length} {rows.length}
</p>
</div>
<div className="sm:col-span-2">
<select
value={channel}
onChange={(e) => setChannel(e.target.value as "all" | Channel)}
className="w-full rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm outline-none"
<div className="flex items-center gap-2">
<button
onClick={() => setOpenCustomize(true)}
className="rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-1.5 text-sm shadow-sm hover:bg-neutral-100/80"
>
<option value="all"></option>
<option value="shopee">Shopee</option>
<option value="lazada">Lazada</option>
<option value="tiktok">TikTok</option>
<option value="d2c"> (D2C)</option>
<option value="chat"></option>
</select>
</div>
<div className="sm:col-span-2">
<select
value={payment}
onChange={(e) => setPayment(e.target.value as "all" | PaymentStatus)}
className="w-full rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm outline-none"
</button>
<button
onClick={() => downloadCsv("orders_visible.csv", rows, visibleCols)}
className="rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-1.5 text-sm shadow-sm hover:bg-neutral-100/80"
>
<option value="all">การชำระเงิน: ทั้งหมด</option>
<option value="paid"> (paid)</option>
<option value="pending"> (pending)</option>
<option value="failed"> (failed)</option>
<option value="cod">COD</option>
<option value="refunded"> (refunded)</option>
</select>
</div>
<div className="sm:col-span-2">
<select
value={fulfillment}
onChange={(e) => setFulfillment(e.target.value as "all" | FulfillmentStatus)}
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="unfulfilled"> (unfulfilled)</option>
<option value="processing"> (processing)</option>
<option value="shipped"> (shipped)</option>
<option value="delivered"> (delivered)</option>
<option value="returned"> (returned)</option>
<option value="cancelled"> (cancelled)</option>
</select>
CSV ()
</button>
<button className="rounded-xl bg-neutral-900 px-3 py-1.5 text-sm font-semibold text-white shadow-sm hover:bg-neutral-800">
</button>
</div>
</div>
</section>
{/* Table — ดึงสกรอลบาร์ออกนอก padding ด้วย -mx-5 แล้วชดเชย px-5 */}
<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="px-5">
<table className="min-w-full table-auto text-sm">
<thead>
<tr className="text-neutral-600">
{topHeaderSegments.map((seg, idx) => (
<th
key={`${seg.groupTH}-${idx}`}
colSpan={seg.colSpan}
className="pb-1 pr-4 whitespace-nowrap text-center text-[12px] font-semibold"
>
{seg.groupTH}
</th>
))}
</tr>
<tr className="text-left text-neutral-500">
{visibleCols.map((c) => (
<th
key={String(c.key)}
className={`pb-2 pr-4 whitespace-nowrap ${c.align === "right" ? "text-right" : ""}`}
>
{c.labelTH}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-neutral-200/70">
{rows.map((o) => (
<tr key={o.id} className="align-middle">
{visibleCols.map((c) => {
const raw = getProp(o, c.key);
const content: ReactNode = c.format ? c.format(raw, o) : defaultCell(raw);
const align = c.align === "right" ? "text-right" : "";
return (
<td key={`${o.id}-${String(c.key)}`} className={`whitespace-nowrap py-2 pr-4 ${align}`}>
{c.key === "orderNo" ? (
<a
href={`/orders/${o.id}`}
className="text-neutral-900 underline decoration-neutral-200 underline-offset-4 hover:decoration-neutral-400"
>
{content}
</a>
) : c.key === "tags" && Array.isArray(o.tags) ? (
<div className="flex gap-1">
{o.tags!.length ? (
o.tags!.map((t) => (
<span
key={t}
className="rounded-full border border-neutral-200 bg-neutral-50 px-2 py-0.5 text-[11px] text-neutral-700"
>
{t}
</span>
))
) : (
<span>-</span>
)}
</div>
) : (
content
)}
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
</section>
{openCustomize && (
<div
className="fixed inset-0 z-40 flex items-end justify-center bg-black/40 p-3 sm:items-center"
onClick={() => setOpenCustomize(false)}
>
<div
className="flex h-[90vh] max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-3xl border border-neutral-200/70 bg-white shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="flex shrink-0 items-center justify-between border-b border-neutral-200/70 px-5 py-4">
<h3 className="text-lg font-semibold tracking-tight"></h3>
<button
onClick={() => setOpenCustomize(false)}
className="rounded-lg border border-neutral-200/70 bg-white/70 px-2.5 py-1.5 text-sm shadow-sm hover:bg-neutral-100/80"
{/* Filters */}
<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="grid gap-3 sm:grid-cols-12">
<div className="sm:col-span-6">
<input
placeholder="ค้นหา: เลขออเดอร์ / อ้างอิง / ลูกค้า / เบอร์ / อีเมล / ผู้เปิด / เลขภาษี / Tracking…"
value={q}
onChange={(e) => setQ(e.target.value)}
className="w-full rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm outline-none placeholder:text-neutral-400 focus:border-neutral-300"
/>
</div>
<div className="sm:col-span-2">
<select
value={channel}
onChange={(e) => setChannel(e.target.value as "all" | Channel)}
className="w-full rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-2 text-sm shadow-sm outline-none"
>
</button>
<option value="all"></option>
<option value="shopee">Shopee</option>
<option value="lazada">Lazada</option>
<option value="tiktok">TikTok</option>
<option value="d2c"> (D2C)</option>
<option value="chat"></option>
</select>
</div>
<div className="sm:col-span-2">
<select
value={payment}
onChange={(e) => setPayment(e.target.value as "all" | PaymentStatus)}
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="paid"> (paid)</option>
<option value="pending"> (pending)</option>
<option value="failed"> (failed)</option>
<option value="cod">COD</option>
<option value="refunded"> (refunded)</option>
</select>
</div>
<div className="sm:col-span-2">
<select
value={fulfillment}
onChange={(e) => setFulfillment(e.target.value as "all" | FulfillmentStatus)}
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="unfulfilled"> (unfulfilled)</option>
<option value="processing"> (processing)</option>
<option value="shipped"> (shipped)</option>
<option value="delivered"> (delivered)</option>
<option value="returned"> (returned)</option>
<option value="cancelled"> (cancelled)</option>
</select>
</div>
</div>
</section>
<div className="grow overflow-y-auto p-5">
<div className="mb-4 flex flex-wrap items-center gap-2">
<button
onClick={() => setTempKeys(ALL_COLS.map((c) => c.key))}
className="rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-1.5 text-xs shadow-sm hover:bg-neutral-100/80"
>
</button>
<button
onClick={() => setTempKeys([])}
className="rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-1.5 text-xs shadow-sm hover:bg-neutral-100/80"
>
</button>
<button
onClick={() => setTempKeys(DEFAULT_VISIBLE)}
className="rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-1.5 text-xs shadow-sm hover:bg-neutral-100/80"
>
</button>
<span className="text-xs text-neutral-500"> {tempKeys.length} </span>
</div>
{/* 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)]">
<div className="-mx-5 overflow-x-auto">
<div className="px-5">
<table className="min-w-full table-auto text-sm">
<thead>
<tr className="text-neutral-600">
{topHeaderSegments.map((seg, idx) => (
<th
key={`${seg.groupTH}-${idx}`}
colSpan={seg.colSpan}
className="whitespace-nowrap pb-1 pr-4 text-center text-[12px] font-semibold"
>
{seg.groupTH}
</th>
))}
</tr>
<tr className="text-left text-neutral-500">
{visibleCols.map((c) => (
<th
key={String(c.key)}
className={`whitespace-nowrap pb-2 pr-4 ${c.align === "right" ? "text-right" : ""}`}
>
{c.labelTH}
</th>
))}
</tr>
</thead>
<div className="grid gap-4 sm:grid-cols-2">
{GROUPS.map((g) => (
<fieldset key={g.titleTH} className="rounded-2xl border border-neutral-200/70 bg-white/60 p-4">
<div className="mb-2 flex items-center justify-between">
<legend className="text-sm font-semibold">{g.titleTH}</legend>
<div className="flex gap-1">
<button
className="rounded-lg border border-neutral-200/70 bg-white/70 px-2 py-1 text-[11px] shadow-sm hover:bg-neutral-100/80"
onClick={() => setTempKeys((prev) => Array.from(new Set([...prev, ...g.keys])))}
>
</button>
<button
className="rounded-lg border border-neutral-200/70 bg-white/70 px-2 py-1 text-[11px] shadow-sm hover:bg-neutral-100/80"
onClick={() => setTempKeys((prev) => prev.filter((k) => !g.keys.includes(k)))}
>
</button>
</div>
</div>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
{g.keys.map((k) => {
const col = ALL_COLS.find((c) => c.key === k)!;
const checked = tempKeys.includes(k);
return (
<label key={k} className="flex cursor-pointer items-center gap-2 text-sm">
<input
type="checkbox"
className="h-4 w-4 rounded border-neutral-300 text-neutral-900 focus:ring-neutral-900"
checked={checked}
onChange={(e) =>
setTempKeys((prev) =>
e.target.checked ? [...prev, k] : prev.filter((x) => x !== k),
)
}
/>
<span>{col.labelTH}</span>
</label>
);
})}
</div>
</fieldset>
<tbody className="divide-y divide-neutral-200/70">
{rows.map((o) => (
<tr key={o.id} className="align-middle">
{visibleCols.map((c) => {
const raw = getProp(o, c.key);
const content: ReactNode = c.format ? c.format(raw, o) : defaultCell(raw);
const align = c.align === "right" ? "text-right" : "";
return (
<td key={`${o.id}-${String(c.key)}`} className={`whitespace-nowrap py-2 pr-4 ${align}`}>
{c.key === "orderNo" ? (
<a
href={`/orders/${o.id}`}
className="text-neutral-900 underline decoration-neutral-200 underline-offset-4 hover:decoration-neutral-400"
>
{content}
</a>
) : c.key === "tags" && Array.isArray(o.tags) ? (
<div className="flex gap-1">
{o.tags!.length ? (
o.tags!.map((t) => (
<span
key={t}
className="rounded-full border border-neutral-200 bg-neutral-50 px-2 py-0.5 text-[11px] text-neutral-700"
>
{t}
</span>
))
) : (
<span>-</span>
)}
</div>
) : (
content
)}
</td>
);
})}
</tr>
))}
</div>
</tbody>
</table>
</div>
</div>
</section>
<div className="flex shrink-0 items-center justify-between border-t border-neutral-200/70 px-5 py-4">
<div className="text-xs text-neutral-500">
เคล็ดลับ: ระบบจะจำคอลัมน์ที่คุณเลือกไว้ในเบราว์เซอร์นี้
</div>
<div className="flex items-center gap-2">
{openCustomize && (
<div
className="fixed inset-0 z-40 flex items-end justify-center bg-black/40 p-3 sm:items-center"
onClick={() => setOpenCustomize(false)}
>
<div
className="flex h-[90vh] max-h-[90vh] w-full max-w-5xl flex-col overflow-hidden rounded-3xl border border-neutral-200/70 bg-white shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="flex shrink-0 items-center justify-between border-b border-neutral-200/70 px-5 py-4">
<h3 className="text-lg font-semibold tracking-tight"></h3>
<button
onClick={() => setOpenCustomize(false)}
className="rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-1.5 text-sm shadow-sm hover:bg-neutral-100/80"
className="rounded-lg border border-neutral-200/70 bg-white/70 px-2.5 py-1.5 text-sm shadow-sm hover:bg-neutral-100/80"
>
</button>
<button
disabled={tempKeys.length === 0}
onClick={() => {
persistVisible(tempKeys);
setOpenCustomize(false);
}}
className={`rounded-xl px-3 py-1.5 text-sm font-semibold shadow-sm ${
tempKeys.length === 0 ? "cursor-not-allowed bg-neutral-200 text-neutral-500" : "bg-neutral-900 text-white hover:bg-neutral-800"
}`}
>
</button>
</div>
<div className="grow overflow-y-auto p-5">
<div className="mb-4 flex flex-wrap items-center gap-2">
<button
onClick={() => setTempKeys(ALL_COLS.map((c) => c.key))}
className="rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-1.5 text-xs shadow-sm hover:bg-neutral-100/80"
>
</button>
<button
onClick={() => setTempKeys([])}
className="rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-1.5 text-xs shadow-sm hover:bg-neutral-100/80"
>
</button>
<button
onClick={() => setTempKeys(DEFAULT_VISIBLE)}
className="rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-1.5 text-xs shadow-sm hover:bg-neutral-100/80"
>
</button>
<span className="text-xs text-neutral-500"> {tempKeys.length} </span>
</div>
<div className="grid gap-4 sm:grid-cols-2">
{GROUPS.map((g) => (
<fieldset key={g.titleTH} className="rounded-2xl border border-neutral-200/70 bg-white/60 p-4">
<div className="mb-2 flex items-center justify-between">
<legend className="text-sm font-semibold">{g.titleTH}</legend>
<div className="flex gap-1">
<button
className="rounded-lg border border-neutral-200/70 bg-white/70 px-2 py-1 text-[11px] shadow-sm hover:bg-neutral-100/80"
onClick={() => setTempKeys((prev) => Array.from(new Set([...prev, ...g.keys])))}
>
</button>
<button
className="rounded-lg border border-neutral-200/70 bg-white/70 px-2 py-1 text-[11px] shadow-sm hover:bg-neutral-100/80"
onClick={() => setTempKeys((prev) => prev.filter((k) => !g.keys.includes(k)))}
>
</button>
</div>
</div>
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
{g.keys.map((k) => {
const col = ALL_COLS.find((c) => c.key === k)!;
const checked = tempKeys.includes(k);
return (
<label key={k} className="flex cursor-pointer items-center gap-2 text-sm">
<input
type="checkbox"
className="h-4 w-4 rounded border-neutral-300 text-neutral-900 focus:ring-neutral-900"
checked={checked}
onChange={(e) =>
setTempKeys((prev) =>
e.target.checked ? [...prev, k] : prev.filter((x) => x !== k),
)
}
/>
<span>{col.labelTH}</span>
</label>
);
})}
</div>
</fieldset>
))}
</div>
</div>
<div className="flex shrink-0 items-center justify-between border-t border-neutral-200/70 px-5 py-4">
<div className="text-xs text-neutral-500">
เคล็ดลับ: ระบบจะจำคอลัมน์ที่คุณเลือกไว้ในเบราว์เซอร์นี้
</div>
<div className="flex items-center gap-2">
<button
onClick={() => setOpenCustomize(false)}
className="rounded-xl border border-neutral-200/70 bg-white/70 px-3 py-1.5 text-sm shadow-sm hover:bg-neutral-100/80"
>
</button>
<button
disabled={tempKeys.length === 0}
onClick={() => {
persistVisible(tempKeys);
setOpenCustomize(false);
}}
className={`rounded-xl px-3 py-1.5 text-sm font-semibold shadow-sm ${
tempKeys.length === 0
? "cursor-not-allowed bg-neutral-200 text-neutral-500"
: "bg-neutral-900 text-white hover:bg-neutral-800"
}`}
>
</button>
</div>
</div>
</div>
</div>
</div>
)}
</div>
)}
</div>
</>
);
}

View File

@@ -1,5 +1,11 @@
@import "tailwindcss";
html { scrollbar-gutter: stable both-edges; }
@supports not (scrollbar-gutter: stable both-edges) {
html { overflow-y: scroll; }
}
:root {
--background: #ffffff;
--foreground: #171717;