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

View File

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