fix bonus completeions

This commit is contained in:
JCEEE
2026-09-14 13:00:13 +01:00
parent e441c3d2a7
commit da6a6e372c
7 changed files with 60 additions and 12 deletions
+11
View File
@@ -589,6 +589,17 @@ async function ensureBonusFields(): Promise<void> {
fields.push({ name: 'targetChoreIds', type: 'json', required: false });
changed = true;
}
// Completion audit (bonus_configs only): who/when a goal was finished —
// stamped by the manual "Mark as complete" action or automatically when
// a `once` goal's reward is approved.
if (name === 'bonus_configs' && !has('completedBy')) {
fields.push({ name: 'completedBy', type: 'text', required: false });
changed = true;
}
if (name === 'bonus_configs' && !has('completedAt')) {
fields.push({ name: 'completedAt', type: 'date', 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) {
+13 -1
View File
@@ -252,8 +252,20 @@ export async function evaluateFam(pb: any, famId: string) {
}
if (cfg.occurrence === 'once' && (existingRewards.length > 0 || createdReward)) {
// Status write via superuser: evaluate often runs as a child token
// (fire-and-forget after a chore toggle) and bonus_configs updates
// are parent-only — the user-token write 403s and the config stays
// `active` forever. Stamp who earned it + when.
const earner =
existingRewards.find((r: any) => r.memberId)?.memberId ||
allMembers[0]?.id ||
'auto';
try {
await pb.collection('bonus_configs').update(cfg.id, { status: 'completed' });
await pbAdmin.update('bonus_configs', cfg.id, {
status: 'completed',
completedAt: new Date().toISOString().slice(0, 10),
completedBy: earner
});
} catch {}
}
}
+5 -1
View File
@@ -79,7 +79,11 @@ export function createServices(
return pb.collection('bonus_configs').update(configId, updates);
},
complete: (famId: string, configId: string) =>
pb.collection('bonus_configs').update(configId, { status: 'completed' }),
pb.collection('bonus_configs').update(configId, {
status: 'completed',
completedAt: new Date().toISOString().slice(0, 10),
completedBy: 'admin'
}),
destroy: (famId: string, configId: string) => pb.collection('bonus_configs').delete(configId)
},
settings: {
+26 -2
View File
@@ -1,10 +1,31 @@
import { todayInTz, resolveTz } from '@shared/timezone';
import { famMeta } from './fam';
import { pbAdmin } from '$lib/server/pocketbase';
function resolveServerTz(tz?: string): string {
return resolveTz(tz || 'auto');
}
// A `once` goal is finished the moment its reward is approved — complete the
// config here (not just in evaluateFam, which only runs on chore toggles) so
// the child dashboard stops offering it even before the next toggle.
// Config write via superuser: bonus_configs updates are parent-only.
// Outstanding (unclaimed/requested) rewards are untouched — they stay visible
// and collectible; only the approval moment completes the goal.
async function completeOnceConfig(famId: string, reward: any) {
if (!reward?.bonusConfigId) return;
try {
const cfg: any = await pbAdmin.getOne('bonus_configs', reward.bonusConfigId);
if (!cfg || cfg.famId !== famId) return;
if (cfg.occurrence !== 'once' || cfg.status === 'completed') return;
await pbAdmin.update('bonus_configs', cfg.id, {
status: 'completed',
completedAt: new Date().toISOString().slice(0, 10),
completedBy: reward.memberId || 'admin'
});
} catch {}
}
export function assertPaydayUnlocked(reward: any, tz?: string) {
if (!reward || reward.claimable !== 'payday' || !reward.settleDate) return;
const today = todayInTz(resolveServerTz(tz));
@@ -28,10 +49,12 @@ export async function claim(pb: any, famId: string, id: string) {
// Admin approval → status 'claimed'.
export async function approve(pb: any, famId: string, id: string) {
return pb.collection('rewards').update(id, {
const record = await pb.collection('rewards').update(id, {
status: 'claimed',
claimedAt: new Date().toISOString()
});
await completeOnceConfig(famId, record);
return record;
}
export async function requestAll(pb: any, famId: string, memberId: string) {
@@ -61,7 +84,8 @@ export async function issueAll(pb: any, famId: string, memberId: string) {
});
let count = 0;
for (const r of rewards) {
await pb.collection('rewards').update(r.id, { status: 'claimed', claimedAt: now });
const record = await pb.collection('rewards').update(r.id, { status: 'claimed', claimedAt: now });
await completeOnceConfig(famId, record);
count++;
}
return { count };
+2
View File
@@ -146,6 +146,8 @@ export interface BonusConfig {
criteriaValue?: number;
period?: BonusPeriod;
status: BonusStatus;
completedBy?: string;
completedAt?: string;
targetChoreId?: string;
targetChoreIds?: Record<string, string>;
isPocketMoney?: boolean;
@@ -623,12 +623,6 @@
const rDate = (existingReward?.date || '').slice(0, 10);
const rewardInWeek = !!rDate && rDate >= weekStart && rDate <= weekEnd;
const rewardOutstanding = !!existingReward && existingReward.status !== 'claimed';
// A claimed reward from a previous week with no fresh progress
// means the goal is done — drop the card entirely instead of
// showing a stale 0% chase.
const staleClaimed =
!!existingReward && !rewardInWeek && !rewardOutstanding && current === 0;
if (staleClaimed) return null;
const achieved = existingReward
? rewardInWeek || rewardOutstanding
: isPercent
@@ -659,8 +653,7 @@
periodStart: pStart,
periodEnd: pEnd
};
})
.filter((g) => g !== null);
});
});
// ─── Goal celebrations (bonus rewards achieved) ───