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 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 10:15:19 -08:00
parent 3e7ff2a50b
commit a019e2dda5
3 changed files with 90 additions and 8 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -154,11 +154,32 @@
</div>
<div class="block-list">
<div v-for="block in template.blocks" :key="block.id" class="block-row">
<template v-for="block in template.blocks" :key="block.id">
<!-- Edit mode -->
<form
v-if="editingBlock && editingBlock.id === block.id"
class="edit-block-form"
@submit.prevent="saveBlock(template.id)"
>
<select v-model="editingBlock.subject_id">
<option :value="null">No subject</option>
<option v-for="s in subjects" :key="s.id" :value="s.id">{{ s.icon }} {{ s.name }}</option>
</select>
<input v-model="editingBlock.time_start" type="time" required />
<span>to</span>
<input v-model="editingBlock.time_end" type="time" required />
<input v-model="editingBlock.label" placeholder="Label (optional)" />
<button type="submit" class="btn-sm btn-primary">Save</button>
<button type="button" class="btn-sm" @click="editingBlock = null">Cancel</button>
</form>
<!-- Display mode -->
<div v-else class="block-row">
<span class="block-time">{{ block.time_start }} {{ block.time_end }}</span>
<span class="block-label">{{ block.label || subjectName(block.subject_id) || 'Unnamed' }}</span>
<button class="btn-sm" @click="startEditBlock(block)">Edit</button>
<button class="btn-sm btn-danger" @click="deleteBlock(template.id, block.id)"></button>
</div>
</template>
<div v-if="template.blocks.length === 0" class="empty-small">No blocks yet.</div>
</div>
@@ -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; }