From a019e2dda5a63fd2f3201df93afe82ef8dcdae5f Mon Sep 17 00:00:00 2001 From: derekc Date: Sat, 28 Feb 2026 10:15:19 -0800 Subject: [PATCH] Add inline block editing to schedule templates - Backend: PATCH /api/schedules/{template_id}/blocks/{block_id} endpoint - Frontend: Edit button on each block row expands an inline form pre-filled with current subject, times, and label; saves via PATCH Co-Authored-By: Claude Sonnet 4.6 --- backend/app/routers/schedules.py | 29 +++++++++++++++ backend/app/schemas/schedule.py | 9 +++++ frontend/src/views/AdminView.vue | 60 +++++++++++++++++++++++++++----- 3 files changed, 90 insertions(+), 8 deletions(-) diff --git a/backend/app/routers/schedules.py b/backend/app/routers/schedules.py index 3f6e31b..e1f0cfc 100644 --- a/backend/app/routers/schedules.py +++ b/backend/app/routers/schedules.py @@ -11,6 +11,7 @@ from app.schemas.schedule import ( ScheduleTemplateOut, ScheduleTemplateUpdate, ScheduleBlockCreate, + ScheduleBlockUpdate, ScheduleBlockOut, ) @@ -144,6 +145,34 @@ async def add_block( return block +@router.patch("/{template_id}/blocks/{block_id}", response_model=ScheduleBlockOut) +async def update_block( + template_id: int, + block_id: int, + body: ScheduleBlockUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + result = await db.execute( + select(ScheduleBlock) + .join(ScheduleTemplate) + .where( + ScheduleBlock.id == block_id, + ScheduleBlock.template_id == template_id, + ScheduleTemplate.user_id == current_user.id, + ) + ) + block = result.scalar_one_or_none() + if not block: + raise HTTPException(status_code=404, detail="Block not found") + + for field, value in body.model_dump(exclude_unset=True).items(): + setattr(block, field, value) + await db.commit() + await db.refresh(block) + return block + + @router.delete("/{template_id}/blocks/{block_id}", status_code=status.HTTP_204_NO_CONTENT) async def delete_block( template_id: int, diff --git a/backend/app/schemas/schedule.py b/backend/app/schemas/schedule.py index 6fafef5..78c4d2a 100644 --- a/backend/app/schemas/schedule.py +++ b/backend/app/schemas/schedule.py @@ -11,6 +11,15 @@ class ScheduleBlockCreate(BaseModel): order_index: int = 0 +class ScheduleBlockUpdate(BaseModel): + subject_id: int | None = None + time_start: time | None = None + time_end: time | None = None + label: str | None = None + notes: str | None = None + order_index: int | None = None + + class ScheduleBlockOut(BaseModel): id: int subject_id: int | None diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index 8919345..d969061 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -154,11 +154,32 @@
-
- {{ block.time_start }} – {{ block.time_end }} - {{ block.label || subjectName(block.subject_id) || 'Unnamed' }} - -
+
No blocks yet.
@@ -275,6 +296,7 @@ const showCreateForm = ref(false) const editingTemplate = ref(null) const newTemplate = ref({ name: '', child_id: null, is_default: false }) const newBlock = ref({ subject_id: null, time_start: '', time_end: '', label: '', order_index: 0 }) +const editingBlock = ref(null) function childName(id) { return childrenStore.children.find((c) => c.id === id)?.name || 'Unknown' @@ -313,6 +335,23 @@ async function addBlock(templateId) { await loadTemplates() } +function startEditBlock(block) { + editingBlock.value = { + id: block.id, + subject_id: block.subject_id, + time_start: block.time_start ? block.time_start.slice(0, 5) : '', + time_end: block.time_end ? block.time_end.slice(0, 5) : '', + label: block.label || '', + } +} + +async function saveBlock(templateId) { + const { id, ...payload } = editingBlock.value + await api.patch(`/api/schedules/${templateId}/blocks/${id}`, payload) + editingBlock.value = null + await loadTemplates() +} + async function deleteBlock(templateId, blockId) { await api.delete(`/api/schedules/${templateId}/blocks/${blockId}`) await loadTemplates() @@ -442,7 +481,8 @@ h2 { font-size: 1.1rem; color: #94a3b8; text-transform: uppercase; letter-spacin .block-time { font-size: 0.8rem; color: #64748b; font-variant-numeric: tabular-nums; } .block-label { flex: 1; font-size: 0.9rem; } -.add-block-form { +.add-block-form, +.edit-block-form { display: flex; align-items: center; gap: 0.5rem; @@ -452,7 +492,9 @@ h2 { font-size: 1.1rem; color: #94a3b8; text-transform: uppercase; letter-spacin border-radius: 0.75rem; } .add-block-form select, -.add-block-form input { +.add-block-form input, +.edit-block-form select, +.edit-block-form input { padding: 0.4rem 0.6rem; background: #1e293b; border: 1px solid #334155; @@ -460,7 +502,9 @@ h2 { font-size: 1.1rem; color: #94a3b8; text-transform: uppercase; letter-spacin color: #f1f5f9; font-size: 0.85rem; } -.add-block-form span { color: #64748b; } +.add-block-form span, +.edit-block-form span { color: #64748b; } +.edit-block-form { border: 1px solid #4f46e5; } .empty-small { color: #64748b; font-size: 0.9rem; padding: 1rem 0; }