fixed payout locks & time sensitive todos & minor ui updates on chores view

This commit is contained in:
JCEEE
2026-08-04 11:38:38 +01:00
parent 7603c3eb21
commit c38ca43f0b
11 changed files with 289 additions and 164 deletions
@@ -4,6 +4,7 @@
import { onMount } from 'svelte';
import { famStore } from '$lib/stores/fam.svelte';
import { memberApi } from '$lib/client/api';
import { formatDDMMYY, formatShortDate } from '$lib/format';
import { ViewHeader, CardGrid, Card, Button } from '$lib/components';
import type { AssignedChore, Completion, ChoreTemplate, BonusConfig, Reward } from '$lib/types';
import {
@@ -40,9 +41,8 @@
let summary = $state(data.summary);
let today = $derived(todayInTz(famTz));
let simulateEow = $state(!!data.settings?.simulateEow);
let simulateEow = $state(!!(data.settings?.simulateEow ?? data.simulateEow));
let eowPreview = $state<any>(null);
const responseTags = [
'👏 well done',
'😊 really pleased',
@@ -352,9 +352,17 @@
.reduce((sum, r) => sum + Number(r.value), 0)
);
// Money owed to this member across all time (incl. carry-over from past weeks).
// Payday-gated rewards are excluded until their settleDate — the wallet shows
// them separately as a locked "pays out on payday" line.
let owedCash = $derived.by(() =>
rewards
.filter((r) => r.memberId === memberId && r.rewardType === 'cash' && r.status !== 'claimed')
.filter(
(r) =>
r.memberId === memberId &&
r.rewardType === 'cash' &&
r.status !== 'claimed' &&
!paydayLocked(r)
)
.reduce((sum, r) => sum + Number(r.value), 0)
);
let weekPointsEarned = $derived.by(() =>
@@ -370,6 +378,10 @@
(r.status === 'unclaimed' || r.status === 'requested')
)
);
// Payday-gated bonus rewards unlock on their settleDate (stamped server-side).
function paydayLocked(r: any) {
return r.claimable === 'payday' && r.settleDate && todayChild < r.settleDate;
}
let weekBonusTallies = $derived.by(() => {
const map = new Map<string, number>();
for (const r of weekRewards) {
@@ -786,7 +798,12 @@
{#each outstanding as r}
<div class="payment-row outstanding">
<span>{r.label}</span>
<span class="value">{rewardLabel(r)}</span>
<span class="value">
{rewardLabel(r)}
{#if paydayLocked(r)}
<span class="owe-locked">🔒 {formatShortDate(r.settleDate)}</span>
{/if}
</span>
</div>
{/each}
{/if}
@@ -822,28 +839,10 @@
</Card>
</CardGrid>
<CardGrid>
<Card cols={3} title="Debug: Simulate Payday" accent="#f59e0b">
<Card cols={3} title="Debug: Preview payday" accent="#f59e0b">
<p class="eow-desc">
Read-only preview of what the payday rollover will produce. Nothing here is written.
</p>
<form
method="POST"
action="?/setEow"
use:enhance={() => {
return async ({ result }) => {
const d = (result as any).data || {};
if (d.error) toast = d.error;
else simulateEow = !!d.simulateEow;
};
}}
>
<input type="hidden" name="on" value={simulateEow ? 'false' : 'true'} />
<button type="submit" class="eow-switch" class:on={simulateEow}>
{simulateEow ? 'Simulation ON' : 'Simulation OFF'}
</button>
</form>
<p class="eow-note">
This toggles the family debug flag only. It does not alter any live data.
Read-only preview of what the payday rollover will settle for this week. Nothing here is
written.
</p>
<form
@@ -853,19 +852,44 @@
return async ({ result }) => {
const d = (result as any).data || {};
if (d.error) toast = d.error;
else eowPreview = d.preview;
else {
eowPreview = d.preview;
simulateEow = !!d.simulateEow;
}
};
}}
>
<Button type="submit" size="sm" variant="secondary">Preview rollover</Button>
<Button type="submit" size="sm" variant="secondary">Preview payday</Button>
</form>
{#if simulateEow}
<p class="eow-mode-on">
👀 Preview mode is ON — child dashboards show a "payday preview" notice.
</p>
<form
method="POST"
action="?/setEow"
use:enhance={() => {
return async ({ result }) => {
const d = (result as any).data || {};
if (d.error) toast = d.error;
else simulateEow = !!d.simulateEow;
};
}}
>
<input type="hidden" name="on" value="false" />
<Button type="submit" size="sm" variant="ghost">Turn off preview</Button>
</form>
{/if}
{#if eowPreview}
<div class="eow-out">
<p class="eow-range">Week {eowPreview.weekStart}{eowPreview.weekEnd}</p>
<p class="eow-range">
Week {formatDDMMYY(eowPreview.weekStart)}{formatDDMMYY(eowPreview.weekEnd)}
</p>
<p class="eow-reset">
On rollover these {eowPreview.completionsThisWeek} completions reset; week starts anew at
{eowPreview.nextWeekStart}.
On payday these {eowPreview.completionsThisWeek} completions reset; week starts anew at
{formatDDMMYY(eowPreview.nextWeekStart)}.
</p>
<div class="eow-summaries">
@@ -904,6 +928,12 @@
{:else if error}
<p class="error">{error}</p>
{:else}
{#if simulateEow}
<div class="preview-notice">
👀 <b>Payday preview</b> — your parent is checking this week's payday. Nothing is paid
out yet.
</div>
{/if}
{#if owedCash > 0}
<div class="payday-banner">
🎉 You've earned <b>£{owedCash.toFixed(2)}</b> — go get it from your parent!
@@ -1240,6 +1270,10 @@
<span class="wr-pending">✓ credited</span>
{:else if isReq}
<span class="wr-pending">⏳ waiting</span>
{:else if paydayLocked(r)}
<span class="wr-pending wr-locked"
>🔒 pays out {formatShortDate(r.settleDate)}</span
>
{:else}
<button
class="wr-cta"
@@ -2020,6 +2054,15 @@
font-weight: 600;
color: #6b7280;
}
.wr-locked {
color: #d97706;
}
.owe-locked {
margin-left: 0.35rem;
font-size: 0.72rem;
font-weight: 600;
color: #d97706;
}
.wr-cta {
font-size: 0.72rem;
font-weight: 700;
@@ -2038,6 +2081,30 @@
margin: 0 0 0.6rem;
}
.eow-mode-on {
display: flex;
align-items: center;
gap: 0.5rem;
margin: 0.6rem 0;
padding: 0.6rem 0.8rem;
border: 1px dashed #f59e0b;
border-radius: 8px;
background: #fffbeb;
color: #92400e;
font-size: 0.85rem;
}
.preview-notice {
margin-bottom: 1rem;
padding: 0.85rem 1.1rem;
border-radius: 12px;
border: 1px dashed #f59e0b;
background: #fffbeb;
color: #92400e;
font-size: 0.95rem;
line-height: 1.4;
}
.payday-banner {
display: flex;
align-items: center;
@@ -2092,25 +2159,6 @@
font-size: 0.8rem;
opacity: 0.85;
}
.eow-switch {
font-size: 0.85rem;
font-weight: 700;
padding: 0.45rem 1rem;
border: 1px solid #f59e0b;
border-radius: 8px;
background: #fffbeb;
color: #92400e;
cursor: pointer;
}
.eow-switch.on {
background: #f59e0b;
color: #fff;
}
.eow-note {
font-size: 0.75rem;
color: #9ca3af;
margin: 0.4rem 0 0.6rem;
}
.eow-out {
margin-top: 0.75rem;
border-top: 1px dashed #e5e7eb;