update footer | add shared chores | expire todos if not completed

This commit is contained in:
JCEEE
2026-09-03 11:50:51 +01:00
parent 8cec99715a
commit 68a0851467
14 changed files with 531 additions and 126 deletions
@@ -179,9 +179,11 @@
: (data.templates as ChoreTemplate[])
);
let assigned = $derived(
famStore.initialized
? (famStore.assigned as AssignedChore[])
: (data.assigned as AssignedChore[])
(
famStore.initialized
? (famStore.assigned as AssignedChore[])
: (data.assigned as AssignedChore[])
).map((a) => (a.memberId === '' ? { ...a, memberId: null } : a))
);
let completions = $derived(
famStore.initialized
@@ -264,7 +266,7 @@
if (role === 'parent' || !memberId) return [];
const activeSeasonIds = new Set(famStore.seasons.filter((s) => s.active).map((s) => s.id));
return assigned.filter((a) => {
if (a.memberId !== memberId) return false;
if (a.memberId !== null && a.memberId !== memberId) return false;
if (a.isTodo) return false;
if (!a.seasonIds || a.seasonIds.length === 0) return true;
if (activeSeasonIds.size === 0) return true;
@@ -275,7 +277,7 @@
// Todos — one-off items with deadlines (sorted: soonest first)
let memberTodos = $derived.by(() =>
assigned
.filter((a) => a.memberId === memberId && a.isTodo)
.filter((a) => (a.memberId === null || a.memberId === memberId) && a.isTodo)
.sort((a, b) => {
const da = a.completeBy || '9999-99-99';
const db = b.completeBy || '9999-99-99';
@@ -347,7 +349,9 @@
// non-todo chores for the member (no season filter), weighted by the
// number of days in the displayed period — so the hero total agrees
// with the Goals progress instead of showing a different denominator.
const chores = assigned.filter((a) => a.memberId === memberId && !a.isTodo);
const chores = assigned.filter(
(a) => (a.memberId === null || a.memberId === memberId) && !a.isTodo
);
const periodDays =
Math.round(
(new Date(weekEnd + 'T00:00:00').getTime() - new Date(weekStart + 'T00:00:00').getTime()) /
@@ -491,7 +495,9 @@
// ─── Threshold Goals (bonus configs the member is chasing) ───
let thresholdGoals = $derived.by(() => {
// Member's assigned chores (non-todo) — used to compute period potential.
const myChores = assigned.filter((a) => a.memberId === memberId && !a.isTodo);
const myChores = assigned.filter(
(a) => (a.memberId === null || a.memberId === memberId) && !a.isTodo
);
return bonusConfigs
.filter((cfg) => {
if (cfg.status !== 'active') return false;
@@ -674,6 +680,14 @@
return completions.some((c) => c.assignedChoreId === todoId);
}
// A todo whose completeBy has passed without being completed is expired —
// no longer doable for the rest of the week.
function isTodoExpired(todo: AssignedChore): boolean {
if (isTodoDone(todo.id)) return false;
if (!todo.completeBy) return false;
return todo.completeBy < todayChild;
}
function findCompletion(chore: AssignedChore) {
const match = (c: Completion) =>
chore.isTodo
@@ -689,6 +703,7 @@
async function toggle(chore: AssignedChore) {
if (accessDisabled) return;
if (chore.isTodo && isTodoExpired(chore)) return;
if (togglingIds) return;
togglingIds = chore.id;
@@ -1340,6 +1355,7 @@
{/each}
{#each memberTodos.filter((t) => !isTodoDone(t.id)) as todo}
{@const urgency = todoUrgency(todo)}
{@const expired = isTodoExpired(todo)}
{#if todo.type === 'emoji'}
<button
class="chore todo-chore"
@@ -1348,14 +1364,17 @@
class:todo-orange={urgency === 'orange'}
class:todo-red={urgency === 'red'}
class:todo-black={urgency === 'black'}
class:todo-expired={expired}
onclick={() => toggle(todo)}
disabled={!!togglingIds}
disabled={!!togglingIds || expired}
>
<span class="checkbox">{@html circleIcon}</span>
<span class="todo-main">
<span class="chore-name">{todo.customName || 'Todo'}</span>
{#if todo.completeBy}
{#if todo.completeBy === todayChild}
{#if expired}
<span class="todo-due due-expired">EXPIRED</span>
{:else if todo.completeBy === todayChild}
<span class="todo-due due-today">DUE TODAY!</span>
{:else}
<span class="todo-due">due {formatHumanDate(todo.completeBy)}</span>
@@ -1373,14 +1392,17 @@
class:todo-orange={urgency === 'orange'}
class:todo-red={urgency === 'red'}
class:todo-black={urgency === 'black'}
class:todo-expired={expired}
onclick={() => toggle(todo)}
disabled={!!togglingIds}
disabled={!!togglingIds || expired}
>
<span class="checkbox">{@html circleIcon}</span>
<span class="todo-main">
<span class="chore-name">{todo.customName || 'Todo'}</span>
{#if todo.completeBy}
{#if todo.completeBy === todayChild}
{#if expired}
<span class="todo-due due-expired">EXPIRED</span>
{:else if todo.completeBy === todayChild}
<span class="todo-due due-today">DUE TODAY!</span>
{:else}
<span class="todo-due">due {formatHumanDate(todo.completeBy)}</span>
@@ -2372,6 +2394,20 @@
.chore.todo-black {
border-left-color: #1f2937;
}
.chore.todo-expired {
border-left-color: #9ca3af;
background: #f3f4f6;
opacity: 0.75;
}
.chore.todo-expired .chore-name {
text-decoration: line-through;
color: #6b7280;
}
.todo-due.due-expired {
color: #ef4444;
font-weight: 700;
letter-spacing: 0.03em;
}
.todo-label {
font-size: 0.62rem;
font-weight: 700;