update footer | add shared chores | expire todos if not completed
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user