- {/* Tab-Navigation */}
+ {/* Tab-Navigation (Slots entfernt) */}
- {/* Tab Inhalt */}
- {activeSubTab === "slots" && (
- <>
- {/* Slot Type Selection */}
-
-
Neuen Slot anlegen
-
-
-
-
-
-
-
-
-
- setSelectedDate(e.target.value)}
- className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
- />
-
-
-
-
- setTime(e.target.value)}
- className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
- />
-
-
- {slotType === "treatment" ? (
-
-
-
-
- ) : (
-
-
- setDuration(Number(e.target.value))}
- className="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-pink-500"
- />
-
- )}
-
-
-
-
-
-
- {/* Treatment Info Display */}
- {slotType === "treatment" && selectedTreatment && (
-
-
-
-
{selectedTreatment.name}
-
{selectedTreatment.description}
-
-
-
- {(selectedTreatment.price / 100).toFixed(2)} €
-
-
- {selectedTreatment.duration} Minuten
-
-
-
-
- )}
-
-
- {(errorMsg || successMsg) && (
-
- {errorMsg && (
-
-
-
-
Fehler:
-
{errorMsg}
-
-
- )}
- {successMsg && (
-
-
-
-
Erfolg:
-
{successMsg}
-
-
- )}
-
- )}
-
- {/* Quick Stats */}
-
-
-
- {allSlots?.filter(s => s.status === "free").length || 0}
-
-
Freie Slots
-
-
-
- {allSlots?.filter(s => s.status === "reserved").length || 0}
-
-
Reservierte Slots
-
-
-
- {allSlots?.length || 0}
-
-
Slots gesamt
-
-
-
- {/* Cleanup Button */}
-
-
-
-
Bereinigung
-
Vergangene Slots automatisch löschen
-
-
-
-
-
- {/* All Slots List */}
-
-
-
Alle Slots
-
-
- {allSlots
- ?.sort((a, b) => (a.date === b.date ? a.time.localeCompare(b.time) : a.date.localeCompare(b.date)))
- .map((slot) => {
- // Try to find matching treatment based on duration
- const matchingTreatments = treatments?.filter(t => t.duration === slot.durationMinutes) || [];
-
- return (
-
-
-
-
-
Datum
-
{new Date(slot.date).toLocaleDateString('de-DE')}
-
-
-
-
Dauer
-
{slot.durationMinutes} Min
-
- {matchingTreatments.length > 0 && (
-
-
Passende Behandlungen
-
- {matchingTreatments.length === 1
- ? matchingTreatments[0].name
- : `${matchingTreatments.length} Behandlungen`
- }
-
-
- )}
-
- {slot.status === "free" ? "Frei" : "Reserviert"}
-
-
-
-
-
-
-
- {/* Show matching treatments if multiple */}
- {matchingTreatments.length > 1 && (
-
-
Passende Behandlungen:
-
- {matchingTreatments.map(treatment => (
-
- {treatment.name}
-
- ))}
-
-
- )}
-
- );
- })}
- {allSlots?.length === 0 && (
-
-
Keine Slots vorhanden
-
Legen Sie den ersten Slot an, um zu beginnen.
-
- )}
-
-
- >
- )}
+ {/* Slot-Tab und Slot-UI entfernt */}
{/* Tab "Wiederkehrende Zeiten" */}
{activeSubTab === "recurring" && (
diff --git a/src/server/rpc/availability.ts b/src/server/rpc/availability.ts
deleted file mode 100644
index 387fbfb..0000000
--- a/src/server/rpc/availability.ts
+++ /dev/null
@@ -1,250 +0,0 @@
-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
;
-
-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();
-
- // 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;
-});
-
-// Non-mutating function that returns all slots without auto-cleanup
-const peekAll = os.handler(async () => {
- const allSlots = await kv.getAllItems();
- return allSlots;
-});
-
-// Helper function to check if a date is in a time-off period
-function isDateInTimeOffPeriod(date: string, periods: Array<{startDate: string, endDate: string}>): boolean {
- return periods.some(period => date >= period.startDate && date <= period.endDate);
-}
-
-// Filtered list that excludes slots in time-off periods (for customer-facing endpoints)
-const listFiltered = 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`);
- }
-
- // Get remaining slots (all are now current/future)
- let remainingSlots = allSlots.filter(slot => !slotsToDelete.includes(slot.id));
-
- // Note: Time-off filtering would be added here if we had access to time-off periods
- // For now, we'll rely on the slot generation logic to not create slots during time-off periods
-
- 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,
- listFiltered,
- peekAll,
- get,
- getByDate,
- cleanupPastSlots,
- live,
-};
-
-
diff --git a/src/server/rpc/index.ts b/src/server/rpc/index.ts
index 3d72a88..5feea1e 100644
--- a/src/server/rpc/index.ts
+++ b/src/server/rpc/index.ts
@@ -2,7 +2,6 @@ import { demo } from "./demo/index.js";
import { router as treatments } from "./treatments.js";
import { router as bookings } from "./bookings.js";
import { router as auth } from "./auth.js";
-import { router as availability } from "./availability.js";
import { router as recurringAvailability } from "./recurring-availability.js";
import { router as cancellation } from "./cancellation.js";
import { router as legal } from "./legal.js";
@@ -12,7 +11,6 @@ export const router = {
treatments,
bookings,
auth,
- availability,
recurringAvailability,
cancellation,
legal,
diff --git a/src/server/rpc/recurring-availability.ts b/src/server/rpc/recurring-availability.ts
index b85037b..f3b344c 100644
--- a/src/server/rpc/recurring-availability.ts
+++ b/src/server/rpc/recurring-availability.ts
@@ -30,14 +30,12 @@ export type TimeOffPeriod = z.output;
const recurringRulesKV = createKV("recurringRules");
const timeOffPeriodsKV = createKV("timeOffPeriods");
-// Import existing availability KV
-import { router as availabilityRouter } from "./availability.js";
// Import bookings and treatments KV stores for getAvailableTimes endpoint
const bookingsKV = createKV("bookings");
const treatmentsKV = createKV("treatments");
-// Owner-Authentifizierung (kopiert aus availability.ts)
+// Owner-Authentifizierung
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");
@@ -236,23 +234,8 @@ const createTimeOff = os
createdAt: new Date().toISOString(),
};
- // Blockiere bestehende Slots in diesem Zeitraum
- const existingSlots = await call(availabilityRouter.peekAll, {}, {});
- let blockedCount = 0;
-
- for (const slot of existingSlots) {
- if (slot.date >= input.startDate && slot.date <= input.endDate && slot.status === "free") {
- await call(availabilityRouter.remove, { sessionId: input.sessionId, id: slot.id }, {});
- blockedCount++;
- }
- }
-
- if (blockedCount > 0) {
- console.log(`Blocked ${blockedCount} existing slots for time-off period: ${input.reason}`);
- }
-
await timeOffPeriodsKV.setItem(id, timeOff);
- return { ...timeOff, blockedSlots: blockedCount };
+ return timeOff;
} catch (err) {
console.error("recurring-availability.createTimeOff error", err);
throw err;
@@ -294,153 +277,6 @@ const adminListTimeOff = os
return allTimeOff.sort((a, b) => a.startDate.localeCompare(b.startDate));
});
-// Slot-Generator-Endpoint
-// DEPRECATED: This endpoint will be removed in a future version.
-// The system is transitioning to dynamic availability calculation with 15-minute intervals.
-// Slots are no longer pre-generated based on recurring rules.
-const generateSlots = os
- .input(
- z.object({
- sessionId: z.string(),
- startDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
- endDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
- overwriteExisting: z.boolean().default(false),
- })
- )
- .handler(async ({ input }) => {
- try {
- await assertOwner(input.sessionId);
-
- // Validierung: startDate <= endDate
- if (input.startDate > input.endDate) {
- throw new Error("Startdatum muss vor oder am Enddatum liegen.");
- }
-
- // Validierung: maximal 12 Wochen Zeitraum
- const start = new Date(input.startDate);
- const end = new Date(input.endDate);
- const daysDiff = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
- if (daysDiff > 84) { // 12 Wochen = 84 Tage
- throw new Error("Zeitraum darf maximal 12 Wochen betragen.");
- }
-
- // Lade alle aktiven Regeln
- const allRules = await recurringRulesKV.getAllItems();
- const activeRules = allRules.filter(rule => rule.isActive);
-
- // Lade alle Urlaubszeiten
- const timeOffPeriods = await timeOffPeriodsKV.getAllItems();
-
- // Lade bestehende Slots (ohne Auto-Cleanup)
- const existingSlots = await call(availabilityRouter.peekAll, {}, {});
-
- // Erstelle Set für effiziente Duplikat-Prüfung
- const existing = new Set(existingSlots.map(s => `${s.date}T${s.time}`));
-
- let created = 0;
- let skipped = 0;
- let updated = 0;
-
- // Iteriere über jeden Tag im Zeitraum
- const currentDate = new Date(start);
- while (currentDate <= end) {
- const dateStr = formatDate(currentDate);
- // Verwende lokale Datumskomponenten für korrekte Wochentag-Berechnung
- const [y, m, d] = dateStr.split('-').map(Number);
- const localDate = new Date(y, m - 1, d);
- const dayOfWeek = localDate.getDay(); // 0=Sonntag, 1=Montag, ...
-
- // Prüfe, ob Datum in einer Urlaubszeit liegt
- if (isDateInTimeOffPeriod(dateStr, timeOffPeriods)) {
- currentDate.setDate(currentDate.getDate() + 1);
- continue;
- }
-
- // Finde alle Regeln für diesen Wochentag
- const matchingRules = activeRules.filter(rule => rule.dayOfWeek === dayOfWeek);
-
- for (const rule of matchingRules) {
- // Skip rules without slotDurationMinutes (legacy field for deprecated generateSlots)
- if (!rule.slotDurationMinutes) {
- console.log(`Skipping rule ${rule.id} - no slotDurationMinutes defined (legacy field)`);
- continue;
- }
-
- const startMinutes = parseTime(rule.startTime);
- const endMinutes = parseTime(rule.endTime);
-
- // Generiere Slots in slotDurationMinutes-Schritten
- let currentMinutes = startMinutes;
- while (currentMinutes + rule.slotDurationMinutes <= endMinutes) {
- const timeStr = formatTime(currentMinutes);
- const key = `${dateStr}T${timeStr}`;
-
- // Prüfe, ob bereits ein Slot für dieses Datum+Zeit existiert
- if (existing.has(key)) {
- if (input.overwriteExisting) {
- // Finde den bestehenden Slot für Update
- const existingSlot = existingSlots.find(
- slot => slot.date === dateStr && slot.time === timeStr
- );
- if (existingSlot && existingSlot.status === "free") {
- // Überschreibe Dauer des bestehenden Slots
- const updatedSlot = {
- ...existingSlot,
- durationMinutes: rule.slotDurationMinutes,
- };
- await call(availabilityRouter.update, {
- sessionId: input.sessionId,
- ...updatedSlot
- }, {});
- updated++;
- } else {
- skipped++;
- }
- } else {
- skipped++;
- }
- } else {
- // Erstelle neuen Slot mit try/catch für Duplikat-Konflikte
- try {
- await call(availabilityRouter.create, {
- sessionId: input.sessionId,
- date: dateStr,
- time: timeStr,
- durationMinutes: rule.slotDurationMinutes,
- }, {});
- existing.add(key);
- created++;
- } catch (err: any) {
- // Behandle bekannte Duplikat-Fehler
- if (err.message && err.message.includes("bereits ein Slot")) {
- skipped++;
- } else {
- throw err; // Re-throw unbekannte Fehler
- }
- }
- }
-
- currentMinutes += rule.slotDurationMinutes;
- }
- }
-
- currentDate.setDate(currentDate.getDate() + 1);
- }
-
- const message = `${created} Slots erstellt, ${updated} aktualisiert, ${skipped} übersprungen.`;
- console.log(`Slot generation completed: ${message}`);
-
- return {
- created,
- updated,
- skipped,
- message,
- };
- } catch (err) {
- console.error("recurring-availability.generateSlots error", err);
- throw err;
- }
- });
// Get Available Times Endpoint
const getAvailableTimes = os
@@ -643,9 +479,6 @@ export const router = {
listTimeOff,
adminListTimeOff,
- // Generator
- generateSlots,
-
// Availability
getAvailableTimes,