Remove school day hours from schedule templates
The day progress bar no longer uses day start/end times (it uses block durations instead), so the field is no longer needed. Removed from: Admin UI, schedule store, schedule model/schemas/router, session broadcast payload, dashboard snapshot, and startup migrations. DB columns are left in place (harmless, no migration required). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -30,8 +30,6 @@ async def lifespan(app: FastAPI):
|
|||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
# Idempotent column additions for schema migrations
|
# Idempotent column additions for schema migrations
|
||||||
await _add_column_if_missing(conn, "schedule_templates", "day_start_time", "TIME NULL")
|
|
||||||
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, "schedule_blocks", "duration_minutes", "INT NULL")
|
||||||
await _add_column_if_missing(conn, "schedule_blocks", "break_time_enabled", "TINYINT(1) NOT NULL DEFAULT 0")
|
await _add_column_if_missing(conn, "schedule_blocks", "break_time_enabled", "TINYINT(1) NOT NULL DEFAULT 0")
|
||||||
await _add_column_if_missing(conn, "schedule_blocks", "break_time_minutes", "INT NULL")
|
await _add_column_if_missing(conn, "schedule_blocks", "break_time_minutes", "INT NULL")
|
||||||
|
|||||||
@@ -14,8 +14,6 @@ class ScheduleTemplate(TimestampMixin, Base):
|
|||||||
)
|
)
|
||||||
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
name: Mapped[str] = mapped_column(String(100), nullable=False)
|
||||||
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
|
is_default: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||||
day_start_time: Mapped[time | None] = mapped_column(Time, nullable=True)
|
|
||||||
day_end_time: Mapped[time | None] = mapped_column(Time, nullable=True)
|
|
||||||
|
|
||||||
user: Mapped["User"] = relationship("User", back_populates="schedule_templates") # noqa: F821
|
user: Mapped["User"] = relationship("User", back_populates="schedule_templates") # noqa: F821
|
||||||
child: Mapped["Child | None"] = relationship("Child") # noqa: F821
|
child: Mapped["Child | None"] = relationship("Child") # noqa: F821
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from app.dependencies import get_db
|
|||||||
from app.models.child import Child
|
from app.models.child import Child
|
||||||
from app.models.morning_routine import MorningRoutineItem
|
from app.models.morning_routine import MorningRoutineItem
|
||||||
from app.models.break_activity import BreakActivityItem
|
from app.models.break_activity import BreakActivityItem
|
||||||
from app.models.schedule import ScheduleBlock, ScheduleTemplate
|
from app.models.schedule import ScheduleBlock
|
||||||
from app.models.subject import Subject # noqa: F401 — needed for selectinload chain
|
from app.models.subject import Subject # noqa: F401 — needed for selectinload chain
|
||||||
from app.models.session import DailySession, TimerEvent
|
from app.models.session import DailySession, TimerEvent
|
||||||
from app.schemas.session import DashboardSnapshot
|
from app.schemas.session import DashboardSnapshot
|
||||||
@@ -45,8 +45,6 @@ async def get_dashboard(child_id: int, db: AsyncSession = Depends(get_db)):
|
|||||||
blocks = []
|
blocks = []
|
||||||
completed_ids = []
|
completed_ids = []
|
||||||
block_elapsed_seconds = 0
|
block_elapsed_seconds = 0
|
||||||
day_start_time = None
|
|
||||||
day_end_time = None
|
|
||||||
|
|
||||||
if session and session.template_id:
|
if session and session.template_id:
|
||||||
blocks_result = await db.execute(
|
blocks_result = await db.execute(
|
||||||
@@ -57,14 +55,6 @@ async def get_dashboard(child_id: int, db: AsyncSession = Depends(get_db)):
|
|||||||
)
|
)
|
||||||
blocks = blocks_result.scalars().all()
|
blocks = blocks_result.scalars().all()
|
||||||
|
|
||||||
template_result = await db.execute(
|
|
||||||
select(ScheduleTemplate).where(ScheduleTemplate.id == session.template_id)
|
|
||||||
)
|
|
||||||
template = template_result.scalar_one_or_none()
|
|
||||||
if template:
|
|
||||||
day_start_time = template.day_start_time
|
|
||||||
day_end_time = template.day_end_time
|
|
||||||
|
|
||||||
events_result = await db.execute(
|
events_result = await db.execute(
|
||||||
select(TimerEvent).where(
|
select(TimerEvent).where(
|
||||||
TimerEvent.session_id == session.id,
|
TimerEvent.session_id == session.id,
|
||||||
@@ -101,8 +91,6 @@ async def get_dashboard(child_id: int, db: AsyncSession = Depends(get_db)):
|
|||||||
completed_block_ids=completed_ids,
|
completed_block_ids=completed_ids,
|
||||||
block_elapsed_seconds=block_elapsed_seconds,
|
block_elapsed_seconds=block_elapsed_seconds,
|
||||||
is_paused=is_paused,
|
is_paused=is_paused,
|
||||||
day_start_time=day_start_time,
|
|
||||||
day_end_time=day_end_time,
|
|
||||||
morning_routine=morning_routine,
|
morning_routine=morning_routine,
|
||||||
break_activities=break_activities,
|
break_activities=break_activities,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -44,8 +44,6 @@ async def create_template(
|
|||||||
name=body.name,
|
name=body.name,
|
||||||
child_id=body.child_id,
|
child_id=body.child_id,
|
||||||
is_default=body.is_default,
|
is_default=body.is_default,
|
||||||
day_start_time=body.day_start_time,
|
|
||||||
day_end_time=body.day_end_time,
|
|
||||||
)
|
)
|
||||||
db.add(template)
|
db.add(template)
|
||||||
await db.flush() # get template.id before adding blocks
|
await db.flush() # get template.id before adding blocks
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from app.dependencies import get_db, get_current_user
|
|||||||
from app.models.child import Child
|
from app.models.child import Child
|
||||||
from app.models.morning_routine import MorningRoutineItem
|
from app.models.morning_routine import MorningRoutineItem
|
||||||
from app.models.break_activity import BreakActivityItem
|
from app.models.break_activity import BreakActivityItem
|
||||||
from app.models.schedule import ScheduleBlock, ScheduleTemplate
|
from app.models.schedule import ScheduleBlock
|
||||||
from app.models.subject import Subject # noqa: F401 — needed for selectinload chain
|
from app.models.subject import Subject # noqa: F401 — needed for selectinload chain
|
||||||
from app.models.session import DailySession, TimerEvent
|
from app.models.session import DailySession, TimerEvent
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
@@ -23,8 +23,6 @@ router = APIRouter(prefix="/api/sessions", tags=["sessions"])
|
|||||||
async def _broadcast_session(db: AsyncSession, session: DailySession) -> None:
|
async def _broadcast_session(db: AsyncSession, session: DailySession) -> None:
|
||||||
"""Build a snapshot dict and broadcast it to all connected TVs for this child."""
|
"""Build a snapshot dict and broadcast it to all connected TVs for this child."""
|
||||||
blocks = []
|
blocks = []
|
||||||
day_start_time = None
|
|
||||||
day_end_time = None
|
|
||||||
|
|
||||||
if session.template_id:
|
if session.template_id:
|
||||||
blocks_result = await db.execute(
|
blocks_result = await db.execute(
|
||||||
@@ -56,14 +54,6 @@ async def _broadcast_session(db: AsyncSession, session: DailySession) -> None:
|
|||||||
for b in blocks_result.scalars().all()
|
for b in blocks_result.scalars().all()
|
||||||
]
|
]
|
||||||
|
|
||||||
template_result = await db.execute(
|
|
||||||
select(ScheduleTemplate).where(ScheduleTemplate.id == session.template_id)
|
|
||||||
)
|
|
||||||
template = template_result.scalar_one_or_none()
|
|
||||||
if template:
|
|
||||||
day_start_time = str(template.day_start_time) if template.day_start_time else None
|
|
||||||
day_end_time = str(template.day_end_time) if template.day_end_time else None
|
|
||||||
|
|
||||||
# Gather completed block IDs from timer events
|
# Gather completed block IDs from timer events
|
||||||
events_result = await db.execute(
|
events_result = await db.execute(
|
||||||
select(TimerEvent).where(
|
select(TimerEvent).where(
|
||||||
@@ -105,8 +95,6 @@ async def _broadcast_session(db: AsyncSession, session: DailySession) -> None:
|
|||||||
},
|
},
|
||||||
"blocks": blocks,
|
"blocks": blocks,
|
||||||
"completed_block_ids": completed_ids,
|
"completed_block_ids": completed_ids,
|
||||||
"day_start_time": day_start_time,
|
|
||||||
"day_end_time": day_end_time,
|
|
||||||
"morning_routine": morning_routine,
|
"morning_routine": morning_routine,
|
||||||
"break_activities": break_activities,
|
"break_activities": break_activities,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,6 @@ class ScheduleTemplateCreate(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
child_id: int | None = None
|
child_id: int | None = None
|
||||||
is_default: bool = False
|
is_default: bool = False
|
||||||
day_start_time: time | None = None
|
|
||||||
day_end_time: time | None = None
|
|
||||||
blocks: list[ScheduleBlockCreate] = []
|
blocks: list[ScheduleBlockCreate] = []
|
||||||
|
|
||||||
|
|
||||||
@@ -56,8 +54,6 @@ class ScheduleTemplateUpdate(BaseModel):
|
|||||||
name: str | None = None
|
name: str | None = None
|
||||||
child_id: int | None = None
|
child_id: int | None = None
|
||||||
is_default: bool | None = None
|
is_default: bool | None = None
|
||||||
day_start_time: time | None = None
|
|
||||||
day_end_time: time | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class ScheduleTemplateOut(BaseModel):
|
class ScheduleTemplateOut(BaseModel):
|
||||||
@@ -65,8 +61,6 @@ class ScheduleTemplateOut(BaseModel):
|
|||||||
name: str
|
name: str
|
||||||
child_id: int | None
|
child_id: int | None
|
||||||
is_default: bool
|
is_default: bool
|
||||||
day_start_time: time | None
|
|
||||||
day_end_time: time | None
|
|
||||||
blocks: list[ScheduleBlockOut] = []
|
blocks: list[ScheduleBlockOut] = []
|
||||||
|
|
||||||
model_config = {"from_attributes": True}
|
model_config = {"from_attributes": True}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from datetime import date, datetime, time
|
from datetime import date, datetime
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from app.schemas.schedule import ScheduleBlockOut
|
from app.schemas.schedule import ScheduleBlockOut
|
||||||
from app.schemas.child import ChildOut
|
from app.schemas.child import ChildOut
|
||||||
@@ -44,7 +44,5 @@ class DashboardSnapshot(BaseModel):
|
|||||||
completed_block_ids: list[int] = []
|
completed_block_ids: list[int] = []
|
||||||
block_elapsed_seconds: int = 0 # seconds already elapsed for the current block
|
block_elapsed_seconds: int = 0 # seconds already elapsed for the current block
|
||||||
is_paused: bool = False # whether the current block's timer is paused
|
is_paused: bool = False # whether the current block's timer is paused
|
||||||
day_start_time: time | None = None
|
|
||||||
day_end_time: time | None = None
|
|
||||||
morning_routine: list[str] = [] # text items shown on TV during greeting state
|
morning_routine: list[str] = [] # text items shown on TV during greeting state
|
||||||
break_activities: list[str] = [] # text items shown on TV during break time
|
break_activities: list[str] = [] # text items shown on TV during break time
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
const blockStartedAt = ref(null) // Date.now() ms when current counting period started
|
const blockStartedAt = ref(null) // Date.now() ms when current counting period started
|
||||||
const blockElapsedOffset = ref(0) // seconds already elapsed before blockStartedAt
|
const blockElapsedOffset = ref(0) // seconds already elapsed before blockStartedAt
|
||||||
const blockElapsedCache = ref({}) // blockId → total elapsed seconds (survives block switches)
|
const blockElapsedCache = ref({}) // blockId → total elapsed seconds (survives block switches)
|
||||||
const dayStartTime = ref(null) // "HH:MM:SS" string or null
|
|
||||||
const dayEndTime = ref(null) // "HH:MM:SS" string or null
|
|
||||||
const morningRoutine = ref([]) // list of text strings shown during greeting state
|
const morningRoutine = ref([]) // list of text strings shown during greeting state
|
||||||
const breakActivities = ref([]) // list of text strings shown during break time
|
const breakActivities = ref([]) // list of text strings shown during break time
|
||||||
// Break timer state (per-block break time at end of block)
|
// Break timer state (per-block break time at end of block)
|
||||||
@@ -45,8 +43,6 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
completedBlockIds.value = snapshot.completed_block_ids || []
|
completedBlockIds.value = snapshot.completed_block_ids || []
|
||||||
isPaused.value = snapshot.is_paused || false
|
isPaused.value = snapshot.is_paused || false
|
||||||
if (snapshot.child) child.value = snapshot.child
|
if (snapshot.child) child.value = snapshot.child
|
||||||
dayStartTime.value = snapshot.day_start_time || null
|
|
||||||
dayEndTime.value = snapshot.day_end_time || null
|
|
||||||
morningRoutine.value = snapshot.morning_routine || []
|
morningRoutine.value = snapshot.morning_routine || []
|
||||||
breakActivities.value = snapshot.break_activities || []
|
breakActivities.value = snapshot.break_activities || []
|
||||||
// Restore elapsed time from server-computed value and seed the per-block cache
|
// Restore elapsed time from server-computed value and seed the per-block cache
|
||||||
@@ -86,8 +82,6 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
blockStartedAt.value = null
|
blockStartedAt.value = null
|
||||||
blockElapsedOffset.value = 0
|
blockElapsedOffset.value = 0
|
||||||
blockElapsedCache.value = {}
|
blockElapsedCache.value = {}
|
||||||
dayStartTime.value = null
|
|
||||||
dayEndTime.value = null
|
|
||||||
isBreakMode.value = false
|
isBreakMode.value = false
|
||||||
breakStartedAt.value = null
|
breakStartedAt.value = null
|
||||||
breakElapsedOffset.value = 0
|
breakElapsedOffset.value = 0
|
||||||
@@ -322,8 +316,6 @@ export const useScheduleStore = defineStore('schedule', () => {
|
|||||||
blockStartedAt,
|
blockStartedAt,
|
||||||
blockElapsedOffset,
|
blockElapsedOffset,
|
||||||
blockElapsedCache,
|
blockElapsedCache,
|
||||||
dayStartTime,
|
|
||||||
dayEndTime,
|
|
||||||
morningRoutine,
|
morningRoutine,
|
||||||
breakActivities,
|
breakActivities,
|
||||||
isBreakMode,
|
isBreakMode,
|
||||||
|
|||||||
@@ -229,24 +229,6 @@
|
|||||||
|
|
||||||
<!-- Block editor -->
|
<!-- Block editor -->
|
||||||
<div v-if="editingTemplate === template.id" class="block-editor">
|
<div v-if="editingTemplate === template.id" class="block-editor">
|
||||||
<!-- Day hours -->
|
|
||||||
<div class="day-hours-row">
|
|
||||||
<span class="day-hours-label">School day hours</span>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
:value="template.day_start_time ? template.day_start_time.slice(0,5) : ''"
|
|
||||||
@change="e => saveDayHours(template, 'start', e.target.value)"
|
|
||||||
placeholder="Start"
|
|
||||||
/>
|
|
||||||
<span class="day-hours-sep">to</span>
|
|
||||||
<input
|
|
||||||
type="time"
|
|
||||||
:value="template.day_end_time ? template.day_end_time.slice(0,5) : ''"
|
|
||||||
@change="e => saveDayHours(template, 'end', e.target.value)"
|
|
||||||
placeholder="End"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="block-list">
|
<div class="block-list">
|
||||||
<template v-for="block in sortedBlocks(template.blocks)" :key="block.id">
|
<template v-for="block in sortedBlocks(template.blocks)" :key="block.id">
|
||||||
<!-- Edit mode -->
|
<!-- Edit mode -->
|
||||||
@@ -661,13 +643,6 @@ async function deleteBlock(templateId, blockId) {
|
|||||||
await loadTemplates()
|
await loadTemplates()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveDayHours(template, which, value) {
|
|
||||||
const payload = which === 'start'
|
|
||||||
? { day_start_time: value || null }
|
|
||||||
: { day_end_time: value || null }
|
|
||||||
await api.patch(`/api/schedules/${template.id}`, payload)
|
|
||||||
await loadTemplates()
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await childrenStore.fetchChildren()
|
await childrenStore.fetchChildren()
|
||||||
@@ -762,25 +737,6 @@ h2 { font-size: 1.1rem; color: #94a3b8; text-transform: uppercase; letter-spacin
|
|||||||
|
|
||||||
.block-editor { margin-top: 1.25rem; border-top: 1px solid #334155; padding-top: 1.25rem; }
|
.block-editor { margin-top: 1.25rem; border-top: 1px solid #334155; padding-top: 1.25rem; }
|
||||||
|
|
||||||
.day-hours-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.6rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
background: #0f172a;
|
|
||||||
padding: 0.6rem 0.85rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
}
|
|
||||||
.day-hours-label { font-size: 0.8rem; color: #64748b; flex: 1; }
|
|
||||||
.day-hours-sep { font-size: 0.8rem; color: #475569; }
|
|
||||||
.day-hours-row input[type="time"] {
|
|
||||||
padding: 0.35rem 0.5rem;
|
|
||||||
background: #1e293b;
|
|
||||||
border: 1px solid #334155;
|
|
||||||
border-radius: 0.4rem;
|
|
||||||
color: #f1f5f9;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
.block-list { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 1rem; }
|
.block-list { display: flex; flex-direction: column; gap: 0.4rem; margin-bottom: 1rem; }
|
||||||
.block-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem; background: #0f172a; border-radius: 0.5rem; }
|
.block-row { display: flex; align-items: center; gap: 0.75rem; padding: 0.5rem; background: #0f172a; border-radius: 0.5rem; }
|
||||||
.block-time { font-size: 0.8rem; color: #64748b; font-variant-numeric: tabular-nums; }
|
.block-time { font-size: 0.8rem; color: #64748b; font-variant-numeric: tabular-nums; }
|
||||||
|
|||||||
Reference in New Issue
Block a user