added pocketmoney default droplet
This commit is contained in:
@@ -633,16 +633,33 @@
|
||||
{@const done = todays.length}
|
||||
{@const pct = total > 0 ? Math.round((done / total) * 100) : 0}
|
||||
{@const s = memberInSummary(m.id)}
|
||||
{@const pm = parentBonusConfigs.find(
|
||||
(c: any) => c.isPocketMoney && c.memberId === m.id
|
||||
)}
|
||||
<div class="member-card">
|
||||
<div class="card-header">
|
||||
<span class="dot" style="background:{m.color}"></span>
|
||||
<span class="member-name">{m.name}</span>
|
||||
<a href="/{famSlug}/{handleOf(m.username)}" class="link">Kanban</a>
|
||||
<Button
|
||||
href="/{famSlug}/{username}/bonuses?s=pocketmoney"
|
||||
variant="secondary"
|
||||
size="sm">Pocket money</Button
|
||||
>
|
||||
</div>
|
||||
<div class="stats">
|
||||
<span>Points: {s?.pointsEarned ?? 0}</span>
|
||||
<span>Money: £{(s?.moneyEarned ?? 0).toFixed(2)}</span>
|
||||
</div>
|
||||
<div class="pocket-row">
|
||||
{#if pm && pm.rewardValue}
|
||||
<span
|
||||
>Pocket money: £{Number(pm.rewardValue).toFixed(2)} · {pm.criteriaValue || 0}% of
|
||||
chores</span
|
||||
>
|
||||
{:else}
|
||||
<span class="unset">Pocket money: amount not set</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="progress-row">
|
||||
<span class="label">Today:</span>
|
||||
<div class="bar-wrap">
|
||||
@@ -1362,6 +1379,15 @@ const res = await fetch('/api/members', {
|
||||
color: #6366f1;
|
||||
text-decoration: none;
|
||||
}
|
||||
.pocket-row {
|
||||
font-size: 0.78rem;
|
||||
color: #6b7280;
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
.pocket-row .unset {
|
||||
color: #b45309;
|
||||
font-weight: 600;
|
||||
}
|
||||
.stats {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
|
||||
@@ -25,6 +25,7 @@ export const actions = {
|
||||
description: fd.get('description') || '',
|
||||
target: fd.get('target'),
|
||||
type: fd.get('type'),
|
||||
thresholdType: fd.get('thresholdType') || 'points',
|
||||
occurrence: fd.get('occurrence'),
|
||||
rewardType: fd.get('rewardType'),
|
||||
rewardValue: fd.get('rewardValue')
|
||||
@@ -55,6 +56,8 @@ export const actions = {
|
||||
if (target) data.target = target;
|
||||
const type = fd.get('type');
|
||||
if (type) data.type = type;
|
||||
const thresholdType = fd.get('thresholdType');
|
||||
if (thresholdType) data.thresholdType = thresholdType;
|
||||
const occurrence = fd.get('occurrence');
|
||||
if (occurrence) data.occurrence = occurrence;
|
||||
const rewardType = fd.get('rewardType');
|
||||
@@ -101,6 +104,7 @@ export const actions = {
|
||||
name: fd.get('name'),
|
||||
target: fd.get('target'),
|
||||
type: fd.get('type'),
|
||||
thresholdType: fd.get('thresholdType') || 'points',
|
||||
occurrence: fd.get('occurrence'),
|
||||
rewardType: fd.get('rewardType'),
|
||||
rewardValue: fd.get('rewardValue'),
|
||||
@@ -136,6 +140,8 @@ export const actions = {
|
||||
if (target) data.target = target;
|
||||
const type = fd.get('type');
|
||||
if (type) data.type = type;
|
||||
const thresholdType = fd.get('thresholdType');
|
||||
if (thresholdType) data.thresholdType = thresholdType;
|
||||
const occurrence = fd.get('occurrence');
|
||||
if (occurrence) data.occurrence = occurrence;
|
||||
const rewardType = fd.get('rewardType');
|
||||
@@ -185,6 +191,7 @@ export const actions = {
|
||||
description: fd.get('description') || '',
|
||||
target: fd.get('target'),
|
||||
type: fd.get('type'),
|
||||
thresholdType: fd.get('thresholdType') || 'points',
|
||||
occurrence: fd.get('occurrence'),
|
||||
rewardType: fd.get('rewardType'),
|
||||
rewardValue: fd.get('rewardValue'),
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
target: 'individual' as string,
|
||||
memberId: '' as string,
|
||||
type: 'threshold' as string,
|
||||
thresholdType: 'points' as string,
|
||||
occurrence: 'recurring' as string,
|
||||
rewardType: 'points' as string,
|
||||
rewardValue: '100',
|
||||
@@ -41,6 +42,7 @@
|
||||
target: 'individual' as string,
|
||||
memberId: '' as string,
|
||||
type: 'threshold' as string,
|
||||
thresholdType: 'points' as string,
|
||||
occurrence: 'recurring' as string,
|
||||
rewardType: 'points' as string,
|
||||
rewardValue: '100',
|
||||
@@ -69,6 +71,8 @@
|
||||
|
||||
let showPeriod = $derived(editVals.occurrence !== 'once');
|
||||
let showCriteria = $derived(editVals.type !== 'manual');
|
||||
let createIsPercent = $derived(createVals.type === 'threshold' && createVals.thresholdType === 'percent');
|
||||
let editIsPercent = $derived(editVals.type === 'threshold' && editVals.thresholdType === 'percent');
|
||||
|
||||
let templateConfigs = $derived(templates as BonusTemplate[]);
|
||||
let allBonusConfigs = $derived(
|
||||
@@ -88,6 +92,15 @@
|
||||
|
||||
const username = $derived(page.params.username);
|
||||
|
||||
// Pocket money: seeded for every child, but the amount (rewardValue) starts
|
||||
// empty until the parent sets it on this page. The admin dashboard links
|
||||
// here with ?s=pocketmoney to surface this mandatory teaching notice.
|
||||
let pocketNotice = $derived(page.url.searchParams.get('s') === 'pocketmoney');
|
||||
let pocketConfigs = $derived(
|
||||
(configs as BonusConfig[]).filter((c: BonusConfig) => c.isPocketMoney)
|
||||
);
|
||||
let hasUnsetPocket = $derived(pocketConfigs.some((c: BonusConfig) => !c.rewardValue));
|
||||
|
||||
function memberName(id: string): string {
|
||||
const m = members.find((m) => m.id === id);
|
||||
return m?.name || 'Unknown';
|
||||
@@ -131,6 +144,7 @@
|
||||
target: targetType,
|
||||
memberId: '',
|
||||
type: tpl.type,
|
||||
thresholdType: tpl.thresholdType || 'points',
|
||||
occurrence: tpl.occurrence,
|
||||
rewardType: tpl.rewardType,
|
||||
rewardValue: tpl.rewardValue,
|
||||
@@ -186,6 +200,7 @@
|
||||
target: cfg.target,
|
||||
memberId: cfg.memberId || '',
|
||||
type: cfg.type,
|
||||
thresholdType: cfg.thresholdType || 'points',
|
||||
occurrence: cfg.occurrence,
|
||||
rewardType: cfg.rewardType,
|
||||
rewardValue: cfg.rewardValue,
|
||||
@@ -205,6 +220,26 @@
|
||||
</script>
|
||||
|
||||
<ViewHeader title="Bonuses" hero />
|
||||
|
||||
{#if pocketNotice}
|
||||
<CardGrid>
|
||||
<Card cols={3} accent={hasUnsetPocket ? '#f59e0b' : '#10b981'}>
|
||||
<div class="pocket-notice">
|
||||
<strong>Pocket Money</strong>
|
||||
{#if hasUnsetPocket}
|
||||
<p>
|
||||
Every child has a <em>Pocket Money</em> droplet. Set how it's awarded — pick the
|
||||
amount (£) and the % of weekly chores they must complete to earn it — then save. Until
|
||||
you set the amount, pocket money is paused for that child.
|
||||
</p>
|
||||
{:else}
|
||||
<p>All Pocket Money droplets are set. Children will earn pocket money as they complete chores.</p>
|
||||
{/if}
|
||||
</div>
|
||||
</Card>
|
||||
</CardGrid>
|
||||
{/if}
|
||||
|
||||
<CardGrid>
|
||||
<Card cols={3}>
|
||||
<div class="template-section">
|
||||
@@ -214,10 +249,6 @@
|
||||
<div class="template-card" draggable="true" ondragstart={(e) => handleDragStart(e, cfg)}>
|
||||
<div class="card-head">
|
||||
<strong>{cfg.name}</strong>
|
||||
<form method="POST" action="?/deleteTemplate" use:enhance>
|
||||
<input type="hidden" name="id" value={cfg.id} />
|
||||
<button type="submit" class="del-btn" title="Delete">×</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="badges">
|
||||
<span class="badge {badgeClass(cfg)}">{cfg.target}</span>
|
||||
@@ -226,6 +257,9 @@
|
||||
{#if cfg.period}
|
||||
<span class="badge period">{cfg.period}</span>
|
||||
{/if}
|
||||
{#if cfg.isPocketMoney}
|
||||
<span class="badge pocket">Pocket Money</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="reward-preview">
|
||||
{formatReward(cfg.rewardType, cfg.rewardValue)}
|
||||
@@ -243,6 +277,7 @@
|
||||
target: cfg.target,
|
||||
memberId: '',
|
||||
type: cfg.type,
|
||||
thresholdType: cfg.thresholdType || 'points',
|
||||
occurrence: cfg.occurrence,
|
||||
rewardType: cfg.rewardType,
|
||||
rewardValue: cfg.rewardValue,
|
||||
@@ -277,6 +312,9 @@
|
||||
<span class="badge {badgeClass(cfg)}">{cfg.target}</span>
|
||||
<span class="badge">{cfg.type}</span>
|
||||
<span class="badge">{cfg.occurrence}</span>
|
||||
{#if cfg.isPocketMoney}
|
||||
<span class="badge pocket">Pocket Money</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#each members as m}
|
||||
@@ -292,7 +330,12 @@
|
||||
{@const criteria = p?.criteriaValue ?? cfg.criteriaValue ?? 0}
|
||||
{@const current = p?.current ?? 0}
|
||||
{@const achieved = p?.achieved ?? false}
|
||||
{@const pct = criteria > 0 ? Math.min(100, (current / criteria) * 100) : 0}
|
||||
{@const isPct = cfg.thresholdType === 'percent'}
|
||||
{@const pct = isPct
|
||||
? Math.min(100, current)
|
||||
: criteria > 0
|
||||
? Math.min(100, (current / criteria) * 100)
|
||||
: 0}
|
||||
<div class="member-progress">
|
||||
<span class="mname" style="color:{m.color}">{m.name}</span>
|
||||
{#if criteria > 0}
|
||||
@@ -345,7 +388,9 @@
|
||||
class="bar-fill"
|
||||
style="width:{Math.min(
|
||||
100,
|
||||
(p.current / Math.max(1, p.criteriaValue)) * 100
|
||||
cfg.thresholdType === 'percent'
|
||||
? p.current
|
||||
: (p.current / Math.max(1, p.criteriaValue)) * 100
|
||||
)}%"
|
||||
class:met={p.achieved}
|
||||
></div>
|
||||
@@ -391,14 +436,16 @@
|
||||
<span class="mname" style="color:{team.memberColor}">{team.memberName}</span>
|
||||
{#if team.criteriaValue > 0}
|
||||
<div class="bar-wrap">
|
||||
<div
|
||||
class="bar-fill"
|
||||
style="width:{Math.min(
|
||||
<div
|
||||
class="bar-fill"
|
||||
style="width:{Math.min(
|
||||
100,
|
||||
(team.current / Math.max(1, team.criteriaValue)) * 100
|
||||
cfg.thresholdType === 'percent'
|
||||
? team.current
|
||||
: (team.current / Math.max(1, team.criteriaValue)) * 100
|
||||
)}%"
|
||||
class:met={team.achieved}
|
||||
></div>
|
||||
class:met={team.achieved}
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
<span class="stat">
|
||||
@@ -449,20 +496,43 @@
|
||||
<option value="manual">Manual</option>
|
||||
</select>
|
||||
</label>
|
||||
{#if showCriteriaForCreate}
|
||||
{#if createVals.type === 'threshold'}
|
||||
<label
|
||||
>Target Value
|
||||
<input
|
||||
type="number"
|
||||
name="criteriaValue"
|
||||
bind:value={createVals.criteriaValue}
|
||||
required
|
||||
/>
|
||||
<span class="hint"
|
||||
>{createVals.type === 'threshold' ? 'Points to reach' : 'Number of chores'}</span
|
||||
>
|
||||
>Threshold by
|
||||
<select name="thresholdType" bind:value={createVals.thresholdType}>
|
||||
<option value="points">Points</option>
|
||||
<option value="percent">% of chores done</option>
|
||||
</select>
|
||||
</label>
|
||||
{/if}
|
||||
{#if showCriteriaForCreate}
|
||||
{#if createIsPercent}
|
||||
<label
|
||||
>Chores completed (%)
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
name="criteriaValue"
|
||||
bind:value={createVals.criteriaValue}
|
||||
/>
|
||||
<span class="hint">{createVals.criteriaValue}% of chores this week</span>
|
||||
</label>
|
||||
{:else}
|
||||
<label
|
||||
>Target Value
|
||||
<input
|
||||
type="number"
|
||||
name="criteriaValue"
|
||||
bind:value={createVals.criteriaValue}
|
||||
required
|
||||
/>
|
||||
<span class="hint"
|
||||
>{createVals.type === 'threshold' ? 'Points to reach' : 'Number of chores'}</span
|
||||
>
|
||||
</label>
|
||||
{/if}
|
||||
{/if}
|
||||
<label
|
||||
>Occurrence
|
||||
<select name="occurrence" bind:value={createVals.occurrence}>
|
||||
@@ -572,24 +642,47 @@
|
||||
<option value="manual">Manual</option>
|
||||
</select>
|
||||
</label>
|
||||
{#if showCriteria}
|
||||
{#if editVals.type === 'threshold'}
|
||||
<label
|
||||
>Target Value
|
||||
<input
|
||||
type="number"
|
||||
name="criteriaValue"
|
||||
bind:value={editVals.criteriaValue}
|
||||
required
|
||||
/>
|
||||
<span class="hint"
|
||||
>{editVals.type === 'threshold'
|
||||
? 'Points to reach'
|
||||
: editVals.type === 'count'
|
||||
? 'Number of chores'
|
||||
: 'N/A'}</span
|
||||
>
|
||||
>Threshold by
|
||||
<select name="thresholdType" bind:value={editVals.thresholdType}>
|
||||
<option value="points">Points</option>
|
||||
<option value="percent">% of chores done</option>
|
||||
</select>
|
||||
</label>
|
||||
{/if}
|
||||
{#if showCriteria}
|
||||
{#if editIsPercent}
|
||||
<label
|
||||
>Chores completed (%)
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
name="criteriaValue"
|
||||
bind:value={editVals.criteriaValue}
|
||||
/>
|
||||
<span class="hint">{editVals.criteriaValue}% of chores this week</span>
|
||||
</label>
|
||||
{:else}
|
||||
<label
|
||||
>Target Value
|
||||
<input
|
||||
type="number"
|
||||
name="criteriaValue"
|
||||
bind:value={editVals.criteriaValue}
|
||||
required
|
||||
/>
|
||||
<span class="hint"
|
||||
>{editVals.type === 'threshold'
|
||||
? 'Points to reach'
|
||||
: editVals.type === 'count'
|
||||
? 'Number of chores'
|
||||
: 'N/A'}</span
|
||||
>
|
||||
</label>
|
||||
{/if}
|
||||
{/if}
|
||||
<label
|
||||
>Occurrence
|
||||
<select name="occurrence" bind:value={editVals.occurrence}>
|
||||
@@ -731,7 +824,18 @@
|
||||
.template-list {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
margin-bottom: 0.75rem;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.template-list {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.template-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
.template-card {
|
||||
background: white;
|
||||
@@ -804,6 +908,23 @@
|
||||
.badge.manual {
|
||||
background: #f3e8ff;
|
||||
}
|
||||
.badge.pocket {
|
||||
background: #fde68a;
|
||||
color: #92400e;
|
||||
font-weight: 600;
|
||||
}
|
||||
.pocket-notice strong {
|
||||
display: block;
|
||||
margin-bottom: 0.35rem;
|
||||
font-size: 0.95rem;
|
||||
color: #1f2937;
|
||||
}
|
||||
.pocket-notice p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
color: #6b7280;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.reward-preview {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
|
||||
@@ -334,21 +334,6 @@
|
||||
<button onclick={() => openEditTemplate(t)} class="edit-btn" title="Edit template"
|
||||
>✎</button
|
||||
>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteTemplate"
|
||||
use:enhance={() => {
|
||||
return async ({ result, formData }) => {
|
||||
if (result.type === 'success') {
|
||||
const id = formData.get('id');
|
||||
templates = templates.filter((t) => t.id !== id) as ChoreTemplate[];
|
||||
}
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="id" value={t.id} />
|
||||
<button type="submit" class="del-btn" title="Delete template">×</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
@@ -542,7 +527,7 @@
|
||||
<div class="modal" onclick={(e) => e.stopPropagation()} role="dialog">
|
||||
<h3>Edit Template: {editingTemplate.name}</h3>
|
||||
<p class="hint">Changes will also update all existing assigned chores using this template.</p>
|
||||
<form method="POST" action="?/updateTemplate" use:enhance={enhanceUpdateTemplate}>
|
||||
<form id="updateTplForm" method="POST" action="?/updateTemplate" use:enhance={enhanceUpdateTemplate}>
|
||||
<input name="id" type="hidden" value={editingTemplate.id} />
|
||||
<label>
|
||||
Name
|
||||
@@ -566,11 +551,35 @@
|
||||
Default Value
|
||||
<input name="defaultValue" type="number" step="any" bind:value={editTplValue} required />
|
||||
</label>
|
||||
<div class="modal-actions">
|
||||
<button type="button" onclick={closeEditTemplate}>Cancel</button>
|
||||
<button type="submit">Save</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="modal-actions">
|
||||
<button type="button" onclick={closeEditTemplate}>Cancel</button>
|
||||
<form
|
||||
method="POST"
|
||||
action="?/deleteTemplate"
|
||||
use:enhance={() => {
|
||||
return async ({ result, formData }) => {
|
||||
if (result.type === 'success') {
|
||||
const id = formData.get('id');
|
||||
templates = templates.filter((t) => t.id !== id) as ChoreTemplate[];
|
||||
closeEditTemplate();
|
||||
}
|
||||
};
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="id" value={editingTemplate.id} />
|
||||
<button
|
||||
type="submit"
|
||||
class="delete-btn"
|
||||
onclick={(e) => {
|
||||
if (!confirm('Delete this template? This cannot be undone.')) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}}>Delete</button
|
||||
>
|
||||
</form>
|
||||
<button type="submit" form="updateTplForm">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -780,7 +789,17 @@
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.75rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.template-list {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.template-list {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Kanban (3 columns, scrolls via Card scrollX) ── */
|
||||
@@ -1057,6 +1076,10 @@
|
||||
background: #6366f1;
|
||||
color: white;
|
||||
}
|
||||
.modal-actions button.delete-btn {
|
||||
background: #dc2626;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* ── Accordion wrappers ── */
|
||||
.accordion-wrapper {
|
||||
|
||||
Reference in New Issue
Block a user