Add FamDone Brand component and add rewards count system
This commit is contained in:
@@ -362,3 +362,8 @@
|
|||||||
- **`isDummyStripe` removed:** real test keys made every dummy branch dead code — and the webhook's `|| !signature` fallback accepted UNSIGNED events (forgeable access grants). Signature is now mandatory (400 without it); settings' simulated endSubscription/portal branches deleted. Dev verification stays via stripe-cli signed events (`pnpm stripe:listen`).
|
- **`isDummyStripe` removed:** real test keys made every dummy branch dead code — and the webhook's `|| !signature` fallback accepted UNSIGNED events (forgeable access grants). Signature is now mandatory (400 without it); settings' simulated endSubscription/portal branches deleted. Dev verification stays via stripe-cli signed events (`pnpm stripe:listen`).
|
||||||
- **Platform-admin auth hardened (supersedes the constant-cookie version):** `/admin` login now does a real `_superusers.authWithPassword` via PB; the minted superuser JWT goes in `platform_session` (`setPlatformSession` in session.ts). `hooks.server.ts` deviates on `/admin`: verifies the token with `_superusers.authRefresh` → `locals.platformAdmin` (forged values fail authRefresh and get cleared); fam-user pb_token flow skipped on that route. FINAL login shape (user-amended, working): action returns `{success:true}` (no redirect); form's enhance callback does `goto('/admin', { invalidateAll: true })` on success — forcing the load re-run with the fresh cookie; view branches on `data.authenticated`. Verified: real token renders dashboard, forged cookie gets login.
|
- **Platform-admin auth hardened (supersedes the constant-cookie version):** `/admin` login now does a real `_superusers.authWithPassword` via PB; the minted superuser JWT goes in `platform_session` (`setPlatformSession` in session.ts). `hooks.server.ts` deviates on `/admin`: verifies the token with `_superusers.authRefresh` → `locals.platformAdmin` (forged values fail authRefresh and get cleared); fam-user pb_token flow skipped on that route. FINAL login shape (user-amended, working): action returns `{success:true}` (no redirect); form's enhance callback does `goto('/admin', { invalidateAll: true })` on success — forcing the load re-run with the fresh cookie; view branches on `data.authenticated`. Verified: real token renders dashboard, forged cookie gets login.
|
||||||
- **use:enhance redirect gotcha (session-wide lesson):** action-thrown redirects surface as `result.type === 'redirect'` in the RESOLVE callback — handlers checking only `'success'` silently drop them (bit /admin login, pricing choose, and settings billingPortal). Shared fix: `lib/forms.ts` → `handleResult(handler?)` factory follows redirects via `goto`, delegates the rest to the handler or default `update()`. Use it for any form whose action can throw a redirect. Also: enhance is two-stage — `{result}` only exists in the resolve fn, not the submit params (was mis-handled in pricing/signup handlers).
|
- **use:enhance redirect gotcha (session-wide lesson):** action-thrown redirects surface as `result.type === 'redirect'` in the RESOLVE callback — handlers checking only `'success'` silently drop them (bit /admin login, pricing choose, and settings billingPortal). Shared fix: `lib/forms.ts` → `handleResult(handler?)` factory follows redirects via `goto`, delegates the rest to the handler or default `update()`. Use it for any form whose action can throw a redirect. Also: enhance is two-stage — `{result}` only exists in the resolve fn, not the submit params (was mis-handled in pricing/signup handlers).
|
||||||
|
|
||||||
|
### 2026-09-02 — Count-type reward: Target Chore + TODO (period semantics)
|
||||||
|
|
||||||
|
- **Feature:** Count-type rewards now support pinning to a single assigned chore (`bonus_configs.targetChoreId`). On the fam-admin Rewards modal (Step 2), when Type = "Count - (chores)" a **Target Chore** dropdown appears after the Target Value field — options are the selected member's assigned (non-todo) chores, default "All Chores". Evaluation (`bonuses.evaluateFam`) + display (`bonuses.progress`, dashboard `thresholdGoals`) now filter Count progress to only that chore's completions when set. Platform-level template form only needed the relabeled "Count - (chores)" — no target chore at template level. Pocket-money checkbox removed from the platform template form (only the auto-created per-child droplet needs it).
|
||||||
|
- **TODO (logic, not yet fixed):** Count rewards with a `period` (e.g. weekly) reset + re-earn each period like a points/cash threshold. Once a Count reward is pinned to a target (or all chores), the intended semantics are ambiguous: should it be **continuous** (unlimited, measured once since startDate until reached) or **limited to the period window** (re-earn each week)? Currently it follows the periodic-reset behavior. Decide whether Count rewards should ignore `period` (behave as standalone/unlimited since startDate) and adjust `bonusWindow`/evaluation accordingly. Not attempted in this change.
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:element this={tag} class="fam-done">
|
<svelte:element this={tag} class="fam-done">
|
||||||
FamDone<svg
|
Fam<span class="text-green-500">Done</span><svg
|
||||||
class="fam-done-check"
|
class="fam-done-check"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
width="14"
|
width="1em"
|
||||||
height="14"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="#10b981"
|
stroke="#10b981"
|
||||||
@@ -24,10 +24,9 @@
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
.fam-done-check {
|
.fam-done-check {
|
||||||
|
font-size: x-small;
|
||||||
vertical-align: super;
|
vertical-align: super;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.3em;
|
|
||||||
margin-left: 1px;
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -513,6 +513,10 @@ async function ensureBonusFields(): Promise<void> {
|
|||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (name === 'bonus_configs' && !has('targetChoreId')) {
|
||||||
|
fields.push({ name: 'targetChoreId', type: 'text', required: false });
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
// rewardValue must be optional so an unset pocket-money droplet can exist.
|
// rewardValue must be optional so an unset pocket-money droplet can exist.
|
||||||
const rv = fields.find((f: any) => f.name === 'rewardValue');
|
const rv = fields.find((f: any) => f.name === 'rewardValue');
|
||||||
if (rv && rv.required) {
|
if (rv && rv.required) {
|
||||||
|
|||||||
@@ -58,7 +58,15 @@ export async function evaluateFam(pb: any, famId: string) {
|
|||||||
// Pocket money pauses until the parent sets an amount.
|
// Pocket money pauses until the parent sets an amount.
|
||||||
if (cfg.isPocketMoney && !cfg.rewardValue) continue;
|
if (cfg.isPocketMoney && !cfg.rewardValue) continue;
|
||||||
const win = bonusWindow(cfg, paydayEval, tzEval);
|
const win = bonusWindow(cfg, paydayEval, tzEval);
|
||||||
const periodCompletions = allCompletions.filter((c: any) => completionInWindow(c, win));
|
let periodCompletions = allCompletions.filter((c: any) => completionInWindow(c, win));
|
||||||
|
|
||||||
|
// Count-type rewards can pin against a single assigned chore (targetChoreId).
|
||||||
|
// When set, only completions of that specific chore count toward the target.
|
||||||
|
if (cfg.type === 'count' && cfg.targetChoreId) {
|
||||||
|
periodCompletions = periodCompletions.filter(
|
||||||
|
(c: any) => c.assignedChoreId === cfg.targetChoreId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const existingRewards = allRewards.filter((r: any) => r.bonusConfigId === cfg.id);
|
const existingRewards = allRewards.filter((r: any) => r.bonusConfigId === cfg.id);
|
||||||
let createdReward = false;
|
let createdReward = false;
|
||||||
@@ -241,7 +249,15 @@ export async function progress(pb: any, famId: string) {
|
|||||||
const win = bonusWindow(cfg, payday, tz);
|
const win = bonusWindow(cfg, payday, tz);
|
||||||
const pStart = win.from;
|
const pStart = win.from;
|
||||||
const pEnd = win.to;
|
const pEnd = win.to;
|
||||||
const periodCompletions = completionsList.filter((c: any) => completionInWindow(c, win));
|
let periodCompletions = completionsList.filter((c: any) => completionInWindow(c, win));
|
||||||
|
|
||||||
|
// Count-type rewards pinned to a specific chore only count that chore's
|
||||||
|
// completions toward the target.
|
||||||
|
if (cfg.type === 'count' && cfg.targetChoreId) {
|
||||||
|
periodCompletions = periodCompletions.filter(
|
||||||
|
(c: any) => c.assignedChoreId === cfg.targetChoreId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const progressRows: any[] = [];
|
const progressRows: any[] = [];
|
||||||
|
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ export interface BonusConfig {
|
|||||||
criteriaValue?: number;
|
criteriaValue?: number;
|
||||||
period?: BonusPeriod;
|
period?: BonusPeriod;
|
||||||
status: BonusStatus;
|
status: BonusStatus;
|
||||||
|
targetChoreId?: string;
|
||||||
isPocketMoney?: boolean;
|
isPocketMoney?: boolean;
|
||||||
completeBy?: 'unlimited' | 'week' | 'custom';
|
completeBy?: 'unlimited' | 'week' | 'custom';
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
|
|||||||
@@ -522,9 +522,15 @@
|
|||||||
const isPercent = cfg.thresholdType === 'percent';
|
const isPercent = cfg.thresholdType === 'percent';
|
||||||
const isCount = cfg.type === 'count';
|
const isCount = cfg.type === 'count';
|
||||||
|
|
||||||
|
// Count-type rewards pinned to a single chore scoped their count
|
||||||
|
// (and the period's reclaimable potential) to that chore only.
|
||||||
|
const countChores = isCount && cfg.targetChoreId
|
||||||
|
? myChores.filter((a) => a.id === cfg.targetChoreId)
|
||||||
|
: myChores;
|
||||||
|
|
||||||
// Frequency-aware total potential (a daily chore = `periodDays`
|
// Frequency-aware total potential (a daily chore = `periodDays`
|
||||||
// instances, a weekly chore = 1).
|
// instances, a weekly chore = 1).
|
||||||
const totalPotential = myChores.reduce(
|
const totalPotential = countChores.reduce(
|
||||||
(sum, a) => sum + (a.frequency === 'daily' ? periodDays : 1),
|
(sum, a) => sum + (a.frequency === 'daily' ? periodDays : 1),
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
@@ -533,7 +539,9 @@
|
|||||||
if (isPercent) {
|
if (isPercent) {
|
||||||
current = periodCompletions.length;
|
current = periodCompletions.length;
|
||||||
} else if (isCount) {
|
} else if (isCount) {
|
||||||
current = periodCompletions.length;
|
current = cfg.targetChoreId
|
||||||
|
? periodCompletions.filter((c) => c.assignedChoreId === cfg.targetChoreId).length
|
||||||
|
: periodCompletions.length;
|
||||||
} else {
|
} else {
|
||||||
current = periodCompletions.reduce((sum, c) => {
|
current = periodCompletions.reduce((sum, c) => {
|
||||||
const chore = assigned.find((a) => a.id === c.assignedChoreId);
|
const chore = assigned.find((a) => a.id === c.assignedChoreId);
|
||||||
@@ -569,6 +577,7 @@
|
|||||||
const unit = isPercent ? '% of chores' : isCount ? 'chores' : 'pts';
|
const unit = isPercent ? '% of chores' : isCount ? 'chores' : 'pts';
|
||||||
const targetLabel = `${configured} ${unit}`;
|
const targetLabel = `${configured} ${unit}`;
|
||||||
const targetPct = isPercent ? Math.min(100, Math.max(0, configured)) : 100;
|
const targetPct = isPercent ? Math.min(100, Math.max(0, configured)) : 100;
|
||||||
|
const targetChore = isCount ? countChores[0] : null;
|
||||||
return {
|
return {
|
||||||
config: cfg,
|
config: cfg,
|
||||||
current,
|
current,
|
||||||
@@ -578,6 +587,11 @@
|
|||||||
target,
|
target,
|
||||||
totalPotential,
|
totalPotential,
|
||||||
isPercent,
|
isPercent,
|
||||||
|
targetChoreName: targetChore
|
||||||
|
? targetChore.customName ||
|
||||||
|
templates.find((t) => t.id === targetChore.templateId)?.name ||
|
||||||
|
''
|
||||||
|
: '',
|
||||||
pct,
|
pct,
|
||||||
achieved,
|
achieved,
|
||||||
rewardStatus: existingReward?.status || null,
|
rewardStatus: existingReward?.status || null,
|
||||||
@@ -1252,7 +1266,11 @@
|
|||||||
<span class="goal-badge">{@html checkCircleIcon} earned</span>
|
<span class="goal-badge">{@html checkCircleIcon} earned</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="goal-pct">{pct}%</span>
|
<span class="goal-pct">{pct}%</span>
|
||||||
<span class="goal-target-pill">{@html targetIcon} {goal.targetLabel}</span>
|
<span class="goal-target-pill"
|
||||||
|
>{@html targetIcon} {goal.targetLabel}{#if goal.targetChoreName}
|
||||||
|
<span class="goal-chore"> · {goal.targetChoreName}</span>
|
||||||
|
{/if}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2214,6 +2232,10 @@
|
|||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
.goal-chore {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
.goal-achieved .goal-bar-fill {
|
.goal-achieved .goal-bar-fill {
|
||||||
background: linear-gradient(90deg, #10b981, #34d399);
|
background: linear-gradient(90deg, #10b981, #34d399);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,6 +115,8 @@ export const actions = {
|
|||||||
const period = fd.get('period');
|
const period = fd.get('period');
|
||||||
if (period !== null) data.period = period;
|
if (period !== null) data.period = period;
|
||||||
if (data.occurrence === 'once') data.period = '';
|
if (data.occurrence === 'once') data.period = '';
|
||||||
|
const targetChoreId = fd.get('targetChoreId');
|
||||||
|
if (targetChoreId !== null) data.targetChoreId = targetChoreId || '';
|
||||||
const description = fd.get('description');
|
const description = fd.get('description');
|
||||||
if (description) data.description = description;
|
if (description) data.description = description;
|
||||||
const memberId = fd.get('memberId');
|
const memberId = fd.get('memberId');
|
||||||
@@ -159,6 +161,8 @@ export const actions = {
|
|||||||
const period = fd.get('period');
|
const period = fd.get('period');
|
||||||
if (period !== null) data.period = period;
|
if (period !== null) data.period = period;
|
||||||
if (occurrence === 'once') data.period = '';
|
if (occurrence === 'once') data.period = '';
|
||||||
|
const targetChoreId = fd.get('targetChoreId');
|
||||||
|
if (targetChoreId !== null) data.targetChoreId = targetChoreId || '';
|
||||||
const description = fd.get('description');
|
const description = fd.get('description');
|
||||||
if (description) data.description = description;
|
if (description) data.description = description;
|
||||||
const memberId = fd.get('memberId');
|
const memberId = fd.get('memberId');
|
||||||
@@ -212,6 +216,7 @@ export const actions = {
|
|||||||
memberId: fd.get('memberId') || '',
|
memberId: fd.get('memberId') || '',
|
||||||
status,
|
status,
|
||||||
isPocketMoney: !!fd.get('isPocketMoney'),
|
isPocketMoney: !!fd.get('isPocketMoney'),
|
||||||
|
targetChoreId: fd.get('targetChoreId') || '',
|
||||||
completeBy: fd.get('completeBy') || 'unlimited',
|
completeBy: fd.get('completeBy') || 'unlimited',
|
||||||
startDate: fd.get('startDate') || '',
|
startDate: fd.get('startDate') || '',
|
||||||
completeByDate: fd.get('completeByDate') || ''
|
completeByDate: fd.get('completeByDate') || ''
|
||||||
|
|||||||
@@ -89,7 +89,8 @@
|
|||||||
startMode: 'today' as string,
|
startMode: 'today' as string,
|
||||||
completeBy: 'unlimited' as string,
|
completeBy: 'unlimited' as string,
|
||||||
startDate: '' as string,
|
startDate: '' as string,
|
||||||
completeByDate: '' as string
|
completeByDate: '' as string,
|
||||||
|
targetChoreId: '' as string
|
||||||
});
|
});
|
||||||
|
|
||||||
let configs = $derived(
|
let configs = $derived(
|
||||||
@@ -116,6 +117,14 @@
|
|||||||
// Standalone (no period) rewards can scope progress by completeBy + dates.
|
// Standalone (no period) rewards can scope progress by completeBy + dates.
|
||||||
let showProgressWindow = $derived(editVals.type !== 'manual' && !editVals.period);
|
let showProgressWindow = $derived(editVals.type !== 'manual' && !editVals.period);
|
||||||
|
|
||||||
|
// Target Chore options: per the selected member, their assigned (non-todo)
|
||||||
|
// chores. When no member is selected (All), list every family chore.
|
||||||
|
let targetChoreOptions = $derived(
|
||||||
|
famStore.assigned.filter(
|
||||||
|
(a) => (editVals.memberId ? a.memberId === editVals.memberId : true) && !a.isTodo
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
let templateConfigs = $derived((templates as BonusTemplate[]).filter((t) => t.global));
|
let templateConfigs = $derived((templates as BonusTemplate[]).filter((t) => t.global));
|
||||||
let allBonusConfigs = $derived(
|
let allBonusConfigs = $derived(
|
||||||
configs.filter((c: BonusConfig) => c.status !== 'completed') as BonusConfig[]
|
configs.filter((c: BonusConfig) => c.status !== 'completed') as BonusConfig[]
|
||||||
@@ -194,7 +203,8 @@
|
|||||||
startMode: 'today',
|
startMode: 'today',
|
||||||
completeBy: 'unlimited',
|
completeBy: 'unlimited',
|
||||||
startDate: todayIso(),
|
startDate: todayIso(),
|
||||||
completeByDate: ''
|
completeByDate: '',
|
||||||
|
targetChoreId: ''
|
||||||
};
|
};
|
||||||
step = 1;
|
step = 1;
|
||||||
showEditModal = true;
|
showEditModal = true;
|
||||||
@@ -246,7 +256,8 @@
|
|||||||
startMode: cfg.status === 'disabled' ? 'disabled' : 'today',
|
startMode: cfg.status === 'disabled' ? 'disabled' : 'today',
|
||||||
completeBy: cfg.completeBy || 'unlimited',
|
completeBy: cfg.completeBy || 'unlimited',
|
||||||
startDate: cfg.startDate || todayIso(),
|
startDate: cfg.startDate || todayIso(),
|
||||||
completeByDate: cfg.completeByDate || ''
|
completeByDate: cfg.completeByDate || '',
|
||||||
|
targetChoreId: cfg.targetChoreId || ''
|
||||||
};
|
};
|
||||||
step = 1;
|
step = 1;
|
||||||
showEditModal = true;
|
showEditModal = true;
|
||||||
@@ -372,6 +383,11 @@
|
|||||||
<div class="bar-fill" style="width:{pct}%" class:met={achieved}></div>
|
<div class="bar-fill" style="width:{pct}%" class:met={achieved}></div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if cfg.type === 'count'}
|
||||||
|
<span class="count-progress">
|
||||||
|
{Math.min(current, criteria)} / {criteria}{cfg.targetChoreId ? ' chores' : ''}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
<span class="stat {p?.state ?? 'pending'}">
|
<span class="stat {p?.state ?? 'pending'}">
|
||||||
{#if !p || p.state === 'pending'}<Clock size={13} />{:else}{p.state}{/if}
|
{#if !p || p.state === 'pending'}<Clock size={13} />{:else}{p.state}{/if}
|
||||||
</span>
|
</span>
|
||||||
@@ -577,8 +593,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- STEP 1 — Who (basics) -->
|
<!-- STEP 1 — Who (basics) -->
|
||||||
{#if step === 1}
|
<div class="step-body" hidden={step !== 1}>
|
||||||
<div class="step-body">
|
|
||||||
<label>
|
<label>
|
||||||
<span class="field-label">{@html stepIcon(1)} Name</span>
|
<span class="field-label">{@html stepIcon(1)} Name</span>
|
||||||
<input name="name" bind:value={editVals.name} required />
|
<input name="name" bind:value={editVals.name} required />
|
||||||
@@ -607,18 +622,16 @@
|
|||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- STEP 2 — How (rules + progress) -->
|
<!-- STEP 2 — How (rules + progress) -->
|
||||||
{#if step === 2}
|
<div class="step-body" hidden={step !== 2}>
|
||||||
<div class="step-body">
|
|
||||||
<label>
|
<label>
|
||||||
<span class="field-label"><Settings2 size={14} /> Type</span>
|
<span class="field-label"><Settings2 size={14} /> Type</span>
|
||||||
<select name="type" bind:value={editVals.type}>
|
<select name="type" bind:value={editVals.type}>
|
||||||
<option value="threshold">Threshold</option>
|
<option value="threshold">Threshold</option>
|
||||||
<option value="count">Count</option>
|
<option value="count">Count - (chores)</option>
|
||||||
<option value="manual">Manual</option>
|
<option value="manual">Manual</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{#if editVals.type === 'threshold'}
|
{#if editVals.type === 'threshold'}
|
||||||
<label>
|
<label>
|
||||||
@@ -661,6 +674,23 @@
|
|||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if editVals.type === 'count'}
|
||||||
|
<label>
|
||||||
|
<span class="field-label"><Target size={14} /> Target Chore</span>
|
||||||
|
<select name="targetChoreId" bind:value={editVals.targetChoreId}>
|
||||||
|
<option value="">All Chores</option>
|
||||||
|
{#each targetChoreOptions as ac}
|
||||||
|
<option value={ac.id}>
|
||||||
|
{ac.customName || famStore.templateMap().get(ac.templateId || '')?.name || 'Chore'}
|
||||||
|
</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
<span class="hint"
|
||||||
|
>Count completions of a specific chore only. Leave as "All Chores" to count
|
||||||
|
every assigned chore.</span
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
<label>
|
<label>
|
||||||
<span class="field-label"><Settings2 size={14} /> Occurrence</span>
|
<span class="field-label"><Settings2 size={14} /> Occurrence</span>
|
||||||
<select name="occurrence" bind:value={editVals.occurrence}>
|
<select name="occurrence" bind:value={editVals.occurrence}>
|
||||||
@@ -724,11 +754,9 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- STEP 3 — What (reward) -->
|
<!-- STEP 3 — What (reward) -->
|
||||||
{#if step === 3}
|
<div class="step-body" hidden={step !== 3}>
|
||||||
<div class="step-body">
|
|
||||||
<label>
|
<label>
|
||||||
<span class="field-label">
|
<span class="field-label">
|
||||||
{#if editVals.rewardType === 'cash'}
|
{#if editVals.rewardType === 'cash'}
|
||||||
@@ -805,7 +833,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<!-- Footer actions -->
|
<!-- Footer actions -->
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
@@ -1135,6 +1162,12 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
.count-progress {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
.bar-wrap {
|
.bar-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
|
|||||||
@@ -277,8 +277,7 @@ export const actions: Actions = {
|
|||||||
rewardType: (fd.get('rewardType') as string) || 'points',
|
rewardType: (fd.get('rewardType') as string) || 'points',
|
||||||
rewardValue: (fd.get('rewardValue') as string) || '',
|
rewardValue: (fd.get('rewardValue') as string) || '',
|
||||||
criteriaValue: Number(fd.get('criteriaValue') || 0),
|
criteriaValue: Number(fd.get('criteriaValue') || 0),
|
||||||
period: (fd.get('period') as string) || 'weekly',
|
period: (fd.get('period') as string) || 'weekly'
|
||||||
isPocketMoney: fd.get('isPocketMoney') === 'true'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
@@ -322,8 +321,7 @@ export const actions: Actions = {
|
|||||||
rewardType: (fd.get('rewardType') as string) || 'points',
|
rewardType: (fd.get('rewardType') as string) || 'points',
|
||||||
rewardValue: (fd.get('rewardValue') as string) || '',
|
rewardValue: (fd.get('rewardValue') as string) || '',
|
||||||
criteriaValue: Number(fd.get('criteriaValue') || 0),
|
criteriaValue: Number(fd.get('criteriaValue') || 0),
|
||||||
period: (fd.get('period') as string) || 'weekly',
|
period: (fd.get('period') as string) || 'weekly'
|
||||||
isPocketMoney: fd.get('isPocketMoney') === 'true'
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|||||||
@@ -45,8 +45,7 @@
|
|||||||
rewardType: 'points',
|
rewardType: 'points',
|
||||||
rewardValue: '',
|
rewardValue: '',
|
||||||
criteriaValue: 0,
|
criteriaValue: 0,
|
||||||
period: 'weekly',
|
period: 'weekly'
|
||||||
isPocketMoney: false
|
|
||||||
};
|
};
|
||||||
let tplVals = $state({ ...blankTpl });
|
let tplVals = $state({ ...blankTpl });
|
||||||
|
|
||||||
@@ -74,8 +73,7 @@
|
|||||||
rewardType: rec.rewardType || 'points',
|
rewardType: rec.rewardType || 'points',
|
||||||
rewardValue: rec.rewardValue ?? '',
|
rewardValue: rec.rewardValue ?? '',
|
||||||
criteriaValue: rec.criteriaValue ?? 0,
|
criteriaValue: rec.criteriaValue ?? 0,
|
||||||
period: rec.period || 'weekly',
|
period: rec.period || 'weekly'
|
||||||
isPocketMoney: !!rec.isPocketMoney
|
|
||||||
};
|
};
|
||||||
showTplModal = true;
|
showTplModal = true;
|
||||||
}
|
}
|
||||||
@@ -522,11 +520,11 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Type
|
Type
|
||||||
<select name="bonusType" bind:value={tplVals.bonusType}>
|
<select name="bonusType" bind:value={tplVals.bonusType}>
|
||||||
<option value="threshold">Threshold</option>
|
<option value="threshold">Threshold</option>
|
||||||
<option value="count">Count</option>
|
<option value="count">Count - (chores)</option>
|
||||||
<option value="manual">Manual</option>
|
<option value="manual">Manual</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{#if tplVals.bonusType === 'threshold'}
|
{#if tplVals.bonusType === 'threshold'}
|
||||||
<label>
|
<label>
|
||||||
@@ -568,15 +566,7 @@
|
|||||||
<option value="monthly">Monthly</option>
|
<option value="monthly">Monthly</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
name="isPocketMoney"
|
|
||||||
value="true"
|
|
||||||
bind:checked={tplVals.isPocketMoney}
|
|
||||||
/>
|
|
||||||
Pocket Money
|
|
||||||
</label>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
@@ -1118,11 +1108,6 @@
|
|||||||
background: #dcfce7;
|
background: #dcfce7;
|
||||||
color: #166534;
|
color: #166534;
|
||||||
}
|
}
|
||||||
.check {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Modal (shared) ── */
|
/* ── Modal (shared) ── */
|
||||||
.overlay {
|
.overlay {
|
||||||
|
|||||||
@@ -228,6 +228,7 @@ export const SCHEMA_PLAN: CollectionPlanEntry[] = [
|
|||||||
rel("memberId", ids.users),
|
rel("memberId", ids.users),
|
||||||
select("period", ["schedule", "daily", "weekly", "monthly"]),
|
select("period", ["schedule", "daily", "weekly", "monthly"]),
|
||||||
select("status", ["active", "completed"], true),
|
select("status", ["active", "completed"], true),
|
||||||
|
text("targetChoreId"),
|
||||||
bool("isPocketMoney"),
|
bool("isPocketMoney"),
|
||||||
// Standalone/one-off reward progress window. `period` stays empty for
|
// Standalone/one-off reward progress window. `period` stays empty for
|
||||||
// these (they're measured once, not per-cycle). `completeBy` bounds how
|
// these (they're measured once, not per-cycle). `completeBy` bounds how
|
||||||
|
|||||||
Reference in New Issue
Block a user