diff --git a/frontend/src/lib/components/ViewHeader.svelte b/frontend/src/lib/components/ViewHeader.svelte index be5301c..737048b 100644 --- a/frontend/src/lib/components/ViewHeader.svelte +++ b/frontend/src/lib/components/ViewHeader.svelte @@ -17,7 +17,13 @@ active: string; onchange: (v: string) => void; }; - weeknav?: { current: string; onPrev: () => void; onNext: () => void }; + weeknav?: { + current: string; + onPrev: () => void; + onNext: () => void; + onCurrent?: () => void; + isCurrent?: boolean; + }; sort?: { options: { label: string; value: string }[]; active: string; @@ -35,6 +41,15 @@ {@render children?.()} + {#if weeknav?.onCurrent} + + {/if} +
{#if tabs}
@@ -142,6 +157,13 @@ justify-content: space-between; gap: 0.5rem; } + /* Back-to-this-week CTA: lives outside .view-tools on the header's top + row, pushed right. Always rendered (visibility-toggled) so the header + keeps its height when it appears/disappears. */ + .header-now { + margin-left: auto; + align-self: flex-end; + } .nav-btn { padding: 0.2rem 0.5rem; border: 1px solid #d1d5db; @@ -154,6 +176,41 @@ .nav-btn:hover { background: #f3f4f6; } + .nav-btn.now-btn { + font-size: 0.8rem; + font-weight: 700; + white-space: nowrap; + } + /* Same pill CTA as the member hero. Always rendered (visibility-toggled) + so the ‹ › buttons never jump when it appears/disappears. */ + .now-btn { + background: none; + border: none; + cursor: pointer; + padding: 0; + } + .now-btn span { + display: inline-block; + background: rgba(255, 255, 255, 0.92); + color: #4338ca; + border-radius: 999px; + padding: 0.35rem 0.9rem; + font-size: 0.8rem; + font-weight: 800; + white-space: nowrap; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); + } + .view-header.hero .now-btn span { + background: rgba(255, 255, 255, 0.92); + color: #4338ca; + } + .now-btn:hover span { + background: #fff; + } + .now-btn.now-hidden { + visibility: hidden; + pointer-events: none; + } .nav-label { font-size: 0.85rem; color: #374151; diff --git a/frontend/src/routes/[fam]/[username]/+page.svelte b/frontend/src/routes/[fam]/[username]/+page.svelte index f37ad7c..2b72284 100644 --- a/frontend/src/routes/[fam]/[username]/+page.svelte +++ b/frontend/src/routes/[fam]/[username]/+page.svelte @@ -384,13 +384,12 @@ ); let weeklyTotal = $derived.by(() => { - // Must match the pocket-money % threshold definition: all assigned - // 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 === null || a.memberId === memberId) && !a.isTodo - ); + // Must match the pocket-money % threshold definition (and the family + // dashboard Done denominator): only the member's own chores count — + // personal assignments + shared chores they claimed. Unclaimed shared + // chores stay doable in the kanban but out of every denominator, so + // all views agree instead of drifting (e.g. 1/37 here vs 1/30 there). + const chores = assigned.filter((a) => a.memberId === memberId && !a.isTodo); const periodDays = Math.round( (new Date(weekEnd + 'T00:00:00').getTime() - new Date(weekStart + 'T00:00:00').getTime()) / @@ -407,6 +406,29 @@ (c.date?.slice(0, 10) || c.date) <= weekEnd ) ); + // Viewed-week daily activity for the hero background effect: completions + // per day across the 7-day window, plotted as a blurred white line. + let heroWeekActivity = $derived.by(() => { + const counts: number[] = []; + for (let i = 0; i < 7; i++) { + const d = addDays(weekStart, i); + counts.push( + weekCompletions.filter((c) => (c.date?.slice(0, 10) || c.date) === d).length + ); + } + return counts; + }); + let heroSpark = $derived.by(() => { + const max = Math.max(1, ...heroWeekActivity); + const pts = heroWeekActivity.map((v, i) => ({ + x: 20 + i * ((700 - 40) / 6), + y: 100 - (v / max) * 72 + })); + return { + line: pts.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' '), + pts + }; + }); let weeklyDone = $derived(weekCompletions.length); let weeklyPct = $derived(weeklyTotal > 0 ? Math.round((weeklyDone / weeklyTotal) * 100) : 0); @@ -536,10 +558,10 @@ // ─── 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 === null || a.memberId === memberId) && !a.isTodo - ); + // Member's own chores (non-todo) — personal + claimed shared, never + // unclaimed shared — used to compute period potential. Matches + // weeklyTotal above, the family dashboard, and the server's % definition. + const myChores = assigned.filter((a) => a.memberId === memberId && !a.isTodo); return bonusConfigs .filter((cfg) => { if (cfg.status !== 'active') return false; @@ -1227,6 +1249,24 @@ {/if}
+
- {#if anyWeekBonuses} - {#if weekBonusTallies.length > 0} +
+ {#if anyWeekBonuses && weekBonusTallies.length > 0}
{#each weekBonusTallies as t} 🎉 {t.name} ×{t.count} {/each}
{/if} +
+ {#if isCurrentWeek} +
{daysLeft} days until payday
{:else} - no bonuses earned yet + {/if} -
{daysLeft} days until payday