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,
|
ScheduleTemplateOut,
|
||||||
ScheduleTemplateUpdate,
|
ScheduleTemplateUpdate,
|
||||||
ScheduleBlockCreate,
|
ScheduleBlockCreate,
|
||||||
|
ScheduleBlockUpdate,
|
||||||
ScheduleBlockOut,
|
ScheduleBlockOut,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -144,6 +145,34 @@ async def add_block(
|
|||||||
return 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)
|
@router.delete("/{template_id}/blocks/{block_id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||||
async def delete_block(
|
async def delete_block(
|
||||||
template_id: int,
|
template_id: int,
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ class ScheduleBlockCreate(BaseModel):
|
|||||||
order_index: int = 0
|
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):
|
class ScheduleBlockOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
subject_id: int | None
|
subject_id: int | None
|
||||||
|
|||||||
@@ -154,11 +154,32 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="block-list">
|
<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">
|
||||||
<span class="block-time">{{ block.time_start }} – {{ block.time_end }}</span>
|
<!-- Edit mode -->
|
||||||
<span class="block-label">{{ block.label || subjectName(block.subject_id) || 'Unnamed' }}</span>
|
<form
|
||||||
<button class="btn-sm btn-danger" @click="deleteBlock(template.id, block.id)">✕</button>
|
v-if="editingBlock && editingBlock.id === block.id"
|
||||||
</div>
|
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 v-if="template.blocks.length === 0" class="empty-small">No blocks yet.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -275,6 +296,7 @@ const showCreateForm = ref(false)
|
|||||||
const editingTemplate = ref(null)
|
const editingTemplate = ref(null)
|
||||||
const newTemplate = ref({ name: '', child_id: null, is_default: false })
|
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 newBlock = ref({ subject_id: null, time_start: '', time_end: '', label: '', order_index: 0 })
|
||||||
|
const editingBlock = ref(null)
|
||||||
|
|
||||||
function childName(id) {
|
function childName(id) {
|
||||||
return childrenStore.children.find((c) => c.id === id)?.name || 'Unknown'
|
return childrenStore.children.find((c) => c.id === id)?.name || 'Unknown'
|
||||||
@@ -313,6 +335,23 @@ async function addBlock(templateId) {
|
|||||||
await loadTemplates()
|
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) {
|
async function deleteBlock(templateId, blockId) {
|
||||||
await api.delete(`/api/schedules/${templateId}/blocks/${blockId}`)
|
await api.delete(`/api/schedules/${templateId}/blocks/${blockId}`)
|
||||||
await loadTemplates()
|
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-time { font-size: 0.8rem; color: #64748b; font-variant-numeric: tabular-nums; }
|
||||||
.block-label { flex: 1; font-size: 0.9rem; }
|
.block-label { flex: 1; font-size: 0.9rem; }
|
||||||
|
|
||||||
.add-block-form {
|
.add-block-form,
|
||||||
|
.edit-block-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
@@ -452,7 +492,9 @@ h2 { font-size: 1.1rem; color: #94a3b8; text-transform: uppercase; letter-spacin
|
|||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
}
|
}
|
||||||
.add-block-form select,
|
.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;
|
padding: 0.4rem 0.6rem;
|
||||||
background: #1e293b;
|
background: #1e293b;
|
||||||
border: 1px solid #334155;
|
border: 1px solid #334155;
|
||||||
@@ -460,7 +502,9 @@ h2 { font-size: 1.1rem; color: #94a3b8; text-transform: uppercase; letter-spacin
|
|||||||
color: #f1f5f9;
|
color: #f1f5f9;
|
||||||
font-size: 0.85rem;
|
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; }
|
.empty-small { color: #64748b; font-size: 0.9rem; padding: 1rem 0; }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user