Add timezone selector to Admin settings with full-stack support
- Add `timezone` column to User model (VARCHAR 64, default UTC) with idempotent startup migration - Expose and persist timezone via PATCH /api/users/me - Fix TimerEvent.occurred_at serialization to include UTC offset marker (+00:00) so JavaScript correctly parses timestamps as UTC - Add frontend utility (src/utils/time.js) with timezone-aware formatTime, getHHMM, getDateInTZ, tzDateTimeToUTC helpers and a curated IANA timezone list - Add Settings section to Admin page with timezone dropdown; saves to both the API and localStorage for the unauthenticated TV view - Update Activity Log to display and edit times in the user's timezone - Update TV dashboard clock to respect the saved timezone - Update README: features, setup steps, usage table, WebSocket events Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,7 @@ async def lifespan(app: FastAPI):
|
||||
await _add_column_if_missing(conn, "schedule_templates", "day_end_time", "TIME NULL")
|
||||
await _add_column_if_missing(conn, "schedule_blocks", "duration_minutes", "INT NULL")
|
||||
await _add_column_if_missing(conn, "children", "strikes", "INT NOT NULL DEFAULT 0")
|
||||
await _add_column_if_missing(conn, "users", "timezone", "VARCHAR(64) NOT NULL DEFAULT 'UTC'")
|
||||
yield
|
||||
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ class User(TimestampMixin, Base):
|
||||
full_name: Mapped[str] = mapped_column(String(255), nullable=False)
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
timezone: Mapped[str] = mapped_column(String(64), nullable=False, default="UTC")
|
||||
|
||||
children: Mapped[list["Child"]] = relationship("Child", back_populates="user") # noqa: F821
|
||||
subjects: Mapped[list["Subject"]] = relationship("Subject", back_populates="user") # noqa: F821
|
||||
|
||||
@@ -23,6 +23,8 @@ async def update_me(
|
||||
current_user.full_name = body.full_name
|
||||
if body.email is not None:
|
||||
current_user.email = body.email
|
||||
if body.timezone is not None:
|
||||
current_user.timezone = body.timezone
|
||||
await db.commit()
|
||||
await db.refresh(current_user)
|
||||
return current_user
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from datetime import date, datetime
|
||||
from pydantic import BaseModel
|
||||
from datetime import date, datetime, timezone
|
||||
from pydantic import BaseModel, field_serializer
|
||||
|
||||
|
||||
class ActivityLogCreate(BaseModel):
|
||||
@@ -45,3 +45,9 @@ class TimelineEventOut(BaseModel):
|
||||
subject_name: str | None = None
|
||||
subject_icon: str | None = None
|
||||
subject_color: str | None = None
|
||||
|
||||
@field_serializer("occurred_at")
|
||||
def serialize_occurred_at(self, dt: datetime) -> str:
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.isoformat()
|
||||
|
||||
@@ -7,6 +7,7 @@ class UserOut(BaseModel):
|
||||
full_name: str
|
||||
is_active: bool
|
||||
is_admin: bool
|
||||
timezone: str = "UTC"
|
||||
|
||||
model_config = {"from_attributes": True}
|
||||
|
||||
@@ -14,3 +15,4 @@ class UserOut(BaseModel):
|
||||
class UserUpdate(BaseModel):
|
||||
full_name: str | None = None
|
||||
email: EmailStr | None = None
|
||||
timezone: str | None = None
|
||||
|
||||
Reference in New Issue
Block a user