dynamic chore counts
This commit is contained in:
@@ -438,6 +438,7 @@ async function ensureAssignedChoreFields(): Promise<void> {
|
|||||||
const col = await getCollection('assigned_chores');
|
const col = await getCollection('assigned_chores');
|
||||||
if (!col) return;
|
if (!col) return;
|
||||||
const has = (n: string) => col.fields.some((f: any) => f.name === n);
|
const has = (n: string) => col.fields.some((f: any) => f.name === n);
|
||||||
|
const fields = [...col.fields];
|
||||||
const needed: any[] = [];
|
const needed: any[] = [];
|
||||||
if (!has('description')) needed.push({ name: 'description', type: 'text', required: false });
|
if (!has('description')) needed.push({ name: 'description', type: 'text', required: false });
|
||||||
if (!has('icon')) needed.push({ name: 'icon', type: 'text', required: false });
|
if (!has('icon')) needed.push({ name: 'icon', type: 'text', required: false });
|
||||||
@@ -445,27 +446,31 @@ async function ensureAssignedChoreFields(): Promise<void> {
|
|||||||
if (!has('emoji')) needed.push({ name: 'emoji', type: 'text', required: false });
|
if (!has('emoji')) needed.push({ name: 'emoji', type: 'text', required: false });
|
||||||
// Shared chores: memberId becomes optional, shared boolean tracks original mode
|
// Shared chores: memberId becomes optional, shared boolean tracks original mode
|
||||||
if (!has('shared')) needed.push({ name: 'shared', type: 'bool', required: false });
|
if (!has('shared')) needed.push({ name: 'shared', type: 'bool', required: false });
|
||||||
|
let changed = false;
|
||||||
|
if (needed.length) {
|
||||||
|
fields.push(...needed);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
// Todos can be celebration-only (emoji) as well as points/money.
|
// Todos can be celebration-only (emoji) as well as points/money.
|
||||||
let changed = !!needed.length;
|
const typeField = fields.find((f: any) => f.name === 'type');
|
||||||
const typeField = col.fields.find((f: any) => f.name === 'type');
|
|
||||||
if (typeField && Array.isArray(typeField.values) && !typeField.values.includes('emoji')) {
|
if (typeField && Array.isArray(typeField.values) && !typeField.values.includes('emoji')) {
|
||||||
typeField.values = [...typeField.values, 'emoji'];
|
typeField.values = [...typeField.values, 'emoji'];
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
// Direct todos (created by a parent, not from a template) have no templateId.
|
// Direct todos (created by a parent, not from a template) have no templateId.
|
||||||
const tplField = col.fields.find((f: any) => f.name === 'templateId');
|
const tplField = fields.find((f: any) => f.name === 'templateId');
|
||||||
if (tplField && tplField.required) {
|
if (tplField && tplField.required) {
|
||||||
tplField.required = false;
|
tplField.required = false;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
// memberId must be optional for shared chores (null = shared with all members)
|
// memberId must be optional for shared chores (null = shared with all members)
|
||||||
const memberField = col.fields.find((f: any) => f.name === 'memberId');
|
const memberField = fields.find((f: any) => f.name === 'memberId');
|
||||||
if (memberField && memberField.required) {
|
if (memberField && memberField.required) {
|
||||||
memberField.required = false;
|
memberField.required = false;
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
await updateCollection(col.id, { ...col, fields: [...col.fields] });
|
await updateCollection(col.id, { ...col, fields });
|
||||||
}
|
}
|
||||||
const comp = await getCollection('completions');
|
const comp = await getCollection('completions');
|
||||||
if (comp && !comp.fields.some((f: any) => f.name === 'rewardId')) {
|
if (comp && !comp.fields.some((f: any) => f.name === 'rewardId')) {
|
||||||
@@ -526,6 +531,10 @@ async function ensureBonusFields(): Promise<void> {
|
|||||||
fields.push({ name: 'targetChoreId', type: 'text', required: false });
|
fields.push({ name: 'targetChoreId', type: 'text', required: false });
|
||||||
changed = true;
|
changed = true;
|
||||||
}
|
}
|
||||||
|
if (name === 'bonus_configs' && !has('targetChoreIds')) {
|
||||||
|
fields.push({ name: 'targetChoreIds', type: 'json', 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) {
|
||||||
|
|||||||
@@ -24,6 +24,13 @@ function claimableStamp(cfg: any, payday: number, tz: string) {
|
|||||||
return { claimable: 'payday', settleDate: nextPaydayAfter(end, payday, tz) };
|
return { claimable: 'payday', settleDate: nextPaydayAfter(end, payday, tz) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function targetChoreFor(cfg: any, memberId: string): string | undefined {
|
||||||
|
if (cfg.targetChoreIds && typeof cfg.targetChoreIds === 'object' && cfg.targetChoreIds[memberId]) {
|
||||||
|
return cfg.targetChoreIds[memberId];
|
||||||
|
}
|
||||||
|
return cfg.targetChoreId || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export async function evaluateFam(pb: any, famId: string) {
|
export async function evaluateFam(pb: any, famId: string) {
|
||||||
let configs: any[] = [];
|
let configs: any[] = [];
|
||||||
try {
|
try {
|
||||||
@@ -58,15 +65,7 @@ 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);
|
||||||
let periodCompletions = allCompletions.filter((c: any) => completionInWindow(c, win));
|
const 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;
|
||||||
@@ -94,7 +93,9 @@ export async function evaluateFam(pb: any, famId: string) {
|
|||||||
if (cfg.target === 'individual') {
|
if (cfg.target === 'individual') {
|
||||||
const targetMembers = cfg.memberId ? allMembers.filter((m: any) => m.id === cfg.memberId) : allMembers;
|
const targetMembers = cfg.memberId ? allMembers.filter((m: any) => m.id === cfg.memberId) : allMembers;
|
||||||
for (const m of targetMembers) {
|
for (const m of targetMembers) {
|
||||||
const memberCompletions = periodCompletions.filter((c: any) => c.memberId === m.id);
|
const memberTarget = targetChoreFor(cfg, m.id);
|
||||||
|
let memberCompletions = periodCompletions.filter((c: any) => c.memberId === m.id);
|
||||||
|
if (memberTarget) memberCompletions = memberCompletions.filter((c: any) => c.assignedChoreId === memberTarget);
|
||||||
let current = 0;
|
let current = 0;
|
||||||
if (cfg.type === 'threshold') {
|
if (cfg.type === 'threshold') {
|
||||||
if (cfg.thresholdType === 'percent') {
|
if (cfg.thresholdType === 'percent') {
|
||||||
@@ -131,7 +132,8 @@ export async function evaluateFam(pb: any, famId: string) {
|
|||||||
}
|
}
|
||||||
} else if (cfg.target === 'collaborative') {
|
} else if (cfg.target === 'collaborative') {
|
||||||
const allMemberIds = allMembers.map((m: any) => m.id);
|
const allMemberIds = allMembers.map((m: any) => m.id);
|
||||||
const teamCompletions = periodCompletions.filter((c: any) => allMemberIds.includes(c.memberId));
|
let teamCompletions = periodCompletions.filter((c: any) => allMemberIds.includes(c.memberId));
|
||||||
|
if (cfg.targetChoreId) teamCompletions = teamCompletions.filter((c: any) => c.assignedChoreId === cfg.targetChoreId);
|
||||||
let total = 0;
|
let total = 0;
|
||||||
if (cfg.type === 'threshold') {
|
if (cfg.type === 'threshold') {
|
||||||
if (cfg.thresholdType === 'percent') {
|
if (cfg.thresholdType === 'percent') {
|
||||||
@@ -166,7 +168,9 @@ export async function evaluateFam(pb: any, famId: string) {
|
|||||||
}
|
}
|
||||||
} else if (cfg.target === 'competitive') {
|
} else if (cfg.target === 'competitive') {
|
||||||
const scored = allMembers.map((m: any) => {
|
const scored = allMembers.map((m: any) => {
|
||||||
const memberCompletions = periodCompletions.filter((c: any) => c.memberId === m.id);
|
const memberTarget = targetChoreFor(cfg, m.id);
|
||||||
|
let memberCompletions = periodCompletions.filter((c: any) => c.memberId === m.id);
|
||||||
|
if (memberTarget) memberCompletions = memberCompletions.filter((c: any) => c.assignedChoreId === memberTarget);
|
||||||
let current = 0;
|
let current = 0;
|
||||||
if (cfg.type === 'threshold') {
|
if (cfg.type === 'threshold') {
|
||||||
if (cfg.thresholdType === 'percent') {
|
if (cfg.thresholdType === 'percent') {
|
||||||
@@ -249,22 +253,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;
|
||||||
let periodCompletions = completionsList.filter((c: any) => completionInWindow(c, win));
|
const 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[] = [];
|
||||||
|
|
||||||
if (cfg.target === 'collaborative') {
|
if (cfg.target === 'collaborative') {
|
||||||
const teamCompletions = periodCompletions.filter((c: any) =>
|
let teamCompletions = periodCompletions.filter((c: any) =>
|
||||||
members.some((m: any) => m.id === c.memberId)
|
members.some((m: any) => m.id === c.memberId)
|
||||||
);
|
);
|
||||||
|
if (cfg.targetChoreId) teamCompletions = teamCompletions.filter((c: any) => c.assignedChoreId === cfg.targetChoreId);
|
||||||
let teamCurrent = 0;
|
let teamCurrent = 0;
|
||||||
if (cfg.type === 'threshold') {
|
if (cfg.type === 'threshold') {
|
||||||
if (cfg.thresholdType === 'percent') {
|
if (cfg.thresholdType === 'percent') {
|
||||||
@@ -297,7 +294,9 @@ export async function progress(pb: any, famId: string) {
|
|||||||
? members.filter((m: any) => m.id === cfg.memberId)
|
? members.filter((m: any) => m.id === cfg.memberId)
|
||||||
: members;
|
: members;
|
||||||
for (const m of progressMembers) {
|
for (const m of progressMembers) {
|
||||||
const memberCompletions = periodCompletions.filter((c: any) => c.memberId === m.id);
|
const memberTarget = targetChoreFor(cfg, m.id);
|
||||||
|
let memberCompletions = periodCompletions.filter((c: any) => c.memberId === m.id);
|
||||||
|
if (memberTarget) memberCompletions = memberCompletions.filter((c: any) => c.assignedChoreId === memberTarget);
|
||||||
const memberReward = cfgRewards.find((r: any) => r.memberId === m.id);
|
const memberReward = cfgRewards.find((r: any) => r.memberId === m.id);
|
||||||
|
|
||||||
let current = 0;
|
let current = 0;
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ export interface BonusConfig {
|
|||||||
period?: BonusPeriod;
|
period?: BonusPeriod;
|
||||||
status: BonusStatus;
|
status: BonusStatus;
|
||||||
targetChoreId?: string;
|
targetChoreId?: string;
|
||||||
|
targetChoreIds?: Record<string, string>;
|
||||||
isPocketMoney?: boolean;
|
isPocketMoney?: boolean;
|
||||||
completeBy?: 'unlimited' | 'week' | 'custom';
|
completeBy?: 'unlimited' | 'week' | 'custom';
|
||||||
startDate?: string;
|
startDate?: string;
|
||||||
|
|||||||
@@ -1,6 +1,24 @@
|
|||||||
import { fail, redirect } from '@sveltejs/kit';
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
import { servicesFor } from '$lib/server/servicesFor';
|
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) {
|
export async function load(event) {
|
||||||
if (!event.locals.user) throw redirect(303, '/login');
|
if (!event.locals.user) throw redirect(303, '/login');
|
||||||
const famId = event.locals.user.famId;
|
const famId = event.locals.user.famId;
|
||||||
@@ -117,6 +135,9 @@ export const actions = {
|
|||||||
if (data.occurrence === 'once') data.period = '';
|
if (data.occurrence === 'once') data.period = '';
|
||||||
const targetChoreId = fd.get('targetChoreId');
|
const targetChoreId = fd.get('targetChoreId');
|
||||||
if (targetChoreId !== null) data.targetChoreId = 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');
|
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');
|
||||||
@@ -163,6 +184,9 @@ export const actions = {
|
|||||||
if (occurrence === 'once') data.period = '';
|
if (occurrence === 'once') data.period = '';
|
||||||
const targetChoreId = fd.get('targetChoreId');
|
const targetChoreId = fd.get('targetChoreId');
|
||||||
if (targetChoreId !== null) data.targetChoreId = 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');
|
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');
|
||||||
@@ -221,6 +245,9 @@ export const actions = {
|
|||||||
startDate: fd.get('startDate') || '',
|
startDate: fd.get('startDate') || '',
|
||||||
completeByDate: fd.get('completeByDate') || ''
|
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 = '';
|
if (data.occurrence === 'once') data.period = '';
|
||||||
try {
|
try {
|
||||||
const s = servicesFor(event);
|
const s = servicesFor(event);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
BonusConfig,
|
BonusConfig,
|
||||||
BonusTemplate,
|
BonusTemplate,
|
||||||
Member,
|
Member,
|
||||||
|
AssignedChore,
|
||||||
BonusConfigWithProgress,
|
BonusConfigWithProgress,
|
||||||
BonusProgress,
|
BonusProgress,
|
||||||
Reward
|
Reward
|
||||||
@@ -90,7 +91,8 @@
|
|||||||
completeBy: 'unlimited' as string,
|
completeBy: 'unlimited' as string,
|
||||||
startDate: '' as string,
|
startDate: '' as string,
|
||||||
completeByDate: '' as string,
|
completeByDate: '' as string,
|
||||||
targetChoreId: '' as string
|
targetChoreId: '' as string,
|
||||||
|
targetChoreIds: {} as Record<string, string>
|
||||||
});
|
});
|
||||||
|
|
||||||
let configs = $derived(
|
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 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[]
|
||||||
@@ -204,7 +225,8 @@
|
|||||||
completeBy: 'unlimited',
|
completeBy: 'unlimited',
|
||||||
startDate: todayIso(),
|
startDate: todayIso(),
|
||||||
completeByDate: '',
|
completeByDate: '',
|
||||||
targetChoreId: ''
|
targetChoreId: '',
|
||||||
|
targetChoreIds: {}
|
||||||
};
|
};
|
||||||
step = 1;
|
step = 1;
|
||||||
showEditModal = true;
|
showEditModal = true;
|
||||||
@@ -241,6 +263,12 @@
|
|||||||
function openEditConfig(cfg: BonusConfig) {
|
function openEditConfig(cfg: BonusConfig) {
|
||||||
editingConfig = cfg;
|
editingConfig = cfg;
|
||||||
creatingFromTemplate = null;
|
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 = {
|
editVals = {
|
||||||
name: cfg.name,
|
name: cfg.name,
|
||||||
description: cfg.description || '',
|
description: cfg.description || '',
|
||||||
@@ -257,7 +285,8 @@
|
|||||||
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 || ''
|
targetChoreId: cfg.targetChoreId || '',
|
||||||
|
targetChoreIds: targetIds
|
||||||
};
|
};
|
||||||
step = 1;
|
step = 1;
|
||||||
showEditModal = true;
|
showEditModal = true;
|
||||||
@@ -389,7 +418,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{#if cfg.type === 'count'}
|
{#if cfg.type === 'count'}
|
||||||
<span class="count-progress">
|
<span class="count-progress">
|
||||||
{Math.min(current, criteria)} / {criteria}{cfg.targetChoreId ? ' chores' : ''}
|
{Math.min(current, criteria)} / {criteria}{hasTargetChore(cfg) ? ' chores' : ''}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class="stat {p?.state ?? 'pending'}">
|
<span class="stat {p?.state ?? 'pending'}">
|
||||||
@@ -682,23 +711,74 @@
|
|||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
{/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>
|
<label>
|
||||||
<span class="field-label"><Target size={14} /> Target Chore</span>
|
<span class="field-label"><Target size={14} /> Target Chore</span>
|
||||||
<select name="targetChoreId" bind:value={editVals.targetChoreId}>
|
<select name="targetChoreId" bind:value={editVals.targetChoreId}>
|
||||||
<option value="">All Chores</option>
|
<option value="">All Chores</option>
|
||||||
{#each targetChoreOptions as ac}
|
{#each targetChoreOptions as ac}
|
||||||
<option value={ac.id}>
|
<option value={ac.id}>{choreName(ac)}</option>
|
||||||
{ac.customName || famStore.templateMap().get(ac.templateId || '')?.name || 'Chore'}
|
|
||||||
</option>
|
|
||||||
{/each}
|
{/each}
|
||||||
</select>
|
</select>
|
||||||
<span class="hint"
|
<span class="hint"
|
||||||
>Count completions of a specific chore only. Leave as "All Chores" to count
|
>Count completions of a specific chore only. Leave as "All Chores" to
|
||||||
every assigned chore.</span
|
count every assigned chore.</span
|
||||||
>
|
>
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
|
{/if}
|
||||||
{#if !isPocketMoney}
|
{#if !isPocketMoney}
|
||||||
<label>
|
<label>
|
||||||
<span class="field-label"><Settings2 size={14} /> Occurrence</span>
|
<span class="field-label"><Settings2 size={14} /> Occurrence</span>
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ export const SCHEMA_PLAN: CollectionPlanEntry[] = [
|
|||||||
select("period", ["schedule", "daily", "weekly", "monthly"]),
|
select("period", ["schedule", "daily", "weekly", "monthly"]),
|
||||||
select("status", ["active", "completed"], true),
|
select("status", ["active", "completed"], true),
|
||||||
text("targetChoreId"),
|
text("targetChoreId"),
|
||||||
|
jsonField("targetChoreIds"),
|
||||||
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