- Add confirmation modal for booking cancellations - Implement proper error handling and success messages - Fix live updates for booking status changes - Add manual refetch to ensure immediate UI updates - Auto-delete past availability slots on list access - Add manual cleanup function for past slots - Improve user experience with instant feedback
194 lines
5.8 KiB
TypeScript
194 lines
5.8 KiB
TypeScript
import { call, os } from "@orpc/server";
|
|
import { z } from "zod";
|
|
import { randomUUID } from "crypto";
|
|
import { createKV } from "../lib/create-kv.js";
|
|
|
|
const AvailabilitySchema = z.object({
|
|
id: z.string(),
|
|
date: z.string(), // YYYY-MM-DD
|
|
time: z.string(), // HH:MM
|
|
durationMinutes: z.number().int().positive(),
|
|
status: z.enum(["free", "reserved"]),
|
|
reservedByBookingId: z.string().optional(),
|
|
createdAt: z.string(),
|
|
});
|
|
|
|
export type Availability = z.output<typeof AvailabilitySchema>;
|
|
|
|
const kv = createKV<Availability>("availability");
|
|
|
|
// Minimal Owner-Prüfung über Sessions/Users KV
|
|
type Session = { id: string; userId: string; expiresAt: string; createdAt: string };
|
|
type User = { id: string; username: string; email: string; passwordHash: string; role: "customer" | "owner"; createdAt: string };
|
|
const sessionsKV = createKV<Session>("sessions");
|
|
const usersKV = createKV<User>("users");
|
|
|
|
async function assertOwner(sessionId: string): Promise<void> {
|
|
const session = await sessionsKV.getItem(sessionId);
|
|
if (!session) throw new Error("Invalid session");
|
|
if (new Date(session.expiresAt) < new Date()) throw new Error("Session expired");
|
|
const user = await usersKV.getItem(session.userId);
|
|
if (!user || user.role !== "owner") throw new Error("Forbidden");
|
|
}
|
|
|
|
const create = os
|
|
.input(
|
|
z.object({
|
|
sessionId: z.string(),
|
|
date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
|
|
time: z.string().regex(/^\d{2}:\d{2}$/),
|
|
durationMinutes: z.number().int().positive(),
|
|
})
|
|
)
|
|
.handler(async ({ input }) => {
|
|
try {
|
|
await assertOwner(input.sessionId);
|
|
// Prevent duplicate slot on same date+time
|
|
const existing = await kv.getAllItems();
|
|
const conflict = existing.some((s) => s.date === input.date && s.time === input.time);
|
|
if (conflict) {
|
|
throw new Error("Es existiert bereits ein Slot zu diesem Datum und dieser Uhrzeit.");
|
|
}
|
|
const id = randomUUID();
|
|
const slot: Availability = {
|
|
id,
|
|
date: input.date,
|
|
time: input.time,
|
|
durationMinutes: input.durationMinutes,
|
|
status: "free",
|
|
createdAt: new Date().toISOString(),
|
|
};
|
|
await kv.setItem(id, slot);
|
|
return slot;
|
|
} catch (err) {
|
|
console.error("availability.create error", err);
|
|
throw err;
|
|
}
|
|
});
|
|
|
|
const update = os
|
|
.input(AvailabilitySchema.extend({ sessionId: z.string() }))
|
|
.handler(async ({ input }) => {
|
|
await assertOwner(input.sessionId);
|
|
const { sessionId, ...rest } = input as any;
|
|
await kv.setItem(rest.id, rest as Availability);
|
|
return rest as Availability;
|
|
});
|
|
|
|
const remove = os
|
|
.input(z.object({ sessionId: z.string(), id: z.string() }))
|
|
.handler(async ({ input }) => {
|
|
await assertOwner(input.sessionId);
|
|
const slot = await kv.getItem(input.id);
|
|
if (slot && slot.status === "reserved") throw new Error("Cannot delete reserved slot");
|
|
await kv.removeItem(input.id);
|
|
});
|
|
|
|
const list = os.handler(async () => {
|
|
const allSlots = await kv.getAllItems();
|
|
|
|
// Auto-delete past slots
|
|
const today = new Date().toISOString().split("T")[0]; // YYYY-MM-DD
|
|
const now = new Date();
|
|
const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
|
|
|
let deletedCount = 0;
|
|
const slotsToDelete: string[] = [];
|
|
|
|
// Identify past slots for deletion
|
|
allSlots.forEach(slot => {
|
|
const isPastDate = slot.date < today;
|
|
const isPastTime = slot.date === today && slot.time <= currentTime;
|
|
|
|
if (isPastDate || isPastTime) {
|
|
slotsToDelete.push(slot.id);
|
|
}
|
|
});
|
|
|
|
// Delete past slots (only if not reserved)
|
|
for (const slotId of slotsToDelete) {
|
|
const slot = await kv.getItem(slotId);
|
|
if (slot && slot.status !== "reserved") {
|
|
await kv.removeItem(slotId);
|
|
deletedCount++;
|
|
}
|
|
}
|
|
|
|
if (deletedCount > 0) {
|
|
console.log(`Auto-deleted ${deletedCount} past availability slots`);
|
|
}
|
|
|
|
// Return remaining slots (all are now current/future)
|
|
const remainingSlots = allSlots.filter(slot => !slotsToDelete.includes(slot.id));
|
|
|
|
return remainingSlots;
|
|
});
|
|
|
|
const get = os.input(z.string()).handler(async ({ input }) => {
|
|
return kv.getItem(input);
|
|
});
|
|
|
|
const getByDate = os
|
|
.input(z.string()) // YYYY-MM-DD
|
|
.handler(async ({ input }) => {
|
|
const all = await kv.getAllItems();
|
|
return all.filter((s) => s.date === input);
|
|
});
|
|
|
|
// Cleanup function to manually delete past slots
|
|
const cleanupPastSlots = os
|
|
.input(z.object({ sessionId: z.string() }))
|
|
.handler(async ({ input }) => {
|
|
await assertOwner(input.sessionId);
|
|
|
|
const allSlots = await kv.getAllItems();
|
|
const today = new Date().toISOString().split("T")[0];
|
|
const now = new Date();
|
|
const currentTime = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
|
|
|
let deletedCount = 0;
|
|
|
|
for (const slot of allSlots) {
|
|
const isPastDate = slot.date < today;
|
|
const isPastTime = slot.date === today && slot.time <= currentTime;
|
|
|
|
if ((isPastDate || isPastTime) && slot.status !== "reserved") {
|
|
await kv.removeItem(slot.id);
|
|
deletedCount++;
|
|
}
|
|
}
|
|
|
|
console.log(`Manual cleanup: deleted ${deletedCount} past availability slots`);
|
|
return { deletedCount, message: `${deletedCount} vergangene Slots wurden gelöscht.` };
|
|
});
|
|
|
|
const live = {
|
|
list: os.handler(async function* ({ signal }) {
|
|
yield call(list, {}, { signal });
|
|
for await (const _ of kv.subscribe()) {
|
|
yield call(list, {}, { signal });
|
|
}
|
|
}),
|
|
byDate: os
|
|
.input(z.string())
|
|
.handler(async function* ({ input, signal }) {
|
|
yield call(getByDate, input, { signal });
|
|
for await (const _ of kv.subscribe()) {
|
|
yield call(getByDate, input, { signal });
|
|
}
|
|
}),
|
|
};
|
|
|
|
export const router = {
|
|
create,
|
|
update,
|
|
remove,
|
|
list,
|
|
get,
|
|
getByDate,
|
|
cleanupPastSlots,
|
|
live,
|
|
};
|
|
|
|
|