Refactor and enhance game management, user roles, and state handling

This commit introduces significant changes across the backend and frontend to improve game creation, joining, and member management. Key updates include adding a host role, structured handling of winners, and a New Game modal in the frontend. The refactor also simplifies join codes, improves persistence for user themes, and enhances overall user interaction with better UI feedback and logic.
This commit is contained in:
2026-02-06 11:21:43 +01:00
parent d0f65b856e
commit 4669d1f8c4
9 changed files with 488 additions and 268 deletions

View File

@@ -1,19 +1,30 @@
import enum
import uuid
from sqlalchemy import String, Boolean, DateTime, ForeignKey, Integer, SmallInteger, UniqueConstraint
from sqlalchemy import (
String,
Boolean,
DateTime,
ForeignKey,
Integer,
SmallInteger,
UniqueConstraint,
)
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.sql import func
from .db import Base
class Role(str, enum.Enum):
admin = "admin"
user = "user"
class Category(str, enum.Enum):
suspect = "suspect"
item = "item"
location = "location"
class User(Base):
__tablename__ = "users"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
@@ -21,40 +32,39 @@ class User(Base):
password_hash: Mapped[str] = mapped_column(String)
role: Mapped[str] = mapped_column(String, default=Role.user.value)
disabled: Mapped[bool] = mapped_column(Boolean, default=False)
created_at: Mapped[str] = mapped_column(DateTime(timezone=True), server_default=func.now())
# UI preferences (persisted server-side)
# NEW: Theme im Userprofil (damit es auf anderen Geräten mitkommt)
theme_key: Mapped[str] = mapped_column(String, default="default")
created_at: Mapped[str] = mapped_column(DateTime(timezone=True), server_default=func.now())
class Game(Base):
__tablename__ = "games"
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
# Creator/owner (for audit), membership controls access
owner_user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"), index=True)
# NEW: Host (nur Host darf Winner setzen)
host_user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"), index=True)
name: Mapped[str] = mapped_column(String)
seed: Mapped[int] = mapped_column(Integer)
created_at: Mapped[str] = mapped_column(DateTime(timezone=True), server_default=func.now())
# "Kahoot"-style join code
join_code: Mapped[str] = mapped_column(String, unique=True, index=True)
# NEW: Join-Code (Kahoot-Style)
code: Mapped[str] = mapped_column(String, unique=True, index=True)
# Winner (shared for the game)
# NEW: Winner (aus Users, nicht Freitext)
winner_user_id: Mapped[str | None] = mapped_column(String, ForeignKey("users.id"), nullable=True)
created_at: Mapped[str] = mapped_column(DateTime(timezone=True), server_default=func.now())
class GameMember(Base):
__tablename__ = "game_members"
__table_args__ = (
UniqueConstraint("game_id", "user_id", name="uq_game_member"),
)
__table_args__ = (UniqueConstraint("game_id", "user_id", name="uq_game_member"),)
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
game_id: Mapped[str] = mapped_column(String, ForeignKey("games.id"), index=True)
user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"), index=True)
created_at: Mapped[str] = mapped_column(DateTime(timezone=True), server_default=func.now())
joined_at: Mapped[str] = mapped_column(DateTime(timezone=True), server_default=func.now())
class Entry(Base):
__tablename__ = "entries"
@@ -62,11 +72,13 @@ class Entry(Base):
category: Mapped[str] = mapped_column(String, index=True)
label: Mapped[str] = mapped_column(String)
class SheetState(Base):
__tablename__ = "sheet_state"
__table_args__ = (
UniqueConstraint("game_id", "owner_user_id", "entry_id", name="uq_sheet"),
)
id: Mapped[str] = mapped_column(String, primary_key=True, default=lambda: str(uuid.uuid4()))
game_id: Mapped[str] = mapped_column(String, ForeignKey("games.id"), index=True)
owner_user_id: Mapped[str] = mapped_column(String, ForeignKey("users.id"), index=True)
@@ -75,6 +87,6 @@ class SheetState(Base):
status: Mapped[int] = mapped_column(SmallInteger, default=0) # 0 unknown, 1 crossed, 2 confirmed, 3 maybe
note_tag: Mapped[str | None] = mapped_column(String, nullable=True) # null | 'i' | 'm' | 's'
# Frontend "s.XX" chip selection (persisted)
chip_code: Mapped[str | None] = mapped_column(String, nullable=True)
# NEW: Chip persistieren (statt LocalStorage)
chip: Mapped[str | None] = mapped_column(String, nullable=True)