update ui and other frontend updates
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user