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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user