added floor plan to rooms

This commit is contained in:
Rajpreet0 2026-10-02 00:41:54 +02:00
parent 9fa335ea40
commit a8db32bf24
33 changed files with 2695 additions and 61 deletions

View file

@ -0,0 +1,34 @@
-- AlterTable
ALTER TABLE "Course" ADD COLUMN "spotBooking" BOOLEAN NOT NULL DEFAULT false;
-- AlterTable
ALTER TABLE "CourseBooking" ADD COLUMN "spotId" TEXT,
ADD COLUMN "spotLabel" TEXT;
-- AlterTable
ALTER TABLE "Room" ADD COLUMN "floorPlan" JSONB;
-- CreateTable
CREATE TABLE "CourseSpotHold" (
"id" TEXT NOT NULL,
"courseId" TEXT NOT NULL,
"spotId" TEXT NOT NULL,
"holderKey" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "CourseSpotHold_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "CourseSpotHold_courseId_idx" ON "CourseSpotHold"("courseId");
-- CreateIndex
CREATE UNIQUE INDEX "CourseSpotHold_courseId_spotId_key" ON "CourseSpotHold"("courseId", "spotId");
-- CreateIndex
CREATE INDEX "CourseBooking_spotId_idx" ON "CourseBooking"("spotId");
-- AddForeignKey
ALTER TABLE "CourseSpotHold" ADD CONSTRAINT "CourseSpotHold_courseId_fkey" FOREIGN KEY ("courseId") REFERENCES "Course"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View file

@ -172,9 +172,13 @@ model Course {
createdBy String // Manager ID who created the course
businessId String?
coverImage String?
// Kunden wählen beim Buchen ein Gerät aus dem Grundriss des Raums. Wirkt nur,
// solange der Raum buchbare Geräte hat - sonst wird ganz normal ohne Platz gebucht.
spotBooking Boolean @default(false)
business Business? @relation(fields: [businessId], references: [id], onDelete: SetNull)
bookings CourseBooking[]
waitlistEntries CourseWaitlistEntry[]
spotHolds CourseSpotHold[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@ -190,6 +194,10 @@ model Room {
id String @id @default(uuid())
name String
createdBy String // Manager ID who created the room
// Grundriss mit Geräten: { width, depth, items: [{ id, type, x, y, rotation, label }] }.
// Format und Gerätekatalog siehe src/modules/rooms/lib/floor-plan.ts. null = kein Grundriss.
// Die item-IDs sind stabil und stehen als CourseBooking.spotId an Buchungen.
floorPlan Json?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@ -226,6 +234,8 @@ model CourseBooking {
cancellationReason String?
discountCodeId String? // Gesetzt, sobald bei der Buchung ein gültiger Rabattcode eingelöst wurde
discountAmount Int? // Tatsächlich gewährter Rabatt in Cents (Snapshot, unabhängig von späteren Code-Änderungen)
spotId String? // Gerät aus dem Raum-Grundriss (Room.floorPlan.items[].id), nur bei Kursen mit Platzwahl. Belegt ist ein Platz, solange die Buchung "paid" ist.
spotLabel String? // Anzeigename des Platzes als Momentaufnahme, z.B. "Reformer 3" - bleibt lesbar, auch wenn der Grundriss sich später ändert
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
user User? @relation(fields: [userId], references: [id], onDelete: Cascade)
guestLead GuestLead? @relation(fields: [guestLeadId], references: [id], onDelete: SetNull)
@ -238,6 +248,28 @@ model CourseBooking {
@@index([userId])
@@index([guestLeadId])
@@index([discountCodeId])
@@index([spotId])
}
// Hält einen Platz frei, während der Kunde im Stripe-Checkout bezahlt - analog
// zur DiscountRedemption-Reservierung. Ohne sie könnten zwei Kunden gleichzeitig
// mit demselben Reformer zur Kasse gehen. Entsteht beim Checkout-Start, wird im
// Webhook nach der Zahlung gelöscht (dann belegt die Buchung den Platz) oder bei
// abgelaufener Session freigegeben. Abgelaufene Holds (expiresAt < jetzt) zählen
// nicht und werden beim nächsten Reservieren aufgeräumt.
// holderKey = "user:<id>" oder "guest:<email>": pro Person und Kurs gibt es höchstens
// einen Hold - wer zurück geht und einen anderen Platz wählt, gibt den alten frei.
model CourseSpotHold {
id String @id @default(uuid())
courseId String
spotId String
holderKey String
expiresAt DateTime
course Course @relation(fields: [courseId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
@@unique([courseId, spotId])
@@index([courseId])
}
// Bewusst nur für registrierte User (kein guestEmail-Feld): ein