同日で時刻が被らない開催を登録できるように変更
This commit is contained in:
parent
2cd12da2f6
commit
60bb6935c9
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user