dynamic chore counts

This commit is contained in:
JCEEE
2026-09-07 20:53:37 +01:00
parent f2d6f8368c
commit 2abc9788a2
6 changed files with 155 additions and 38 deletions
+14 -5
View File
@@ -438,6 +438,7 @@ async function ensureAssignedChoreFields(): Promise<void> {
const col = await getCollection('assigned_chores');
if (!col) return;
const has = (n: string) => col.fields.some((f: any) => f.name === n);
const fields = [...col.fields];
const needed: any[] = [];
if (!has('description')) needed.push({ name: 'description', 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 });
// Shared chores: memberId becomes optional, shared boolean tracks original mode
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.
let changed = !!needed.length;
const typeField = col.fields.find((f: any) => f.name === 'type');
const typeField = fields.find((f: any) => f.name === 'type');
if (typeField && Array.isArray(typeField.values) && !typeField.values.includes('emoji')) {
typeField.values = [...typeField.values, 'emoji'];
changed = true;
}
// 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) {
tplField.required = false;
changed = true;
}
// 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) {
memberField.required = false;
changed = true;
}
if (changed) {
await updateCollection(col.id, { ...col, fields: [...col.fields] });
await updateCollection(col.id, { ...col, fields });
}
const comp = await getCollection('completions');
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 });
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.
const rv = fields.find((f: any) => f.name === 'rewardValue');
if (rv && rv.required) {
+22 -23
View File
@@ -24,6 +24,13 @@ function claimableStamp(cfg: any, payday: number, tz: string) {
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) {
let configs: any[] = [];
try {
@@ -58,15 +65,7 @@ export async function evaluateFam(pb: any, famId: string) {
// Pocket money pauses until the parent sets an amount.
if (cfg.isPocketMoney && !cfg.rewardValue) continue;
const win = bonusWindow(cfg, paydayEval, tzEval);
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 periodCompletions = allCompletions.filter((c: any) => completionInWindow(c, win));
const existingRewards = allRewards.filter((r: any) => r.bonusConfigId === cfg.id);
let createdReward = false;
@@ -94,7 +93,9 @@ export async function evaluateFam(pb: any, famId: string) {
if (cfg.target === 'individual') {
const targetMembers = cfg.memberId ? allMembers.filter((m: any) => m.id === cfg.memberId) : allMembers;
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;
if (cfg.type === 'threshold') {
if (cfg.thresholdType === 'percent') {
@@ -131,7 +132,8 @@ export async function evaluateFam(pb: any, famId: string) {
}
} else if (cfg.target === 'collaborative') {
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;
if (cfg.type === 'threshold') {
if (cfg.thresholdType === 'percent') {
@@ -166,7 +168,9 @@ export async function evaluateFam(pb: any, famId: string) {
}
} else if (cfg.target === 'competitive') {
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;
if (cfg.type === 'threshold') {
if (cfg.thresholdType === 'percent') {
@@ -249,22 +253,15 @@ export async function progress(pb: any, famId: string) {
const win = bonusWindow(cfg, payday, tz);
const pStart = win.from;
const pEnd = win.to;
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 periodCompletions = completionsList.filter((c: any) => completionInWindow(c, win));
const progressRows: any[] = [];
if (cfg.target === 'collaborative') {
const teamCompletions = periodCompletions.filter((c: any) =>
let teamCompletions = periodCompletions.filter((c: any) =>
members.some((m: any) => m.id === c.memberId)
);
if (cfg.targetChoreId) teamCompletions = teamCompletions.filter((c: any) => c.assignedChoreId === cfg.targetChoreId);
let teamCurrent = 0;
if (cfg.type === 'threshold') {
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;
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);
let current = 0;