TICKET-005: Frontend daily view — milestone M1 complete
- Dashboard: progress bar vs target, entries grouped by meal slot, edit/delete inline, date navigation (UTC-safe shiftDate helper) - Add Food manual creation form, search-and-log flow with live preview - Minimal target form; loading/error/empty states throughout - Stores (current date, log, summary) with summary refresh on mutation - All HTTP via lib/api.js; formatting via lib/format.js; runes only - Full suites green (backend 104 passed, frontend vitest + build)
This commit is contained in:
@@ -1,34 +1,111 @@
|
||||
<script>
|
||||
// Dashboard — Daily view: progress bar + log entries grouped by meal_slot (spec §4.6)
|
||||
import { api } from '../lib/api.js'
|
||||
// Dashboard — Daily view: progress bar + log entries grouped by meal_slot (spec §4.6).
|
||||
// Every async view handles loading / error / empty states (spec §8.2 rule 4).
|
||||
|
||||
import { currentDate, dayData, refreshDayData, appView } from '../lib/stores.svelte.js'
|
||||
import { formatDate } from '../lib/format.js'
|
||||
import ProgressBar from './ProgressBar.svelte'
|
||||
import LogEntry from './LogEntry.svelte'
|
||||
|
||||
let { date } = $props()
|
||||
|
||||
let entries = $state(null) // null = loading
|
||||
let error = $state(null)
|
||||
|
||||
// Re-fetch whenever date changes
|
||||
$effect(() => {
|
||||
entries = null
|
||||
error = null
|
||||
api.getLog(date)
|
||||
.then((data) => (entries = data))
|
||||
.catch((e) => (error = e.message))
|
||||
// reading date triggers re-run
|
||||
void date
|
||||
refreshDayData()
|
||||
})
|
||||
|
||||
// Group entries by meal_slot (null → "Other")
|
||||
let groups = $derived.by(() => {
|
||||
const log = dayData.log
|
||||
const map = new Map()
|
||||
const order = ['breakfast', 'lunch', 'dinner', 'snack']
|
||||
for (const slot of order) map.set(slot, [])
|
||||
const unslotted = []
|
||||
|
||||
for (const entry of log) {
|
||||
if (entry.meal_slot && map.has(entry.meal_slot)) {
|
||||
map.get(entry.meal_slot).push(entry)
|
||||
} else {
|
||||
unslotted.push(entry)
|
||||
}
|
||||
}
|
||||
|
||||
const result = []
|
||||
for (const slot of order) {
|
||||
const entries = map.get(slot)
|
||||
if (entries?.length) result.push({ slot, entries })
|
||||
}
|
||||
if (unslotted.length) result.push({ slot: null, entries: unslotted })
|
||||
return result
|
||||
})
|
||||
</script>
|
||||
|
||||
<h2>{formatDate(date)}</h2>
|
||||
<section class="dashboard">
|
||||
<h2>{formatDate(date)}</h2>
|
||||
|
||||
{#if error}
|
||||
<p role="alert">Failed to load log: {error}</p>
|
||||
{:else if entries === null}
|
||||
<p>Loading…</p>
|
||||
{:else if entries.length === 0}
|
||||
<p>Nothing logged yet today.</p>
|
||||
{:else}
|
||||
<ul>
|
||||
{#each entries as entry (entry.id)}
|
||||
<li>Food #{entry.food_id} × {entry.quantity}</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
{#if dayData.loading}
|
||||
<p class="status">Loading…</p>
|
||||
{:else if dayData.error}
|
||||
<p class="status err" role="alert">Failed to load: {dayData.error}</p>
|
||||
{:else}
|
||||
<ProgressBar
|
||||
totals={dayData.summary?.totals}
|
||||
target={dayData.summary?.target}
|
||||
/>
|
||||
|
||||
{#if dayData.log.length === 0}
|
||||
<p class="status empty">Nothing logged yet. Tap "Add Food" to get started.</p>
|
||||
{:else}
|
||||
{#each groups as group (group.slot ?? 'other')}
|
||||
<div class="meal-group">
|
||||
<h3 class="slot-header">{group.slot || 'Other'}</h3>
|
||||
<ul class="entry-list">
|
||||
{#each group.entries as entry (entry.id)}
|
||||
<LogEntry {entry} />
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.dashboard { }
|
||||
|
||||
h2 {
|
||||
margin: 0 0 0.75rem;
|
||||
font-size: 1.15rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted, #6b7280);
|
||||
}
|
||||
.status.err { color: #dc2626; }
|
||||
.status.empty {
|
||||
margin-top: 1.5rem;
|
||||
text-align: center;
|
||||
padding: 2rem 1rem;
|
||||
border: 2px dashed var(--border, #e5e7eb);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.meal-group {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.slot-header {
|
||||
font-size: 0.85rem;
|
||||
text-transform: capitalize;
|
||||
color: var(--text-muted, #6b7280);
|
||||
margin: 0 0 0.25rem;
|
||||
padding-bottom: 0.2rem;
|
||||
border-bottom: 1px solid var(--border, #e5e7eb);
|
||||
}
|
||||
.entry-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user