Add break time feature to schedule blocks
- Admin: per-block "Break Time" checkbox + duration (min) setting; new Break Activities section (global list, same pattern as Morning Routine) - Dashboard: break timer section appears on blocks with break enabled; Start/Pause/Resume/Reset controls work independently of the main timer - TV: left column switches to amber break badge + countdown during break; center column shows configurable Break Activities list - Backend: break_time_enabled/break_time_minutes columns on schedule_blocks (auto-migrated on startup); break_activity_items table + CRUD router; break timer events (break_start/pause/resume/reset) stored as TimerEvents and broadcast via WebSocket; break_activities included in dashboard snapshot and session_update broadcast Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,12 @@ export const useScheduleStore = defineStore('schedule', () => {
|
||||
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 breakActivities = ref([]) // list of text strings shown during break time
|
||||
// Break timer state (per-block break time at end of block)
|
||||
const isBreakMode = ref(false) // currently in break time
|
||||
const breakStartedAt = ref(null) // Date.now() ms when break counting started
|
||||
const breakElapsedOffset = ref(0) // break seconds already elapsed
|
||||
const breakElapsedCache = ref({}) // blockId → total break elapsed seconds
|
||||
|
||||
const currentBlock = computed(() =>
|
||||
session.value?.current_block_id
|
||||
@@ -42,6 +48,7 @@ export const useScheduleStore = defineStore('schedule', () => {
|
||||
dayStartTime.value = snapshot.day_start_time || null
|
||||
dayEndTime.value = snapshot.day_end_time || null
|
||||
morningRoutine.value = snapshot.morning_routine || []
|
||||
breakActivities.value = snapshot.break_activities || []
|
||||
// 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) {
|
||||
@@ -54,6 +61,11 @@ export const useScheduleStore = defineStore('schedule', () => {
|
||||
blockElapsedOffset.value = 0
|
||||
blockStartedAt.value = null
|
||||
}
|
||||
// Reset break state on snapshot (not persisted across page loads)
|
||||
isBreakMode.value = false
|
||||
breakStartedAt.value = null
|
||||
breakElapsedOffset.value = 0
|
||||
breakElapsedCache.value = {}
|
||||
}
|
||||
|
||||
function applyWsEvent(event) {
|
||||
@@ -76,6 +88,39 @@ export const useScheduleStore = defineStore('schedule', () => {
|
||||
blockElapsedCache.value = {}
|
||||
dayStartTime.value = null
|
||||
dayEndTime.value = null
|
||||
isBreakMode.value = false
|
||||
breakStartedAt.value = null
|
||||
breakElapsedOffset.value = 0
|
||||
breakElapsedCache.value = {}
|
||||
return
|
||||
}
|
||||
// Break timer events
|
||||
if (event.event === 'break_start') {
|
||||
const elapsed = event.break_elapsed_seconds ?? breakElapsedCache.value[event.block_id] ?? 0
|
||||
breakElapsedOffset.value = elapsed
|
||||
if (event.block_id) breakElapsedCache.value[event.block_id] = elapsed
|
||||
breakStartedAt.value = Date.now()
|
||||
isBreakMode.value = true
|
||||
}
|
||||
if (event.event === 'break_pause') {
|
||||
if (breakStartedAt.value) {
|
||||
breakElapsedOffset.value += Math.floor((Date.now() - breakStartedAt.value) / 1000)
|
||||
}
|
||||
if (event.block_id) breakElapsedCache.value[event.block_id] = breakElapsedOffset.value
|
||||
breakStartedAt.value = null
|
||||
isBreakMode.value = true
|
||||
}
|
||||
if (event.event === 'break_resume') {
|
||||
breakStartedAt.value = Date.now()
|
||||
isBreakMode.value = true
|
||||
}
|
||||
if (event.event === 'break_reset') {
|
||||
if (event.block_id) breakElapsedCache.value[event.block_id] = 0
|
||||
breakElapsedOffset.value = 0
|
||||
breakStartedAt.value = Date.now()
|
||||
isBreakMode.value = true
|
||||
}
|
||||
if (['break_start', 'break_pause', 'break_resume', 'break_reset'].includes(event.event)) {
|
||||
return
|
||||
}
|
||||
// Pause — accumulate elapsed, save to cache, stop counting
|
||||
@@ -101,6 +146,12 @@ export const useScheduleStore = defineStore('schedule', () => {
|
||||
}
|
||||
blockStartedAt.value = Date.now()
|
||||
isPaused.value = false
|
||||
// Switching to a new block clears break mode
|
||||
if (event.block_id !== event.current_block_id || !isBreakMode.value) {
|
||||
isBreakMode.value = false
|
||||
breakStartedAt.value = null
|
||||
breakElapsedOffset.value = breakElapsedCache.value[event.block_id] ?? 0
|
||||
}
|
||||
}
|
||||
// Reset — clear elapsed to 0 and start counting immediately
|
||||
if (event.event === 'reset') {
|
||||
@@ -121,6 +172,10 @@ export const useScheduleStore = defineStore('schedule', () => {
|
||||
if (event.block_id) blockElapsedCache.value[event.block_id] = elapsed
|
||||
blockStartedAt.value = null
|
||||
isPaused.value = true
|
||||
// Switching blocks clears break mode
|
||||
isBreakMode.value = false
|
||||
breakStartedAt.value = null
|
||||
breakElapsedOffset.value = breakElapsedCache.value[event.block_id] ?? 0
|
||||
}
|
||||
// Resume — continue from where we left off
|
||||
if (event.event === 'resume') {
|
||||
@@ -212,6 +267,41 @@ export const useScheduleStore = defineStore('schedule', () => {
|
||||
sendTimerAction(sessionId, 'start', session.value.current_block_id)
|
||||
}
|
||||
|
||||
// Break timer actions
|
||||
function startBreak(sessionId) {
|
||||
if (!session.value?.current_block_id) return
|
||||
const blockId = session.value.current_block_id
|
||||
isBreakMode.value = true
|
||||
breakElapsedOffset.value = breakElapsedCache.value[blockId] ?? 0
|
||||
breakStartedAt.value = Date.now()
|
||||
sendTimerAction(sessionId, 'break_start', blockId)
|
||||
}
|
||||
|
||||
function pauseBreak(sessionId) {
|
||||
if (!session.value?.current_block_id) return
|
||||
if (breakStartedAt.value) {
|
||||
breakElapsedOffset.value += Math.floor((Date.now() - breakStartedAt.value) / 1000)
|
||||
}
|
||||
breakStartedAt.value = null
|
||||
sendTimerAction(sessionId, 'break_pause', session.value.current_block_id)
|
||||
}
|
||||
|
||||
function resumeBreak(sessionId) {
|
||||
if (!session.value?.current_block_id) return
|
||||
breakStartedAt.value = Date.now()
|
||||
sendTimerAction(sessionId, 'break_resume', session.value.current_block_id)
|
||||
}
|
||||
|
||||
function resetBreak(sessionId) {
|
||||
if (!session.value?.current_block_id) return
|
||||
const blockId = session.value.current_block_id
|
||||
breakElapsedCache.value[blockId] = 0
|
||||
breakElapsedOffset.value = 0
|
||||
breakStartedAt.value = Date.now()
|
||||
isBreakMode.value = true
|
||||
sendTimerAction(sessionId, 'break_reset', blockId)
|
||||
}
|
||||
|
||||
// Reset the current block's timer to 0 and start counting immediately.
|
||||
function resetCurrentBlock(sessionId) {
|
||||
if (!session.value?.current_block_id) return
|
||||
@@ -235,6 +325,11 @@ export const useScheduleStore = defineStore('schedule', () => {
|
||||
dayStartTime,
|
||||
dayEndTime,
|
||||
morningRoutine,
|
||||
breakActivities,
|
||||
isBreakMode,
|
||||
breakStartedAt,
|
||||
breakElapsedOffset,
|
||||
breakElapsedCache,
|
||||
currentBlock,
|
||||
progressPercent,
|
||||
applySnapshot,
|
||||
@@ -246,5 +341,9 @@ export const useScheduleStore = defineStore('schedule', () => {
|
||||
selectBlock,
|
||||
startCurrentBlock,
|
||||
resetCurrentBlock,
|
||||
startBreak,
|
||||
pauseBreak,
|
||||
resumeBreak,
|
||||
resetBreak,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -148,6 +148,37 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Break Activities section -->
|
||||
<section class="section">
|
||||
<div class="section-header">
|
||||
<h2>Break Activities</h2>
|
||||
</div>
|
||||
<div class="card">
|
||||
<p class="routine-hint">These items appear in the Activities panel on the TV during break time.</p>
|
||||
<div class="option-list">
|
||||
<template v-for="item in breakActivities" :key="item.id">
|
||||
<div v-if="editingBreakItem && editingBreakItem.id === item.id" class="option-edit-row">
|
||||
<input v-model="editingBreakItem.text" class="option-input" @keyup.enter="saveBreakItem" />
|
||||
<button class="btn-sm btn-primary" @click="saveBreakItem">Save</button>
|
||||
<button class="btn-sm" @click="editingBreakItem = 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="startEditBreakItem(item)">Edit</button>
|
||||
<button class="btn-sm btn-danger" @click="deleteBreakItem(item.id)">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="breakActivities.length === 0" class="empty-small">No items yet.</div>
|
||||
</div>
|
||||
<form class="option-add-row" style="margin-top: 0.75rem" @submit.prevent="addBreakItem">
|
||||
<input v-model="newBreakText" placeholder="Add a break activity..." 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">
|
||||
@@ -238,6 +269,17 @@
|
||||
style="width:130px"
|
||||
/>
|
||||
<input v-model="editingBlock.label" placeholder="Label (optional)" />
|
||||
<label class="break-check-label">
|
||||
<input type="checkbox" v-model="editingBlock.break_time_enabled" />
|
||||
Break
|
||||
</label>
|
||||
<input
|
||||
v-if="editingBlock.break_time_enabled"
|
||||
v-model.number="editingBlock.break_time_minutes"
|
||||
type="number" min="1" max="120"
|
||||
placeholder="Break (min)"
|
||||
style="width:100px"
|
||||
/>
|
||||
<button type="submit" class="btn-sm btn-primary">Save</button>
|
||||
<button type="button" class="btn-sm" @click="editingBlock = null">Cancel</button>
|
||||
</form>
|
||||
@@ -248,6 +290,9 @@
|
||||
<span class="block-duration" :class="{ 'block-duration-custom': block.duration_minutes != null }">
|
||||
{{ blockDurationLabel(block) }}
|
||||
</span>
|
||||
<span v-if="block.break_time_enabled" class="break-badge">
|
||||
☕ {{ block.break_time_minutes ? `${block.break_time_minutes}min` : '' }} break
|
||||
</span>
|
||||
<button class="btn-sm" @click="startEditBlock(block)">Edit</button>
|
||||
<button class="btn-sm btn-danger" @click="deleteBlock(template.id, block.id)">✕</button>
|
||||
</div>
|
||||
@@ -271,6 +316,17 @@
|
||||
style="width:130px"
|
||||
/>
|
||||
<input v-model="newBlock.label" placeholder="Label (optional)" />
|
||||
<label class="break-check-label">
|
||||
<input type="checkbox" v-model="newBlock.break_time_enabled" />
|
||||
Break
|
||||
</label>
|
||||
<input
|
||||
v-if="newBlock.break_time_enabled"
|
||||
v-model.number="newBlock.break_time_minutes"
|
||||
type="number" min="1" max="120"
|
||||
placeholder="Break (min)"
|
||||
style="width:100px"
|
||||
/>
|
||||
<button type="submit" class="btn-primary btn-sm">Add Block</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -475,12 +531,48 @@ async function deleteRoutineItem(id) {
|
||||
await loadMorningRoutine()
|
||||
}
|
||||
|
||||
// Break Activities
|
||||
const breakActivities = ref([])
|
||||
const newBreakText = ref('')
|
||||
const editingBreakItem = ref(null)
|
||||
|
||||
async function loadBreakActivities() {
|
||||
const res = await api.get('/api/break-activities')
|
||||
breakActivities.value = res.data
|
||||
}
|
||||
|
||||
async function addBreakItem() {
|
||||
await api.post('/api/break-activities', {
|
||||
text: newBreakText.value,
|
||||
order_index: breakActivities.value.length,
|
||||
})
|
||||
newBreakText.value = ''
|
||||
await loadBreakActivities()
|
||||
}
|
||||
|
||||
function startEditBreakItem(item) {
|
||||
editingBreakItem.value = { ...item }
|
||||
}
|
||||
|
||||
async function saveBreakItem() {
|
||||
await api.patch(`/api/break-activities/${editingBreakItem.value.id}`, {
|
||||
text: editingBreakItem.value.text,
|
||||
})
|
||||
editingBreakItem.value = null
|
||||
await loadBreakActivities()
|
||||
}
|
||||
|
||||
async function deleteBreakItem(id) {
|
||||
await api.delete(`/api/break-activities/${id}`)
|
||||
await loadBreakActivities()
|
||||
}
|
||||
|
||||
// Schedules
|
||||
const templates = ref([])
|
||||
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: '', duration_minutes: null, label: '', order_index: 0 })
|
||||
const newBlock = ref({ subject_id: null, time_start: '', time_end: '', duration_minutes: null, label: '', order_index: 0, break_time_enabled: false, break_time_minutes: null })
|
||||
const editingBlock = ref(null)
|
||||
|
||||
function childName(id) {
|
||||
@@ -540,7 +632,7 @@ async function addBlock(templateId) {
|
||||
order_index: templates.value.find((t) => t.id === templateId)?.blocks.length || 0,
|
||||
}
|
||||
await api.post(`/api/schedules/${templateId}/blocks`, payload)
|
||||
newBlock.value = { subject_id: null, time_start: '', time_end: '', duration_minutes: null, label: '', order_index: 0 }
|
||||
newBlock.value = { subject_id: null, time_start: '', time_end: '', duration_minutes: null, label: '', order_index: 0, break_time_enabled: false, break_time_minutes: null }
|
||||
await loadTemplates()
|
||||
}
|
||||
|
||||
@@ -552,6 +644,8 @@ function startEditBlock(block) {
|
||||
time_end: block.time_end ? block.time_end.slice(0, 5) : '',
|
||||
duration_minutes: block.duration_minutes ?? null,
|
||||
label: block.label || '',
|
||||
break_time_enabled: block.break_time_enabled || false,
|
||||
break_time_minutes: block.break_time_minutes ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -577,7 +671,7 @@ async function saveDayHours(template, which, value) {
|
||||
|
||||
onMounted(async () => {
|
||||
await childrenStore.fetchChildren()
|
||||
await Promise.all([loadSubjects(), loadTemplates(), loadMorningRoutine()])
|
||||
await Promise.all([loadSubjects(), loadTemplates(), loadMorningRoutine(), loadBreakActivities()])
|
||||
selectedTimezone.value = authStore.timezone
|
||||
})
|
||||
</script>
|
||||
@@ -824,4 +918,25 @@ h2 { font-size: 1.1rem; color: #94a3b8; text-transform: uppercase; letter-spacin
|
||||
.btn-sm.btn-danger { border-color: #7f1d1d; color: #fca5a5; }
|
||||
.btn-sm.btn-danger:hover { background: #7f1d1d; }
|
||||
.btn-primary.btn-sm { padding: 0.4rem 0.8rem; font-size: 0.85rem; }
|
||||
|
||||
.break-badge {
|
||||
font-size: 0.72rem;
|
||||
background: #451a03;
|
||||
color: #fdba74;
|
||||
border: 1px solid #92400e;
|
||||
padding: 0.15rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.break-check-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.85rem;
|
||||
color: #94a3b8;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.break-check-label input[type="checkbox"] { cursor: pointer; }
|
||||
</style>
|
||||
|
||||
@@ -72,6 +72,52 @@
|
||||
:block-elapsed-offset="scheduleStore.blockElapsedOffset"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Break Time section -->
|
||||
<div
|
||||
v-if="scheduleStore.currentBlock?.break_time_enabled"
|
||||
class="break-section"
|
||||
:class="{ 'break-active': scheduleStore.isBreakMode }"
|
||||
>
|
||||
<div class="break-header">
|
||||
<span class="break-icon">☕</span>
|
||||
<span class="break-title">Break Time</span>
|
||||
<span class="break-duration-badge">{{ scheduleStore.currentBlock.break_time_minutes }} min</span>
|
||||
</div>
|
||||
<div v-if="scheduleStore.isBreakMode" class="break-timer-display">
|
||||
<TimerDisplay
|
||||
compact
|
||||
:block="breakBlock"
|
||||
:session="scheduleStore.session"
|
||||
:is-paused="!scheduleStore.breakStartedAt"
|
||||
:block-started-at="scheduleStore.breakStartedAt"
|
||||
:block-elapsed-offset="scheduleStore.breakElapsedOffset"
|
||||
/>
|
||||
</div>
|
||||
<div class="break-actions">
|
||||
<button
|
||||
class="btn-sm btn-break"
|
||||
v-if="!scheduleStore.isBreakMode"
|
||||
@click="scheduleStore.startBreak(scheduleStore.session.id)"
|
||||
>Start Break</button>
|
||||
<button
|
||||
class="btn-sm btn-break"
|
||||
v-if="scheduleStore.isBreakMode && scheduleStore.breakStartedAt"
|
||||
@click="scheduleStore.pauseBreak(scheduleStore.session.id)"
|
||||
>Pause</button>
|
||||
<button
|
||||
class="btn-sm btn-break"
|
||||
v-if="scheduleStore.isBreakMode && !scheduleStore.breakStartedAt && scheduleStore.breakElapsedOffset > 0"
|
||||
@click="scheduleStore.resumeBreak(scheduleStore.session.id)"
|
||||
>Resume</button>
|
||||
<button
|
||||
class="btn-sm"
|
||||
v-if="scheduleStore.isBreakMode"
|
||||
@click="scheduleStore.resetBreak(scheduleStore.session.id)"
|
||||
>Reset</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="session-actions">
|
||||
<div class="session-actions-left">
|
||||
<button
|
||||
@@ -161,6 +207,13 @@ import TimerDisplay from '@/components/TimerDisplay.vue'
|
||||
const childrenStore = useChildrenStore()
|
||||
const scheduleStore = useScheduleStore()
|
||||
const activeChild = computed(() => childrenStore.activeChild)
|
||||
|
||||
// Virtual block for break timer (same block but with break duration)
|
||||
const breakBlock = computed(() => {
|
||||
const block = scheduleStore.currentBlock
|
||||
if (!block?.break_time_enabled) return null
|
||||
return { ...block, duration_minutes: block.break_time_minutes }
|
||||
})
|
||||
const showStartDialog = ref(false)
|
||||
const selectedTemplate = ref(null)
|
||||
const templates = ref([])
|
||||
@@ -333,6 +386,34 @@ h1 { font-size: 1.75rem; font-weight: 700; }
|
||||
.btn-sm.btn-start { border-color: #4f46e5; color: #818cf8; }
|
||||
.btn-sm.btn-start:hover { background: #4f46e5; color: #fff; }
|
||||
|
||||
.break-section {
|
||||
margin: 0.75rem 0;
|
||||
background: #1c1207;
|
||||
border: 1px solid #78350f;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
.break-section.break-active { border-color: #f59e0b; background: #1c1a07; }
|
||||
.break-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.break-icon { font-size: 1rem; }
|
||||
.break-title { font-size: 0.8rem; font-weight: 600; color: #fbbf24; text-transform: uppercase; letter-spacing: 0.06em; flex: 1; }
|
||||
.break-duration-badge {
|
||||
font-size: 0.75rem;
|
||||
background: #78350f;
|
||||
color: #fde68a;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.break-timer-display { display: flex; justify-content: flex-start; margin-bottom: 0.5rem; }
|
||||
.break-actions { display: flex; gap: 0.4rem; flex-wrap: wrap; }
|
||||
.btn-break { border-color: #92400e !important; color: #fbbf24 !important; }
|
||||
.btn-break:hover { background: #78350f !important; }
|
||||
|
||||
.no-session { text-align: center; padding: 1.5rem 0; color: #64748b; }
|
||||
.no-session p { margin-bottom: 1rem; }
|
||||
|
||||
|
||||
@@ -44,31 +44,59 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="tv-timer-col" v-else>
|
||||
<div class="tv-subject-badge" :style="{ background: currentSubjectColor }">
|
||||
{{ currentSubjectIcon }} {{ currentSubjectName }}
|
||||
</div>
|
||||
<TimerDisplay
|
||||
:block="scheduleStore.currentBlock"
|
||||
:session="scheduleStore.session"
|
||||
:is-paused="scheduleStore.isPaused"
|
||||
:block-started-at="scheduleStore.blockStartedAt"
|
||||
:block-elapsed-offset="scheduleStore.blockElapsedOffset"
|
||||
/>
|
||||
<div class="tv-block-notes" v-if="scheduleStore.currentBlock.notes">
|
||||
{{ scheduleStore.currentBlock.notes }}
|
||||
</div>
|
||||
<!-- Break mode badge -->
|
||||
<template v-if="scheduleStore.isBreakMode">
|
||||
<div class="tv-subject-badge tv-break-badge">
|
||||
☕ Break Time
|
||||
</div>
|
||||
<TimerDisplay
|
||||
:block="tvBreakBlock"
|
||||
:session="scheduleStore.session"
|
||||
:is-paused="!scheduleStore.breakStartedAt"
|
||||
:block-started-at="scheduleStore.breakStartedAt"
|
||||
:block-elapsed-offset="scheduleStore.breakElapsedOffset"
|
||||
/>
|
||||
</template>
|
||||
<!-- Normal subject timer -->
|
||||
<template v-else>
|
||||
<div class="tv-subject-badge" :style="{ background: currentSubjectColor }">
|
||||
{{ currentSubjectIcon }} {{ currentSubjectName }}
|
||||
</div>
|
||||
<TimerDisplay
|
||||
:block="scheduleStore.currentBlock"
|
||||
:session="scheduleStore.session"
|
||||
:is-paused="scheduleStore.isPaused"
|
||||
:block-started-at="scheduleStore.blockStartedAt"
|
||||
:block-elapsed-offset="scheduleStore.blockElapsedOffset"
|
||||
/>
|
||||
<div class="tv-block-notes" v-if="scheduleStore.currentBlock.notes">
|
||||
{{ scheduleStore.currentBlock.notes }}
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Center: subject options or morning routine -->
|
||||
<!-- Center: subject options / break message / morning routine -->
|
||||
<div
|
||||
class="tv-options-col"
|
||||
:style="scheduleStore.currentBlock
|
||||
? { background: currentSubjectColor + '22', borderColor: currentSubjectColor }
|
||||
: { background: '#1e293b', borderColor: '#334155' }"
|
||||
:style="scheduleStore.isBreakMode
|
||||
? { background: '#451a0322', borderColor: '#f59e0b' }
|
||||
: scheduleStore.currentBlock
|
||||
? { background: currentSubjectColor + '22', borderColor: currentSubjectColor }
|
||||
: { background: '#1e293b', borderColor: '#334155' }"
|
||||
>
|
||||
<div class="tv-options-title">Activities</div>
|
||||
<!-- Break time panel -->
|
||||
<template v-if="scheduleStore.isBreakMode">
|
||||
<div class="tv-options-title" style="color: #f59e0b;">Break Activities</div>
|
||||
<div v-if="scheduleStore.breakActivities.length" class="tv-options-list">
|
||||
<div v-for="(item, i) in scheduleStore.breakActivities" :key="i" class="tv-option-item">
|
||||
{{ item }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tv-options-empty">No break activities added yet.</div>
|
||||
</template>
|
||||
<!-- Morning routine during greeting state -->
|
||||
<template v-if="!scheduleStore.currentBlock">
|
||||
<template v-else-if="!scheduleStore.currentBlock">
|
||||
<div class="tv-options-title">Activities</div>
|
||||
<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 }}
|
||||
@@ -78,6 +106,7 @@
|
||||
</template>
|
||||
<!-- Subject options during active block -->
|
||||
<template v-else>
|
||||
<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 }}
|
||||
@@ -177,6 +206,20 @@ const firstBlockCountdown = computed(() => {
|
||||
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
|
||||
})
|
||||
|
||||
// Virtual block for break timer display on TV.
|
||||
// We don't re-check break_time_enabled here — if isBreakMode is true we always
|
||||
// want to show a timer. Fall back to the block's own duration when
|
||||
// break_time_minutes is not set.
|
||||
const tvBreakBlock = computed(() => {
|
||||
const block = scheduleStore.currentBlock
|
||||
if (!block) return null
|
||||
return {
|
||||
...block,
|
||||
duration_minutes: block.break_time_minutes ?? null,
|
||||
subject: { color: '#f59e0b' },
|
||||
}
|
||||
})
|
||||
|
||||
// Subject display helpers
|
||||
const currentSubjectColor = computed(() => {
|
||||
const block = scheduleStore.currentBlock
|
||||
@@ -302,6 +345,12 @@ onMounted(async () => {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tv-break-badge {
|
||||
background: #92400e;
|
||||
color: #fde68a;
|
||||
}
|
||||
|
||||
|
||||
.tv-greeting-col {
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
|
||||
Reference in New Issue
Block a user