dynamic chore counts
This commit is contained in:
@@ -1,6 +1,24 @@
|
||||
import { fail, redirect } from '@sveltejs/kit';
|
||||
import { servicesFor } from '$lib/server/servicesFor';
|
||||
|
||||
// Per-member target-chore picks come in as `targetChore_<memberId>` form fields.
|
||||
// Build the memberId → assignedChoreId map (empty values = count all of that
|
||||
// member's chores, so they're dropped). `used` tells callers whether the
|
||||
// per-member select UI was rendered at all (i.e. an individual-target reward),
|
||||
// so the old scalar targetChoreId can be cleared on save.
|
||||
function parseTargetChoreIds(fd: FormData): { map: Record<string, string>; used: boolean } {
|
||||
const map: Record<string, string> = {};
|
||||
let used = false;
|
||||
for (const [k, v] of fd.entries()) {
|
||||
if (k.startsWith('targetChore_')) {
|
||||
used = true;
|
||||
const memberId = k.slice('targetChore_'.length);
|
||||
if (v) map[memberId] = String(v);
|
||||
}
|
||||
}
|
||||
return { map, used };
|
||||
}
|
||||
|
||||
export async function load(event) {
|
||||
if (!event.locals.user) throw redirect(303, '/login');
|
||||
const famId = event.locals.user.famId;
|
||||
@@ -117,6 +135,9 @@ export const actions = {
|
||||
if (data.occurrence === 'once') data.period = '';
|
||||
const targetChoreId = fd.get('targetChoreId');
|
||||
if (targetChoreId !== null) data.targetChoreId = targetChoreId || '';
|
||||
const { map: targetChoreIds, used: perMemberTargets } = parseTargetChoreIds(fd);
|
||||
if (Object.keys(targetChoreIds).length) data.targetChoreIds = targetChoreIds;
|
||||
if (perMemberTargets) data.targetChoreId = '';
|
||||
const description = fd.get('description');
|
||||
if (description) data.description = description;
|
||||
const memberId = fd.get('memberId');
|
||||
@@ -163,6 +184,9 @@ export const actions = {
|
||||
if (occurrence === 'once') data.period = '';
|
||||
const targetChoreId = fd.get('targetChoreId');
|
||||
if (targetChoreId !== null) data.targetChoreId = targetChoreId || '';
|
||||
const { map: targetChoreIds, used: perMemberTargets } = parseTargetChoreIds(fd);
|
||||
if (Object.keys(targetChoreIds).length) data.targetChoreIds = targetChoreIds;
|
||||
if (perMemberTargets) data.targetChoreId = '';
|
||||
const description = fd.get('description');
|
||||
if (description) data.description = description;
|
||||
const memberId = fd.get('memberId');
|
||||
@@ -221,6 +245,9 @@ export const actions = {
|
||||
startDate: fd.get('startDate') || '',
|
||||
completeByDate: fd.get('completeByDate') || ''
|
||||
};
|
||||
const { map: targetChoreIds, used: perMemberTargets } = parseTargetChoreIds(fd);
|
||||
if (Object.keys(targetChoreIds).length) data.targetChoreIds = targetChoreIds;
|
||||
if (perMemberTargets) data.targetChoreId = '';
|
||||
if (data.occurrence === 'once') data.period = '';
|
||||
try {
|
||||
const s = servicesFor(event);
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
BonusConfig,
|
||||
BonusTemplate,
|
||||
Member,
|
||||
AssignedChore,
|
||||
BonusConfigWithProgress,
|
||||
BonusProgress,
|
||||
Reward
|
||||
@@ -90,7 +91,8 @@
|
||||
completeBy: 'unlimited' as string,
|
||||
startDate: '' as string,
|
||||
completeByDate: '' as string,
|
||||
targetChoreId: '' as string
|
||||
targetChoreId: '' as string,
|
||||
targetChoreIds: {} as Record<string, string>
|
||||
});
|
||||
|
||||
let configs = $derived(
|
||||
@@ -125,6 +127,25 @@
|
||||
)
|
||||
);
|
||||
|
||||
// Chores assigned to a particular member (plus shared chores, which every
|
||||
// member can complete). Used for the dynamic per-member target-chore
|
||||
// dropdowns when a count:chores reward is set to target "All (everyone)".
|
||||
function choresForMember(memberId: string): AssignedChore[] {
|
||||
return famStore.assigned.filter((a) => !a.isTodo && (!a.memberId || a.memberId === memberId));
|
||||
}
|
||||
|
||||
function choreName(ac: AssignedChore): string {
|
||||
return ac.customName || famStore.templateMap().get(ac.templateId || '')?.name || 'Chore';
|
||||
}
|
||||
|
||||
function hasTargetChore(cfg: BonusConfig): boolean {
|
||||
return !!cfg.targetChoreId || Object.values(cfg.targetChoreIds || {}).some(Boolean);
|
||||
}
|
||||
|
||||
function editTargetChore(memberId: string, value: string) {
|
||||
editVals.targetChoreIds = { ...editVals.targetChoreIds, [memberId]: value };
|
||||
}
|
||||
|
||||
let templateConfigs = $derived((templates as BonusTemplate[]).filter((t) => t.global));
|
||||
let allBonusConfigs = $derived(
|
||||
configs.filter((c: BonusConfig) => c.status !== 'completed') as BonusConfig[]
|
||||
@@ -204,7 +225,8 @@
|
||||
completeBy: 'unlimited',
|
||||
startDate: todayIso(),
|
||||
completeByDate: '',
|
||||
targetChoreId: ''
|
||||
targetChoreId: '',
|
||||
targetChoreIds: {}
|
||||
};
|
||||
step = 1;
|
||||
showEditModal = true;
|
||||
@@ -241,6 +263,12 @@
|
||||
function openEditConfig(cfg: BonusConfig) {
|
||||
editingConfig = cfg;
|
||||
creatingFromTemplate = null;
|
||||
const targetIds = (cfg.targetChoreIds as Record<string, string> | undefined) || {};
|
||||
// Legacy individual rewards pinned a chore via the scalar targetChoreId —
|
||||
// surface that in the per-member dropdown too.
|
||||
if (cfg.target === 'individual' && cfg.memberId && cfg.targetChoreId && !targetIds[cfg.memberId]) {
|
||||
targetIds[cfg.memberId] = cfg.targetChoreId;
|
||||
}
|
||||
editVals = {
|
||||
name: cfg.name,
|
||||
description: cfg.description || '',
|
||||
@@ -257,7 +285,8 @@
|
||||
completeBy: cfg.completeBy || 'unlimited',
|
||||
startDate: cfg.startDate || todayIso(),
|
||||
completeByDate: cfg.completeByDate || '',
|
||||
targetChoreId: cfg.targetChoreId || ''
|
||||
targetChoreId: cfg.targetChoreId || '',
|
||||
targetChoreIds: targetIds
|
||||
};
|
||||
step = 1;
|
||||
showEditModal = true;
|
||||
@@ -389,7 +418,7 @@
|
||||
{/if}
|
||||
{#if cfg.type === 'count'}
|
||||
<span class="count-progress">
|
||||
{Math.min(current, criteria)} / {criteria}{cfg.targetChoreId ? ' chores' : ''}
|
||||
{Math.min(current, criteria)} / {criteria}{hasTargetChore(cfg) ? ' chores' : ''}
|
||||
</span>
|
||||
{/if}
|
||||
<span class="stat {p?.state ?? 'pending'}">
|
||||
@@ -682,23 +711,74 @@
|
||||
</label>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if editVals.type === 'count'}
|
||||
{#if editVals.type === 'count'}
|
||||
{#if editVals.target === 'individual'}
|
||||
{#if editVals.memberId}
|
||||
{@const m = members.find((x) => x.id === editVals.memberId)}
|
||||
{@const chores = choresForMember(editVals.memberId)}
|
||||
<label>
|
||||
<span class="field-label"
|
||||
><Target size={14} /> Target Chore
|
||||
{m?.name ? `(${m.name})` : ''}</span
|
||||
>
|
||||
<select
|
||||
name="targetChore_{editVals.memberId}"
|
||||
value={editVals.targetChoreIds[editVals.memberId] || ''}
|
||||
onchange={(e) => editTargetChore(editVals.memberId!, e.currentTarget.value)}
|
||||
>
|
||||
<option value="">All of {m?.name || "this member"}’s chores</option>
|
||||
{#each chores as ac}
|
||||
<option value={ac.id}>{choreName(ac)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span class="hint"
|
||||
>Count completions of a specific chore for {m?.name || 'this member'} only.
|
||||
Leave as "All..." to count every chore they’re assigned.</span
|
||||
>
|
||||
</label>
|
||||
{:else}
|
||||
<p class="hint member-target-hint">
|
||||
Pick a target chore per member — each child counts their own assigned
|
||||
chores.
|
||||
</p>
|
||||
{#each members as m}
|
||||
{@const chores = choresForMember(m.id)}
|
||||
<label>
|
||||
<span class="field-label"
|
||||
><UsersRound size={14} /> {m.name} — Target Chore</span
|
||||
>
|
||||
<select
|
||||
name="targetChore_{m.id}"
|
||||
value={editVals.targetChoreIds[m.id] || ''}
|
||||
onchange={(e) => editTargetChore(m.id, e.currentTarget.value)}
|
||||
>
|
||||
<option value="">All of {m.name}’s chores</option>
|
||||
{#each chores as ac}
|
||||
<option value={ac.id}>{choreName(ac)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</label>
|
||||
{/each}
|
||||
{#if members.length === 0}
|
||||
<p class="empty">No members yet</p>
|
||||
{/if}
|
||||
{/if}
|
||||
{:else}
|
||||
<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>
|
||||
<option value={ac.id}>{choreName(ac)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<span class="hint"
|
||||
>Count completions of a specific chore only. Leave as "All Chores" to count
|
||||
every assigned chore.</span
|
||||
>Count completions of a specific chore only. Leave as "All Chores" to
|
||||
count every assigned chore.</span
|
||||
>
|
||||
</label>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !isPocketMoney}
|
||||
<label>
|
||||
<span class="field-label"><Settings2 size={14} /> Occurrence</span>
|
||||
|
||||
Reference in New Issue
Block a user