同日で時刻が被らない開催を登録できるように変更

This commit is contained in:
system_master 2026-01-24 19:18:54 +09:00
parent 2cd12da2f6
commit 60bb6935c9
3 changed files with 186 additions and 174 deletions

View File

@ -4,7 +4,7 @@
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev -p 3004", "dev": "next dev -p 3004",
"build": "next build", "build": "dotenv -e .env.production -- next build",
"start": "dotenv -e .env.production -- next start", "start": "dotenv -e .env.production -- next start",
"lint": "eslint" "lint": "eslint"
}, },

View File

@ -1,186 +1,184 @@
// src/app/api/reserve-setting/[id]/route.ts // src/app/api/reserve-setting/[id]/route.ts
import { NextResponse, type NextRequest } from "next/server"; import { NextResponse, type NextRequest } from "next/server";
import { prisma } from "@/lib/prisma"; import { prisma } from "@/lib/prisma";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { validateReserveSetting } from "@/lib/validators/reserveSettingValidator"; import { validateReserveSetting } from "@/lib/validators/reserveSettingValidator";
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { type RouteContext = { params: Promise<{ id: string }> };
try {
const { id } = await params;
const settingId = parseInt(id, 10);
const setting = await prisma.reserveSetting.findUnique({ export async function GET(req: NextRequest, context: RouteContext) {
where: { autonum: settingId }, try {
select: { const { id } = await context.params;
autonum: true, const settingId = parseInt(id, 10);
diner_date: true,
start_time: true,
end_time: true,
time_range: true,
limit_method: true,
limit_type: true,
reserve_limit: true,
},
});
if (!setting) { const setting = await prisma.reserveSetting.findUnique({
return NextResponse.json({ error: "開催が見つかりません" }, { status: 404 }); where: { autonum: settingId },
} select: {
autonum: true,
diner_date: true,
start_time: true,
end_time: true,
time_range: true,
limit_method: true,
limit_type: true,
reserve_limit: true,
},
});
// 予約数集計 if (!setting) {
let allCount = 0; return NextResponse.json({ error: "開催が見つかりません" }, { status: 404 });
const blockCounts: Record<string, number> = {}; }
if (setting.limit_method === "all") { let allCount = 0;
if (setting.limit_type === "set") { const blockCounts: Record<string, number> = {};
allCount = await prisma.reserveCustomer.count({
where: { reserve_num: settingId, cancel_flag: 0 },
});
} else {
const agg = await prisma.reserveCustomer.aggregate({
where: { reserve_num: settingId, cancel_flag: 0 },
_sum: { cust_parent: true, cust_child: true },
});
allCount = (agg._sum.cust_parent ?? 0) + (agg._sum.cust_child ?? 0);
}
} else {
if (setting.limit_type === "set") {
const rows = await prisma.reserveCustomer.groupBy({
by: ["reserve_time"],
where: { reserve_num: settingId, cancel_flag: 0 },
_count: { _all: true },
});
rows.forEach((r) => (blockCounts[r.reserve_time] = r._count._all));
} else {
const rows = await prisma.reserveCustomer.groupBy({
by: ["reserve_time"],
where: { reserve_num: settingId, cancel_flag: 0 },
_sum: { cust_parent: true, cust_child: true },
});
rows.forEach((r) => {
blockCounts[r.reserve_time] =
(r._sum.cust_parent ?? 0) + (r._sum.cust_child ?? 0);
});
}
}
return NextResponse.json({ if (setting.limit_method === "all") {
...setting, if (setting.limit_type === "set") {
current: { all: allCount, blocks: blockCounts }, allCount = await prisma.reserveCustomer.count({
}); where: { reserve_num: settingId, cancel_flag: 0 },
} catch (error) { });
console.error("GET Error:", error); } else {
return NextResponse.json({ error: "取得に失敗しました" }, { status: 500 }); const agg = await prisma.reserveCustomer.aggregate({
} where: { reserve_num: settingId, cancel_flag: 0 },
_sum: { cust_parent: true, cust_child: true },
});
allCount = (agg._sum.cust_parent ?? 0) + (agg._sum.cust_child ?? 0);
}
} else {
if (setting.limit_type === "set") {
const rows = await prisma.reserveCustomer.groupBy({
by: ["reserve_time"],
where: { reserve_num: settingId, cancel_flag: 0 },
_count: { _all: true },
});
rows.forEach((r) => (blockCounts[r.reserve_time] = r._count._all));
} else {
const rows = await prisma.reserveCustomer.groupBy({
by: ["reserve_time"],
where: { reserve_num: settingId, cancel_flag: 0 },
_sum: { cust_parent: true, cust_child: true },
});
rows.forEach((r) => {
blockCounts[r.reserve_time] =
(r._sum.cust_parent ?? 0) + (r._sum.cust_child ?? 0);
});
}
}
return NextResponse.json({
...setting,
current: { all: allCount, blocks: blockCounts },
});
} catch (error) {
console.error("GET Error:", error);
return NextResponse.json({ error: "取得に失敗しました" }, { status: 500 });
}
} }
export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { export async function PUT(req: NextRequest, context: RouteContext) {
try { try {
const { id } = await params; const { id } = await context.params;
const settingId = Number(id); const settingId = parseInt(id, 10);
const data = await req.json(); const data = await req.json();
// 必須チェック if (!data.dinerDate || !data.reserveStartDate || !data.reserveLimitDate) {
if (!data.dinerDate || !data.reserveStartDate || !data.reserveLimitDate) { return NextResponse.json({ error: "必須項目が入力されていません" }, { status: 400 });
return NextResponse.json({ error: "必須項目が入力されていません" }, { status: 400 }); }
}
const dinerDate = dayjs(data.dinerDate); const now = dayjs();
if (dinerDate.isBefore(dayjs())) { const dinerDate = dayjs(data.dinerDate);
return NextResponse.json({ error: "過去の日付は登録できません" }, { status: 400 }); if (dinerDate.isBefore(now)) {
} return NextResponse.json({ error: "過去の日付は登録できません" }, { status: 400 });
}
// 開催日重複チェック(自分以外) // 同日開催OK時間重複はNG
const checkDate = dinerDate.format("YYYY-MM-DD"); const dinerDateStr = dinerDate.format("YYYY-MM-DD");
const newStartTime = dayjs(data.dinerDate).format("HH:mm:ss");
const newEndTime = dayjs(`${data.dinerDate.split("T")[0]}T${data.endTime}`).format("HH:mm:ss");
const exists = await prisma.$queryRawUnsafe<{ dummy: number }[]>( const overlaps = await prisma.$queryRawUnsafe<{ dummy: number }[]>(
`SELECT 1 as dummy FROM reserve_setting WHERE diner_date = ? AND delete_flag = 0 AND autonum <> ? LIMIT 1`, `
checkDate, SELECT 1 as dummy
data.id FROM reserve_setting
); WHERE diner_date = ?
console.log([checkDate, data.id, exists]); AND delete_flag = 0
if (exists.length > 0) { AND autonum <> ?
return NextResponse.json( AND start_time < ?
{ error: "同じ開催日が既に登録されています" }, AND end_time > ?
{ status: 400 } LIMIT 1
); `,
} dinerDateStr,
settingId,
newEndTime,
newStartTime
);
// 共通バリデーション if (overlaps.length > 0) {
const validationError = validateReserveSetting(data); return NextResponse.json(
if (validationError) { { error: "同じ開催日で時間が重複する開催が既に登録されています" },
return NextResponse.json({ error: validationError }, { status: 400 }); { status: 400 }
} );
}
// JST文字列に変換 const validationError = validateReserveSetting(data);
const dinerDateStr = dayjs(data.dinerDate).format("YYYY-MM-DD"); if (validationError) {
const startTimeStr = dayjs(data.dinerDate).format("HH:mm:ss"); return NextResponse.json({ error: validationError }, { status: 400 });
const endTimeStr = dayjs(`${data.dinerDate.split("T")[0]}T${data.endTime}`).format( }
"HH:mm:ss"
);
const reserveStartStr = dayjs(data.reserveStartDate).format("YYYY-MM-DD HH:mm:ss");
const reserveLimitStr = dayjs(data.reserveLimitDate).format("YYYY-MM-DD HH:mm:ss");
// Raw SQL UPDATE const reserveStartStr = dayjs(data.reserveStartDate).format("YYYY-MM-DD HH:mm:ss");
await prisma.$executeRawUnsafe( const reserveLimitStr = dayjs(data.reserveLimitDate).format("YYYY-MM-DD HH:mm:ss");
`UPDATE reserve_setting
await prisma.$executeRawUnsafe(
`UPDATE reserve_setting
SET diner_date = ?, start_time = ?, end_time = ?, time_range = ?, SET diner_date = ?, start_time = ?, end_time = ?, time_range = ?,
reserve_startdate = ?, reserve_limitdate = ?, reserve_startdate = ?, reserve_limitdate = ?,
parent_money = ?, child_money = ?, limit_method = ?, limit_type = ?, parent_money = ?, child_money = ?, limit_method = ?, limit_type = ?,
reserve_limit = ?, note = ?, lastupdate = NOW() reserve_limit = ?, note = ?, lastupdate = NOW()
WHERE autonum = ?`, WHERE autonum = ?`,
dinerDateStr, dinerDateStr,
startTimeStr, newStartTime,
endTimeStr, newEndTime,
Number(data.timeRange), Number(data.timeRange),
reserveStartStr, reserveStartStr,
reserveLimitStr, reserveLimitStr,
Number(data.priceAdult), Number(data.priceAdult),
Number(data.priceChild), Number(data.priceChild),
data.limitMethod, data.limitMethod,
data.limitUnit, data.limitUnit,
Number(data.limitValue), Number(data.limitValue),
data.note || null, data.note || null,
settingId settingId
); );
return NextResponse.json({ success: true }, { status: 200 }); return NextResponse.json({ success: true }, { status: 200 });
} catch (error) { } catch (error) {
console.error("PUT Error:", error); console.error("PUT Error:", error);
return NextResponse.json({ error: "更新に失敗しました" }, { status: 500 }); return NextResponse.json({ error: "更新に失敗しました" }, { status: 500 });
} }
} }
interface DeleteExists { export async function DELETE(req: NextRequest, context: RouteContext) {
autonum: number; try {
} const { id } = await context.params;
// DELETE: 論理削除Raw SQL版 const settingId = parseInt(id, 10);
export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
try { const rows = (await prisma.$queryRawUnsafe(
const { id } = await params; `SELECT autonum FROM reserve_setting WHERE autonum = ? AND delete_flag = 0 LIMIT 1`,
const settingId = parseInt(id, 10); settingId
)) as { autonum: number }[];
// 存在確認
const rows = (await prisma.$queryRawUnsafe( if (rows.length === 0) {
`SELECT autonum FROM reserve_setting WHERE autonum = ? AND delete_flag = 0 LIMIT 1`, return NextResponse.json({ error: "対象データが存在しません" }, { status: 404 });
settingId }
)) as DeleteExists[];
if (rows.length === 0) { await prisma.$executeRawUnsafe(
return NextResponse.json({ error: "対象データが存在しません" }, { status: 404 }); `UPDATE reserve_setting SET delete_flag = 1, lastupdate = NOW() WHERE autonum = ?`,
} settingId
);
// 論理削除
await prisma.$executeRawUnsafe( return NextResponse.json({ success: true }, { status: 200 });
`UPDATE reserve_setting } catch (error) {
SET delete_flag = 1, lastupdate = NOW() console.error("DELETE Error:", error);
WHERE autonum = ?`, return NextResponse.json({ error: "削除に失敗しました" }, { status: 500 });
settingId }
);
return NextResponse.json({ success: true }, { status: 200 });
} catch (error) {
console.error("DELETE Error:", error);
return NextResponse.json({ error: "削除に失敗しました" }, { status: 500 });
}
} }

View File

@ -72,8 +72,6 @@ export async function GET() {
diner_range: `${dayjs(row.diner_date).format("M/DD")}\n${startHM}~${endHM}`, diner_range: `${dayjs(row.diner_date).format("M/DD")}\n${startHM}~${endHM}`,
end_hm: endHM, end_hm: endHM,
// ここは SQL 側で '%Y-%m-%d %H:%i:%s' の文字列にして返しているので
// その文字列を dayjs のフォーマットで ISO 風に成形
reserve_start: row.reserve_start_str reserve_start: row.reserve_start_str
? dayjs(row.reserve_start_str, "YYYY-MM-DD HH:mm:ss").format("YYYY-MM-DDTHH:mm") ? dayjs(row.reserve_start_str, "YYYY-MM-DD HH:mm:ss").format("YYYY-MM-DDTHH:mm")
: "", : "",
@ -106,38 +104,30 @@ export async function GET() {
} }
} }
// POST: 新規作成 // POST: 新規作成同日でも時間が重ならなければOK
export async function POST(req: NextRequest) { export async function POST(req: NextRequest) {
try { try {
const data = await req.json(); const data = await req.json();
console.log("受信:", data.timeRange);
// 必須チェック // 必須チェック
if (!data.dinerDate || !data.reserveStartDate || !data.reserveLimitDate) { if (!data.dinerDate || !data.reserveStartDate || !data.reserveLimitDate) {
return NextResponse.json({ error: "必須項目が入力されていません" }, { status: 400 }); return NextResponse.json({ error: "必須項目が入力されていません" }, { status: 400 });
} }
// 過去日禁止 // 過去日禁止
const now = dayjs(); const now = dayjs();
const dinerDate = dayjs(data.dinerDate); const dinerDate = dayjs(data.dinerDate);
if (dinerDate.isBefore(now)) { if (dinerDate.isBefore(now)) {
return NextResponse.json({ error: "過去の日付は登録できません" }, { status: 400 }); return NextResponse.json({ error: "過去の日付は登録できません" }, { status: 400 });
} }
// --- 同日開催禁止Raw SQL---
const checkDate = dinerDate.format("YYYY-MM-DD"); // 追加バリデーション(形式・範囲など)
const exists = await prisma.$queryRawUnsafe<{ dummy: number }[]>(
`SELECT 1 as dummy FROM reserve_setting WHERE diner_date = ? AND delete_flag = 0 LIMIT 1`,
checkDate
);
if (exists.length > 0) {
return NextResponse.json({ error: "同じ開催日で既に登録があります" }, { status: 400 });
}
// 追加バリデーション
const validationError = validateReserveSetting(data); const validationError = validateReserveSetting(data);
if (validationError) { if (validationError) {
return NextResponse.json({ error: validationError }, { status: 400 }); return NextResponse.json({ error: validationError }, { status: 400 });
} }
// JST文字列に変換 // JST文字列に変換※時間重複チェックでも使う
const dinerDateStr = dayjs(data.dinerDate).format("YYYY-MM-DD"); const dinerDateStr = dayjs(data.dinerDate).format("YYYY-MM-DD");
const startTimeStr = dayjs(data.dinerDate).format("HH:mm:ss"); const startTimeStr = dayjs(data.dinerDate).format("HH:mm:ss");
const endTimeStr = dayjs(`${data.dinerDate.split("T")[0]}T${data.endTime}`).format( const endTimeStr = dayjs(`${data.dinerDate.split("T")[0]}T${data.endTime}`).format(
@ -146,6 +136,30 @@ export async function POST(req: NextRequest) {
const reserveStartStr = dayjs(data.reserveStartDate).format("YYYY-MM-DD HH:mm:ss"); const reserveStartStr = dayjs(data.reserveStartDate).format("YYYY-MM-DD HH:mm:ss");
const reserveLimitStr = dayjs(data.reserveLimitDate).format("YYYY-MM-DD HH:mm:ss"); const reserveLimitStr = dayjs(data.reserveLimitDate).format("YYYY-MM-DD HH:mm:ss");
// --- 同日・時間重複チェック ---
// 重なり判定: existingStart < newEnd AND existingEnd > newStart
const overlap = await prisma.$queryRawUnsafe<{ dummy: number }[]>(
`
SELECT 1 as dummy
FROM reserve_setting
WHERE diner_date = ?
AND delete_flag = 0
AND start_time < ?
AND end_time > ?
LIMIT 1
`,
dinerDateStr,
endTimeStr,
startTimeStr
);
if (overlap.length > 0) {
return NextResponse.json(
{ error: "同じ開催日で時間帯が重複しています" },
{ status: 400 }
);
}
// Raw SQL で保存 // Raw SQL で保存
await prisma.$executeRawUnsafe( await prisma.$executeRawUnsafe(
`INSERT INTO reserve_setting `INSERT INTO reserve_setting
@ -163,7 +177,7 @@ export async function POST(req: NextRequest) {
parseInt(data.priceAdult, 10), parseInt(data.priceAdult, 10),
parseInt(data.priceChild, 10), parseInt(data.priceChild, 10),
data.limitMethod, data.limitMethod,
data.limitUnit, data.limitUnit, // NOTE: フロントのキー名に合わせて現状維持DB列は limit_type
parseInt(data.limitValue, 10) parseInt(data.limitValue, 10)
); );