- React + Vite + TanStack Router/Query frontend - Hono + Prisma + MySQL backend - Prisma schema mit allen Entitäten - Docker + docker-compose Setup - Tailwind mit Salm/Ozean Farbpalette
261 lines
8.6 KiB
Plaintext
261 lines
8.6 KiB
Plaintext
// SchwarmbLog – Prisma Schema
|
||
// Stack: Hono + Prisma + MySQL
|
||
// Conventions: camelCase in Prisma, snake_case in DB, cuid() for IDs
|
||
|
||
generator client {
|
||
provider = "prisma-client-js"
|
||
}
|
||
|
||
datasource db {
|
||
provider = "mysql"
|
||
url = env("DATABASE_URL")
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// USERS & AUTH
|
||
// ─────────────────────────────────────────────
|
||
|
||
model User {
|
||
id String @id @default(cuid())
|
||
name String
|
||
email String? @unique
|
||
role Role @default(STUDENT)
|
||
|
||
// Auth
|
||
passwordHash String?
|
||
qrToken String? @unique @map("qr_token")
|
||
qrTokenCreated DateTime? @map("qr_token_created")
|
||
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
// Relations
|
||
classMemberships ClassMembership[]
|
||
swarmMemberships SwarmMembership[]
|
||
blog Blog?
|
||
assignedTodos Todo[] @relation("AssignedTodos")
|
||
reactions Reaction[]
|
||
comments Comment[]
|
||
|
||
@@map("users")
|
||
}
|
||
|
||
enum Role {
|
||
ADMIN
|
||
TEACHER
|
||
STUDENT
|
||
PARENT
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// CLASSES & SWARMS
|
||
// ─────────────────────────────────────────────
|
||
|
||
model Class {
|
||
id String @id @default(cuid())
|
||
name String // z.B. "7a"
|
||
schoolYear String @map("school_year") // z.B. "2025/26"
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
memberships ClassMembership[]
|
||
swarms Swarm[]
|
||
blogs Blog[]
|
||
|
||
@@map("classes")
|
||
}
|
||
|
||
model ClassMembership {
|
||
id String @id @default(cuid())
|
||
user User @relation(fields: [userId], references: [id])
|
||
userId String @map("user_id")
|
||
class Class @relation(fields: [classId], references: [id])
|
||
classId String @map("class_id")
|
||
role ClassRole @default(STUDENT)
|
||
|
||
@@unique([userId, classId])
|
||
@@map("class_memberships")
|
||
}
|
||
|
||
enum ClassRole {
|
||
TEACHER
|
||
STUDENT
|
||
}
|
||
|
||
// Schwarm = Sichtbarkeitsgruppe (Klasse oder ganzer Jahrgang)
|
||
model Swarm {
|
||
id String @id @default(cuid())
|
||
name String // z.B. "Schwarm 7a" oder "Schwarm Jahrgang 7"
|
||
scope SwarmScope
|
||
class Class? @relation(fields: [classId], references: [id])
|
||
classId String? @map("class_id")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
memberships SwarmMembership[]
|
||
|
||
@@map("swarms")
|
||
}
|
||
|
||
enum SwarmScope {
|
||
CLASS // nur eine Klasse
|
||
YEAR // ganzer Jahrgang
|
||
}
|
||
|
||
model SwarmMembership {
|
||
id String @id @default(cuid())
|
||
swarm Swarm @relation(fields: [swarmId], references: [id])
|
||
swarmId String @map("swarm_id")
|
||
user User @relation(fields: [userId], references: [id])
|
||
userId String @map("user_id")
|
||
|
||
@@unique([swarmId, userId])
|
||
@@map("swarm_memberships")
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// STATIONEN & TEMPLATES
|
||
// ─────────────────────────────────────────────
|
||
|
||
model Station {
|
||
id String @id @default(cuid())
|
||
name String // z.B. "Lagos"
|
||
country String
|
||
latitude Float
|
||
longitude Float
|
||
bneTopic String @map("bne_topic") // z.B. "Elektroschrott"
|
||
description String? @db.Text
|
||
orderIndex Int @map("order_index") // optimale Reihenfolge
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
templates EntryTemplate[]
|
||
blogEntries BlogEntry[]
|
||
|
||
@@map("stations")
|
||
}
|
||
|
||
// Vorstrukturierter Eintrag (Lückentext, Bildblock, etc.)
|
||
// blocks: JSON-Array von Block-Objekten
|
||
// Beispiel: [{ type: "gap_text", content: "Ich bin in ___ angekommen..." }, { type: "image" }, { type: "free_text" }]
|
||
model EntryTemplate {
|
||
id String @id @default(cuid())
|
||
station Station @relation(fields: [stationId], references: [id])
|
||
stationId String @map("station_id")
|
||
title String
|
||
blocks Json // Block-Struktur des Templates
|
||
isOptional Boolean @default(false) @map("is_optional")
|
||
orderIndex Int @default(0) @map("order_index")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
todos Todo[]
|
||
blogEntries BlogEntry[]
|
||
|
||
@@map("entry_templates")
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// BLOG & EINTRÄGE
|
||
// ─────────────────────────────────────────────
|
||
|
||
model Blog {
|
||
id String @id @default(cuid())
|
||
student User @relation(fields: [studentId], references: [id])
|
||
studentId String @unique @map("student_id")
|
||
class Class @relation(fields: [classId], references: [id])
|
||
classId String @map("class_id")
|
||
title String @default("Mein Reiseblog")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
entries BlogEntry[]
|
||
todos Todo[]
|
||
|
||
@@map("blogs")
|
||
}
|
||
|
||
model BlogEntry {
|
||
id String @id @default(cuid())
|
||
blog Blog @relation(fields: [blogId], references: [id])
|
||
blogId String @map("blog_id")
|
||
station Station? @relation(fields: [stationId], references: [id])
|
||
stationId String? @map("station_id")
|
||
template EntryTemplate? @relation(fields: [templateId], references: [id])
|
||
templateId String? @map("template_id") // null = freier Eintrag
|
||
title String
|
||
blocks Json // Ausgefüllte Block-Inhalte (Text, Bildpfad, etc.)
|
||
status EntryStatus @default(DRAFT)
|
||
visibility Visibility @default(PRIVATE)
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
reactions Reaction[]
|
||
comments Comment[]
|
||
|
||
@@map("blog_entries")
|
||
}
|
||
|
||
enum EntryStatus {
|
||
DRAFT
|
||
PUBLISHED
|
||
}
|
||
|
||
enum Visibility {
|
||
PRIVATE // nur Schüler + Lehrkraft
|
||
CLASS // Klassen-Schwarm
|
||
YEAR // Jahrgangs-Schwarm
|
||
PARENTS // zusätzlich Eltern
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// TODOS
|
||
// ─────────────────────────────────────────────
|
||
|
||
// Lehrkraft weist Schüler einen vorstrukturierten Eintrag zu
|
||
model Todo {
|
||
id String @id @default(cuid())
|
||
blog Blog @relation(fields: [blogId], references: [id])
|
||
blogId String @map("blog_id")
|
||
template EntryTemplate @relation(fields: [templateId], references: [id])
|
||
templateId String @map("template_id")
|
||
assignedBy User @relation("AssignedTodos", fields: [assignedById], references: [id])
|
||
assignedById String @map("assigned_by")
|
||
dueDate DateTime? @map("due_date")
|
||
completedAt DateTime? @map("completed_at")
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
|
||
@@map("todos")
|
||
}
|
||
|
||
// ─────────────────────────────────────────────
|
||
// SOZIALES
|
||
// ─────────────────────────────────────────────
|
||
|
||
model Reaction {
|
||
id String @id @default(cuid())
|
||
entry BlogEntry @relation(fields: [entryId], references: [id])
|
||
entryId String @map("entry_id")
|
||
user User @relation(fields: [userId], references: [id])
|
||
userId String @map("user_id")
|
||
type ReactionType @default(LIKE)
|
||
|
||
@@unique([entryId, userId]) // pro Eintrag nur eine Reaktion pro User
|
||
@@map("reactions")
|
||
}
|
||
|
||
enum ReactionType {
|
||
LIKE
|
||
}
|
||
|
||
model Comment {
|
||
id String @id @default(cuid())
|
||
entry BlogEntry @relation(fields: [entryId], references: [id])
|
||
entryId String @map("entry_id")
|
||
user User @relation(fields: [userId], references: [id])
|
||
userId String @map("user_id")
|
||
text String @db.Text
|
||
createdAt DateTime @default(now()) @map("created_at")
|
||
updatedAt DateTime @updatedAt @map("updated_at")
|
||
|
||
@@map("comments")
|
||
}
|