Add time-based day progress bar to dashboards

Replaces block-count progress with a wall-clock progress bar driven by
configurable day start/end hours on each schedule template.

- ScheduleTemplate: add day_start_time / day_end_time (TIME, nullable)
- Startup migration: idempotent ALTER TABLE for existing DBs
- Dashboard snapshot: includes day_start_time / day_end_time from template
- Admin → Schedules: time pickers in block editor to set day hours
- Dashboard view: time-based progress bar with start/current/end labels
- TV view: full-width day progress strip between header and main content

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 10:08:07 -08:00
parent 462205cdc1
commit 3e7ff2a50b
10 changed files with 213 additions and 38 deletions

View File

@@ -7,6 +7,16 @@
<div class="tv-date">{{ dateDisplay }}</div>
</header>
<!-- Day progress bar full width, always visible when day hours are set -->
<div class="tv-day-progress" v-if="scheduleStore.dayStartTime && scheduleStore.dayEndTime">
<div class="tv-day-progress-meta">
<span class="tv-day-start">{{ formatDayTime(scheduleStore.dayStartTime) }}</span>
<span class="tv-day-pct">{{ dayProgressPercent }}% through the day</span>
<span class="tv-day-end">{{ formatDayTime(scheduleStore.dayEndTime) }}</span>
</div>
<ProgressBar :percent="dayProgressPercent" />
</div>
<!-- No session state -->
<div v-if="!scheduleStore.session" class="tv-idle">
<div class="tv-idle-icon">🌟</div>
@@ -36,17 +46,6 @@
</div>
<div class="tv-sidebar">
<!-- Progress -->
<div class="tv-progress-section">
<div class="tv-progress-label">
Day Progress {{ scheduleStore.progressPercent }}%
</div>
<ProgressBar :percent="scheduleStore.progressPercent" />
<div class="tv-block-count">
{{ scheduleStore.completedBlockIds.length }} of {{ scheduleStore.blocks.length }} blocks
</div>
</div>
<!-- Schedule list -->
<div class="tv-schedule-list">
<ScheduleBlock
@@ -92,6 +91,29 @@ const dateDisplay = computed(() =>
now.value.toLocaleDateString([], { weekday: 'long', month: 'long', day: 'numeric' })
)
// Day progress helpers
function timeStrToMinutes(str) {
if (!str) return null
const [h, m] = str.split(':').map(Number)
return h * 60 + m
}
function formatDayTime(str) {
if (!str) return ''
const [h, m] = str.split(':').map(Number)
const period = h >= 12 ? 'PM' : 'AM'
const hour = h % 12 || 12
return `${hour}:${String(m).padStart(2, '0')} ${period}`
}
const dayProgressPercent = computed(() => {
const start = timeStrToMinutes(scheduleStore.dayStartTime)
const end = timeStrToMinutes(scheduleStore.dayEndTime)
if (start === null || end === null || end <= start) return 0
const nowMin = now.value.getHours() * 60 + now.value.getMinutes()
return Math.max(0, Math.min(100, Math.round((nowMin - start) / (end - start) * 100)))
})
// Subject display helpers
const currentSubjectColor = computed(() => {
const block = scheduleStore.currentBlock
@@ -193,33 +215,39 @@ onMounted(async () => {
max-width: 600px;
}
.tv-day-progress {
background: #1e293b;
border-radius: 1rem;
padding: 1rem 1.5rem 1.25rem;
}
.tv-day-progress-meta {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.6rem;
}
.tv-day-start,
.tv-day-end {
font-size: 1rem;
color: #64748b;
font-variant-numeric: tabular-nums;
}
.tv-day-pct {
font-size: 1.1rem;
font-weight: 600;
color: #94a3b8;
letter-spacing: 0.02em;
}
.tv-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.tv-progress-section {
background: #1e293b;
border-radius: 1rem;
padding: 1.25rem;
}
.tv-progress-label {
font-size: 0.9rem;
color: #64748b;
margin-bottom: 0.5rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.tv-block-count {
font-size: 0.85rem;
color: #475569;
margin-top: 0.5rem;
text-align: right;
}
.tv-schedule-list {
overflow-y: auto;
display: flex;