import { call, os } from "@orpc/server"; import { z } from "zod"; import { randomUUID } from "crypto"; import { createKV } from "@/server/lib/create-kv"; 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; const kv = createKV("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("sessions"); const usersKV = createKV("users"); async function assertOwner(sessionId: string): Promise { 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(); // Filter out past slots automatically 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")}`; const filteredSlots = allSlots.filter(slot => { // Keep slots for future dates if (slot.date > today) return true; // For today: only keep future time slots if (slot.date === today) { return slot.time > currentTime; } // Remove past slots return false; }); // Debug logging (commented out - uncomment if needed) // const statusCounts = filteredSlots.reduce((acc, slot) => { // acc[slot.status] = (acc[slot.status] || 0) + 1; // return acc; // }, {} as Record); // console.log(`Availability list: ${filteredSlots.length} slots (${JSON.stringify(statusCounts)})`); return filteredSlots; }); 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); }); 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, live, };