update ui and other frontend updates

This commit is contained in:
JCEEE
2026-07-29 09:41:52 +01:00
parent 04b83925e0
commit 57ef30d3a7
79 changed files with 7323 additions and 429 deletions
+175
View File
@@ -0,0 +1,175 @@
<script lang="ts">
import { page } from '$app/state';
import { famStore } from '$lib/stores/fam.svelte';
import { onMount, onDestroy } from 'svelte';
import { Chart, registerables } from 'chart.js';
Chart.register(...registerables);
let { data } = $props();
let famSlug = $derived(page.params.fam);
let summary = $state(data.summary);
let donutCanvases = $state<Record<string, HTMLCanvasElement | undefined>>({});
let donutCharts: Record<string, Chart> = {};
let choresCanvas: HTMLCanvasElement | undefined;
let choresChart: Chart | undefined;
function captureCanvas(node: HTMLCanvasElement, memberId: string) {
donutCanvases[memberId] = node;
return {
destroy() { delete donutCanvases[memberId]; }
};
}
let members = $state(data.members);
let templates = $state(data.templates);
$effect(() => {
if (data.members?.length) members = data.members;
if (data.templates?.length) templates = data.templates;
if (famStore.initialized) {
if (famStore.members.length) members = famStore.members;
if (famStore.templates.length) templates = famStore.templates;
}
})
onMount(() => {
if (!summary?.summaries?.length) return;
if (choresCanvas && summary.daysInWeek?.length) {
const days = summary.daysInWeek;
const labels = days.map((d: string) =>
new Date(d + 'T12:00:00').toLocaleDateString('en-GB', { weekday: 'short' })
);
const datasets = summary.summaries.map((s: any) => ({
label: s.memberName,
data: days.map((d: string) => s.dayCompletions?.[d] || 0),
backgroundColor: s.memberColor || '#6366f1',
borderRadius: 4,
}));
choresChart = new Chart(choresCanvas, {
type: 'bar',
data: { labels, datasets },
options: {
responsive: true,
plugins: { legend: { display: true }, title: { display: true, text: 'Chores per day' } },
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } },
},
});
}
for (const s of summary.summaries) {
const canvas = donutCanvases[s.memberId];
if (!canvas) continue;
const done = s.choresCompleted || 0;
const total = s.totalChores || 0;
const remaining = Math.max(0, total - done);
donutCharts[s.memberId] = new Chart(canvas, {
type: 'doughnut',
data: {
labels: ['Done', 'Remaining'],
datasets: [{
data: [done, remaining],
backgroundColor: [s.memberColor || '#22c55e', '#e5e7eb'],
borderWidth: 0,
}],
},
options: {
responsive: true,
cutout: '70%',
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: (ctx) => `${ctx.label}: ${ctx.parsed} chores`,
},
},
},
},
});
}
});
onDestroy(() => {
choresChart?.destroy();
for (const c of Object.values(donutCharts)) c?.destroy();
famStore.cleanup();
});
</script>
<h1>🏠 {famSlug}</h1>
{#if summary?.summaries?.length}
<h2>This week</h2>
<table>
<thead><tr><th>Member</th><th>Points</th><th>Done</th></tr></thead>
<tbody>
{#each summary.summaries as s}
<tr>
<td><span style="color:{s.memberColor}"></span> {s.memberName}</td>
<td>{s.pointsEarned}</td>
<td>{s.choresCompleted}/{s.totalChores}</td>
</tr>
{/each}
</tbody>
</table>
<div class="card">
<h2>Chores per day</h2>
<canvas bind:this={choresCanvas} height="200"></canvas>
</div>
<div class="donut-grid">
{#each summary.summaries as s}
<div class="donut-card">
<canvas use:captureCanvas={s.memberId} height="150" width="150"></canvas>
<div class="donut-label" style="color:{s.memberColor}">{s.memberName}</div>
<div class="donut-points">{s.pointsEarned} pts</div>
<div class="donut-hint">{s.choresCompleted}/{s.totalChores} chores</div>
</div>
{/each}
</div>
{/if}
<h2>Members ({members?.length || 0})</h2>
<div class="member-grid">
{#each members || [] as m}
<a href="/{famSlug}/{m.name}" class="member-card" style="border-left: 4px solid {m.color}">
<strong>{m.name}</strong>
<span class="hint">{m.deviceTokenHint || '—'}</span>
</a>
{/each}
</div>
{#if templates?.length}
<h2>Templates ({templates.length})</h2>
<ul>
{#each templates as t}
<li>{t.name}{t.defaultFrequency}{t.defaultValue} {t.defaultType}</li>
{/each}
</ul>
{/if}
<style>
nav { display: flex; gap: 0.5rem; margin: 1rem 0; }
nav a { padding: 0.4rem 0.8rem; background: #6366f1; color: white; border-radius: 4px; text-decoration: none; font-size: 0.9rem; }
table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
th, td { padding: 0.4rem; border: 1px solid #e5e7eb; text-align: left; }
th { background: #f9fafb; font-weight: 600; }
.card { margin: 1rem 0; padding: 1rem; border: 1px solid #e5e7eb; border-radius: 8px; background: #f9fafb; }
.card h2 { margin: 0 0 0.75rem; font-size: 1rem; }
.card canvas { max-height: 50vh; }
.donut-grid { display: flex; gap: 1rem; flex-wrap: wrap; margin: 1rem 0; justify-content: center; }
.donut-card { display: flex; flex-direction: column; align-items: center; padding: 1rem; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; min-width: 160px; }
.donut-card canvas { max-height: 150px; max-width: 150px; }
.donut-label { font-weight: 600; font-size: 0.9rem; margin-top: 0.5rem; }
.donut-points { font-size: 1.1rem; font-weight: 700; }
.donut-hint { font-size: 0.75rem; color: #9ca3af; }
.member-grid { display: flex; gap: 0.5rem; flex-wrap: wrap; }
.member-card { flex: 1; min-width: 160px; padding: 0.8rem; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; text-decoration: none; color: inherit; display: block; }
.member-card strong { display: block; }
.hint { font-size: 0.8rem; color: #9ca3af; }
ul { list-style: none; padding: 0; }
li { padding: 0.25rem 0; }
</style>