fix bonus completeions
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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) ───
|
||||
|
||||
@@ -228,6 +228,8 @@ export const SCHEMA_PLAN: CollectionPlanEntry[] = [
|
||||
rel("memberId", ids.users),
|
||||
select("period", ["schedule", "daily", "weekly", "monthly"]),
|
||||
select("status", ["active", "completed"], true),
|
||||
text("completedBy"),
|
||||
date("completedAt"),
|
||||
text("targetChoreId"),
|
||||
jsonField("targetChoreIds"),
|
||||
bool("isPocketMoney"),
|
||||
|
||||
Reference in New Issue
Block a user