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 });
|
fields.push({ name: 'targetChoreIds', type: 'json', required: false });
|
||||||
changed = true;
|
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.
|
// 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) {
|
||||||
|
|||||||
@@ -252,8 +252,20 @@ export async function evaluateFam(pb: any, famId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (cfg.occurrence === 'once' && (existingRewards.length > 0 || createdReward)) {
|
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 {
|
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 {}
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,7 +79,11 @@ export function createServices(
|
|||||||
return pb.collection('bonus_configs').update(configId, updates);
|
return pb.collection('bonus_configs').update(configId, updates);
|
||||||
},
|
},
|
||||||
complete: (famId: string, configId: string) =>
|
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)
|
destroy: (famId: string, configId: string) => pb.collection('bonus_configs').delete(configId)
|
||||||
},
|
},
|
||||||
settings: {
|
settings: {
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
import { todayInTz, resolveTz } from '@shared/timezone';
|
import { todayInTz, resolveTz } from '@shared/timezone';
|
||||||
import { famMeta } from './fam';
|
import { famMeta } from './fam';
|
||||||
|
import { pbAdmin } from '$lib/server/pocketbase';
|
||||||
|
|
||||||
function resolveServerTz(tz?: string): string {
|
function resolveServerTz(tz?: string): string {
|
||||||
return resolveTz(tz || 'auto');
|
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) {
|
export function assertPaydayUnlocked(reward: any, tz?: string) {
|
||||||
if (!reward || reward.claimable !== 'payday' || !reward.settleDate) return;
|
if (!reward || reward.claimable !== 'payday' || !reward.settleDate) return;
|
||||||
const today = todayInTz(resolveServerTz(tz));
|
const today = todayInTz(resolveServerTz(tz));
|
||||||
@@ -28,10 +49,12 @@ export async function claim(pb: any, famId: string, id: string) {
|
|||||||
|
|
||||||
// Admin approval → status 'claimed'.
|
// Admin approval → status 'claimed'.
|
||||||
export async function approve(pb: any, famId: string, id: string) {
|
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',
|
status: 'claimed',
|
||||||
claimedAt: new Date().toISOString()
|
claimedAt: new Date().toISOString()
|
||||||
});
|
});
|
||||||
|
await completeOnceConfig(famId, record);
|
||||||
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function requestAll(pb: any, famId: string, memberId: string) {
|
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;
|
let count = 0;
|
||||||
for (const r of rewards) {
|
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++;
|
count++;
|
||||||
}
|
}
|
||||||
return { count };
|
return { count };
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ export interface BonusConfig {
|
|||||||
criteriaValue?: number;
|
criteriaValue?: number;
|
||||||
period?: BonusPeriod;
|
period?: BonusPeriod;
|
||||||
status: BonusStatus;
|
status: BonusStatus;
|
||||||
|
completedBy?: string;
|
||||||
|
completedAt?: string;
|
||||||
targetChoreId?: string;
|
targetChoreId?: string;
|
||||||
targetChoreIds?: Record<string, string>;
|
targetChoreIds?: Record<string, string>;
|
||||||
isPocketMoney?: boolean;
|
isPocketMoney?: boolean;
|
||||||
|
|||||||
@@ -623,12 +623,6 @@
|
|||||||
const rDate = (existingReward?.date || '').slice(0, 10);
|
const rDate = (existingReward?.date || '').slice(0, 10);
|
||||||
const rewardInWeek = !!rDate && rDate >= weekStart && rDate <= weekEnd;
|
const rewardInWeek = !!rDate && rDate >= weekStart && rDate <= weekEnd;
|
||||||
const rewardOutstanding = !!existingReward && existingReward.status !== 'claimed';
|
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
|
const achieved = existingReward
|
||||||
? rewardInWeek || rewardOutstanding
|
? rewardInWeek || rewardOutstanding
|
||||||
: isPercent
|
: isPercent
|
||||||
@@ -659,8 +653,7 @@
|
|||||||
periodStart: pStart,
|
periodStart: pStart,
|
||||||
periodEnd: pEnd
|
periodEnd: pEnd
|
||||||
};
|
};
|
||||||
})
|
});
|
||||||
.filter((g) => g !== null);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ─── Goal celebrations (bonus rewards achieved) ───
|
// ─── Goal celebrations (bonus rewards achieved) ───
|
||||||
|
|||||||
@@ -228,6 +228,8 @@ export const SCHEMA_PLAN: CollectionPlanEntry[] = [
|
|||||||
rel("memberId", ids.users),
|
rel("memberId", ids.users),
|
||||||
select("period", ["schedule", "daily", "weekly", "monthly"]),
|
select("period", ["schedule", "daily", "weekly", "monthly"]),
|
||||||
select("status", ["active", "completed"], true),
|
select("status", ["active", "completed"], true),
|
||||||
|
text("completedBy"),
|
||||||
|
date("completedAt"),
|
||||||
text("targetChoreId"),
|
text("targetChoreId"),
|
||||||
jsonField("targetChoreIds"),
|
jsonField("targetChoreIds"),
|
||||||
bool("isPocketMoney"),
|
bool("isPocketMoney"),
|
||||||
|
|||||||
Reference in New Issue
Block a user