176 lines
5.2 KiB
TypeScript
176 lines
5.2 KiB
TypeScript
// src/app/reservation/page.tsx
|
||
import { getRoomReservations, Reservation } from "@/lib/reservation";
|
||
import { RoomInfo } from "@/lib/roominfo";
|
||
|
||
const ROOMS = Object.keys(RoomInfo); // ["ENJI","PINK","MINT"]
|
||
const startHour = 7; // 開店時間
|
||
const endHour = 22; // 閉店時間
|
||
const TIMES = Array.from({ length: endHour - startHour }, (_, i) => `${startHour + i}:00`); // 7:00〜21:00
|
||
|
||
function getNextWeekDates(): Date[] {
|
||
const today = new Date();
|
||
const dates: Date[] = [];
|
||
const dayOfWeek = today.getDay();
|
||
const daysUntilNextSaturday = 6 - dayOfWeek + 7; // 翌週土曜まで
|
||
|
||
for (let i = 0; i <= daysUntilNextSaturday; i++) {
|
||
const d = new Date(today);
|
||
d.setDate(today.getDate() + i);
|
||
dates.push(d);
|
||
}
|
||
return dates;
|
||
}
|
||
|
||
function formatDate(date: Date) {
|
||
const y = date.getFullYear();
|
||
const m = (date.getMonth() + 1).toString().padStart(2, "0");
|
||
const d = date.getDate().toString().padStart(2, "0");
|
||
return `${y}-${m}-${d}`;
|
||
}
|
||
|
||
function buildReservationMap(reservations: Reservation[], dates: Date[]) {
|
||
const map: Record<string, Record<string, "◯" | "✗">> = {};
|
||
TIMES.forEach((time) => {
|
||
map[time] = {};
|
||
dates.forEach((date) => {
|
||
map[time][formatDate(date)] = "◯";
|
||
});
|
||
});
|
||
|
||
reservations.forEach((r) => {
|
||
const dateStr = formatDate(new Date(r.sdate));
|
||
if (!map[TIMES[0]][dateStr]) return;
|
||
|
||
if (r.type === "oneday") {
|
||
TIMES.forEach((time) => (map[time][dateStr] = "✗"));
|
||
} else if (r.type === "range") {
|
||
const [startH, startM] = r.start.split(":").map(Number);
|
||
const startMinutes = startH * 60 + startM;
|
||
|
||
const [endH, endM] = r.end.split(":").map(Number);
|
||
const endMinutes = endH * 60 + endM;
|
||
|
||
TIMES.forEach((time) => {
|
||
const [hour, minute] = time.split(":").map(Number);
|
||
const timeMinutes = hour * 60 + minute;
|
||
if (timeMinutes >= startMinutes && timeMinutes < endMinutes) {
|
||
map[time][dateStr] = "✗";
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
return map;
|
||
}
|
||
|
||
export default async function ReservationPage() {
|
||
const dates = getNextWeekDates();
|
||
const dateLabels = dates.map(
|
||
(d) =>
|
||
`${d.getMonth() + 1}/${d.getDate()} (${
|
||
["日", "月", "火", "水", "木", "金", "土"][d.getDay()]
|
||
})`
|
||
);
|
||
|
||
// 3部屋の予約データ取得
|
||
const roomData: Record<string, Record<string, Record<string, "◯" | "✗">>> = {};
|
||
for (const room of ROOMS) {
|
||
const reservations = await getRoomReservations(room);
|
||
roomData[room] = buildReservationMap(reservations, dates);
|
||
}
|
||
|
||
return (
|
||
<main className="max-w-7xl mx-auto px-4 py-8 space-y-12 text-stone-700">
|
||
{ROOMS.map((room) => (
|
||
<section
|
||
key={room}
|
||
className="bg-white shadow-sm rounded-2xl border border-stone-200 p-6 space-y-6"
|
||
>
|
||
<h2 className="text-2xl text-stone-800 font-semibold">{room} 予約表</h2>
|
||
<p>
|
||
外部予約サービス:{" "}
|
||
<a
|
||
href={RoomInfo[room].url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="text-blue-600 underline"
|
||
>
|
||
こちら
|
||
</a>
|
||
</p>
|
||
|
||
<div
|
||
className="flex border border-stone-200"
|
||
style={{ "--col-count": dates.length } as React.CSSProperties}
|
||
>
|
||
{/* 固定列(時間) */}
|
||
<div className="flex-shrink-0 bg-stone-50">
|
||
<table className="border-collapse border border-stone-200 w-full md:w-[10%] text-stone-700">
|
||
<thead className="bg-stone-100 text-stone-700">
|
||
<tr>
|
||
<th className="border border-stone-200 px-2 py-1 text-sm font-semibold whitespace-nowrap">
|
||
時間
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{TIMES.map((time) => (
|
||
<tr key={time} className="even:bg-stone-50">
|
||
<td className="border border-stone-200 px-2 py-1 text-sm font-medium whitespace-nowrap">
|
||
{time}
|
||
</td>
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{/* 横スクロール可能列 */}
|
||
<div className="overflow-x-auto flex-1 bg-white">
|
||
<table className="border-collapse border border-stone-200 w-full md:table-fixed text-stone-700">
|
||
<thead className="bg-stone-100 text-stone-800">
|
||
<tr>
|
||
{dateLabels.map((label) => (
|
||
<th
|
||
key={label}
|
||
className="border border-stone-200 px-2 py-1 text-sm font-medium whitespace-nowrap
|
||
md:w-[calc(90%/var(--col-count))]"
|
||
>
|
||
{label}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{TIMES.map((time) => (
|
||
<tr key={time} className="even:bg-stone-50">
|
||
{dates.map((date) => {
|
||
const dateStr = formatDate(date);
|
||
const val = roomData[room][time][dateStr];
|
||
return (
|
||
<td
|
||
key={date.toISOString()}
|
||
className={`border border-stone-200 px-2 py-1 text-center text-sm font-medium whitespace-nowrap
|
||
md:w-[calc(90%/var(--col-count))]
|
||
${
|
||
val === "✗"
|
||
? "bg-stone-100 text-stone-400"
|
||
: "bg-white text-stone-700"
|
||
}`}
|
||
>
|
||
{val}
|
||
</td>
|
||
);
|
||
})}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
))}
|
||
</main>
|
||
);
|
||
}
|