Add Morning Routine to Admin and TV greeting state
Adds a per-user Morning Routine item list that appears in the TV dashboard Activities panel during the "Good Morning" countdown (before the first block starts). - morning_routine_items table (auto-created on startup) - CRUD API at /api/morning-routine (auth-required) - Items included in the public DashboardSnapshot so TV gets them without auth - Morning Routine section in Admin page (same add/edit/delete UX as subject options) - TV Activities column shows routine items when no block is active, switches to subject options once a block starts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ export const useScheduleStore = defineStore('schedule', () => {
|
||||
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 currentBlock = computed(() =>
|
||||
session.value?.current_block_id
|
||||
@@ -40,6 +41,7 @@ export const useScheduleStore = defineStore('schedule', () => {
|
||||
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 || []
|
||||
// Restore elapsed time from server-computed value and seed the per-block cache
|
||||
const serverElapsed = snapshot.block_elapsed_seconds || 0
|
||||
if (snapshot.session?.current_block_id && serverElapsed > 0) {
|
||||
@@ -165,6 +167,7 @@ export const useScheduleStore = defineStore('schedule', () => {
|
||||
blockElapsedCache,
|
||||
dayStartTime,
|
||||
dayEndTime,
|
||||
morningRoutine,
|
||||
currentBlock,
|
||||
progressPercent,
|
||||
applySnapshot,
|
||||
|
||||
@@ -117,6 +117,37 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Morning Routine section -->
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Morning Routine</h2>
|
||||
</div>
|
||||
<div class="card">
|
||||
<p class="routine-hint">These items appear in the Activities panel on the TV during the "Good Morning" countdown before the first block starts.</p>
|
||||
<div class="option-list">
|
||||
<template v-for="item in morningRoutine" :key="item.id">
|
||||
<div v-if="editingRoutineItem && editingRoutineItem.id === item.id" class="option-edit-row">
|
||||
<input v-model="editingRoutineItem.text" class="option-input" @keyup.enter="saveRoutineItem" />
|
||||
<button class="btn-sm btn-primary" @click="saveRoutineItem">Save</button>
|
||||
<button class="btn-sm" @click="editingRoutineItem = null">Cancel</button>
|
||||
</div>
|
||||
<div v-else class="option-row">
|
||||
<span class="option-text">{{ item.text }}</span>
|
||||
<div class="item-actions">
|
||||
<button class="btn-sm" @click="startEditRoutineItem(item)">Edit</button>
|
||||
<button class="btn-sm btn-danger" @click="deleteRoutineItem(item.id)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="morningRoutine.length === 0" class="empty-small">No items yet.</div>
|
||||
</div>
|
||||
<form class="option-add-row" style="margin-top: 0.75rem" @submit.prevent="addRoutineItem">
|
||||
<input v-model="newRoutineText" placeholder="Add a morning routine item..." class="option-input" required />
|
||||
<button type="submit" class="btn-primary btn-sm">Add</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Schedules section -->
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
@@ -408,6 +439,42 @@ async function deleteOption(subjectId, optionId) {
|
||||
await loadSubjects()
|
||||
}
|
||||
|
||||
// Morning Routine
|
||||
const morningRoutine = ref([])
|
||||
const newRoutineText = ref('')
|
||||
const editingRoutineItem = ref(null)
|
||||
|
||||
async function loadMorningRoutine() {
|
||||
const res = await api.get('/api/morning-routine')
|
||||
morningRoutine.value = res.data
|
||||
}
|
||||
|
||||
async function addRoutineItem() {
|
||||
await api.post('/api/morning-routine', {
|
||||
text: newRoutineText.value,
|
||||
order_index: morningRoutine.value.length,
|
||||
})
|
||||
newRoutineText.value = ''
|
||||
await loadMorningRoutine()
|
||||
}
|
||||
|
||||
function startEditRoutineItem(item) {
|
||||
editingRoutineItem.value = { ...item }
|
||||
}
|
||||
|
||||
async function saveRoutineItem() {
|
||||
await api.patch(`/api/morning-routine/${editingRoutineItem.value.id}`, {
|
||||
text: editingRoutineItem.value.text,
|
||||
})
|
||||
editingRoutineItem.value = null
|
||||
await loadMorningRoutine()
|
||||
}
|
||||
|
||||
async function deleteRoutineItem(id) {
|
||||
await api.delete(`/api/morning-routine/${id}`)
|
||||
await loadMorningRoutine()
|
||||
}
|
||||
|
||||
// Schedules
|
||||
const templates = ref([])
|
||||
const showCreateForm = ref(false)
|
||||
@@ -510,7 +577,7 @@ async function saveDayHours(template, which, value) {
|
||||
|
||||
onMounted(async () => {
|
||||
await childrenStore.fetchChildren()
|
||||
await Promise.all([loadSubjects(), loadTemplates()])
|
||||
await Promise.all([loadSubjects(), loadTemplates(), loadMorningRoutine()])
|
||||
selectedTimezone.value = authStore.timezone
|
||||
})
|
||||
</script>
|
||||
@@ -703,6 +770,7 @@ h2 { font-size: 1.1rem; color: #94a3b8; text-transform: uppercase; letter-spacin
|
||||
|
||||
.option-add-row .option-input { background: #1e293b; }
|
||||
|
||||
.routine-hint { font-size: 0.82rem; color: #64748b; margin-bottom: 1rem; }
|
||||
.empty-small { color: #64748b; font-size: 0.9rem; padding: 1rem 0; }
|
||||
|
||||
.settings-card {
|
||||
|
||||
@@ -59,22 +59,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Center: subject options -->
|
||||
<div class="tv-options-col" :style="{ background: currentSubjectColor + '22', borderColor: currentSubjectColor }">
|
||||
<!-- Center: subject options or morning routine -->
|
||||
<div
|
||||
class="tv-options-col"
|
||||
:style="scheduleStore.currentBlock
|
||||
? { background: currentSubjectColor + '22', borderColor: currentSubjectColor }
|
||||
: { background: '#1e293b', borderColor: '#334155' }"
|
||||
>
|
||||
<div class="tv-options-title">Activities</div>
|
||||
<div
|
||||
v-if="currentSubjectOptions.length"
|
||||
class="tv-options-list"
|
||||
>
|
||||
<div
|
||||
v-for="opt in currentSubjectOptions"
|
||||
:key="opt.id"
|
||||
class="tv-option-item"
|
||||
>
|
||||
{{ opt.text }}
|
||||
<!-- Morning routine during greeting state -->
|
||||
<template v-if="!scheduleStore.currentBlock">
|
||||
<div v-if="scheduleStore.morningRoutine.length" class="tv-options-list">
|
||||
<div v-for="(item, i) in scheduleStore.morningRoutine" :key="i" class="tv-option-item">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tv-options-empty">No activities listed for this subject.</div>
|
||||
<div v-else class="tv-options-empty">No morning routine items added yet.</div>
|
||||
</template>
|
||||
<!-- Subject options during active block -->
|
||||
<template v-else>
|
||||
<div v-if="currentSubjectOptions.length" class="tv-options-list">
|
||||
<div v-for="opt in currentSubjectOptions" :key="opt.id" class="tv-option-item">
|
||||
{{ opt.text }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tv-options-empty">No activities listed for this subject.</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Right: schedule list -->
|
||||
|
||||
Reference in New Issue
Block a user