134 lines
5.2 KiB
TypeScript
134 lines
5.2 KiB
TypeScript
import { periodWindow } from '@shared/timezone';
|
|
import { pbAdmin } from '$lib/server/pocketbase';
|
|
import { evaluateFam } from './bonuses';
|
|
|
|
// Reads/writes the acting user may not be permitted by PB rules:
|
|
// - `fams` view (prod may predate RULE_OWN_FAM; migrate only bootstraps fresh)
|
|
// - `assigned_chores` update (parent-only rule, but children claim shared chores)
|
|
// Both stay fam-scoped: the fam record id and the chore's own famId are checked.
|
|
async function famMetaSU(famId: string) {
|
|
const fam: any = await pbAdmin.getOne('fams', famId);
|
|
const { resolveTz } = await import('@shared/timezone');
|
|
return {
|
|
payday: fam.payday !== undefined && fam.payday !== null ? Number(fam.payday) : 1,
|
|
paydayTime: fam.paydayTime || '18:00',
|
|
tz: resolveTz(fam.timezone || 'auto')
|
|
};
|
|
}
|
|
|
|
async function claimChore(assignedChoreId: string, famId: string, memberId: string | '') {
|
|
const chore: any = await pbAdmin.getOne('assigned_chores', assignedChoreId);
|
|
if (!chore || chore.famId !== famId) throw new Error('Chore not found');
|
|
await pbAdmin.update('assigned_chores', assignedChoreId, { memberId });
|
|
}
|
|
|
|
export async function myChores(pb: any, famId: string, memberId: string) {
|
|
const [templates, assigned, completions, rewards, bonusConfigs] = await Promise.all([
|
|
pb.collection('chore_templates').getFullList({ filter: `famId = '${famId}'` }),
|
|
pb.collection('assigned_chores').getFullList({
|
|
filter: `famId = '${famId}' && (memberId = '${memberId}' || memberId = '')`
|
|
}),
|
|
pb.collection('completions').getFullList({ filter: `famId = '${famId}' && memberId = '${memberId}'` }),
|
|
pb.collection('rewards').getFullList({ filter: `famId = '${famId}' && memberId = '${memberId}'` }),
|
|
pb.collection('bonus_configs').getFullList({ filter: `famId = '${famId}' && status = 'active'` })
|
|
]);
|
|
const { payday, paydayTime, tz } = await famMetaSU(famId);
|
|
return {
|
|
templates,
|
|
assigned,
|
|
completions,
|
|
rewards,
|
|
bonusConfigs,
|
|
payday,
|
|
paydayTime,
|
|
timezone: tz
|
|
};
|
|
}
|
|
|
|
export async function toggle(pb: any, famId: string, memberId: string, body: { assignedChoreId: string; date: string }) {
|
|
const { assignedChoreId, date } = body;
|
|
if (!assignedChoreId || !date) throw new Error('assignedChoreId and date required');
|
|
|
|
const choreList = await pb
|
|
.collection('assigned_chores')
|
|
.getFullList({ filter: `famId = '${famId}' && id = '${assignedChoreId}'` });
|
|
const chore = choreList?.[0];
|
|
const isTodo = chore?.isTodo;
|
|
const isShared = chore?.shared === true || !chore?.memberId;
|
|
|
|
let filter: string;
|
|
if (isShared && !isTodo) {
|
|
// Shared non-todo: any completion in the period = already claimed
|
|
const { payday, tz } = await famMetaSU(famId);
|
|
const { from, to } = periodWindow(chore?.frequency, payday, tz);
|
|
filter = `assignedChoreId = '${assignedChoreId}' && date >= '${from}' && date < '${to}'`;
|
|
} else if (isTodo) {
|
|
filter = `assignedChoreId = '${assignedChoreId}' && memberId = '${memberId}'`;
|
|
} else {
|
|
const { payday, tz } = await famMetaSU(famId);
|
|
const { from, to } = periodWindow(chore?.frequency, payday, tz);
|
|
filter = `assignedChoreId = '${assignedChoreId}' && memberId = '${memberId}' && date >= '${from}' && date < '${to}'`;
|
|
}
|
|
const existing = await pb
|
|
.collection('completions')
|
|
.getFullList({ filter });
|
|
if (existing?.length > 0) {
|
|
await pb.collection('completions').delete(existing[0].id);
|
|
// Un-completing a cash todo removes its one-time reward.
|
|
if (isTodo && existing[0].rewardId) {
|
|
await pb.collection('rewards').delete(existing[0].rewardId).catch(() => {});
|
|
}
|
|
// If this was a shared chore claimed by this member, un-share it
|
|
if (isShared && existing[0].memberId === memberId) {
|
|
await claimChore(assignedChoreId, famId, '');
|
|
}
|
|
evaluateFam(pb, famId).catch(() => {});
|
|
return { completed: false };
|
|
}
|
|
let rewardId: string | undefined;
|
|
if (isTodo && chore?.type === 'money' && Number(chore.value) > 0) {
|
|
const reward = await pb.collection('rewards').create({
|
|
famId,
|
|
memberId,
|
|
label: chore.customName || 'Cash todo',
|
|
value: Number(chore.value),
|
|
rewardType: 'cash',
|
|
status: 'unclaimed',
|
|
claimable: 'payday',
|
|
date
|
|
});
|
|
rewardId = reward.id;
|
|
}
|
|
const record = await pb.collection('completions').create({
|
|
famId,
|
|
memberId,
|
|
assignedChoreId,
|
|
date,
|
|
completedAt: new Date().toISOString(),
|
|
...(rewardId ? { rewardId } : {})
|
|
});
|
|
// Claim the shared chore by setting memberId
|
|
if (isShared) {
|
|
await claimChore(assignedChoreId, famId, memberId);
|
|
}
|
|
evaluateFam(pb, famId).catch(() => {});
|
|
return { completed: true, record };
|
|
}
|
|
|
|
export async function revoke(pb: any, famId: string, completionId: string) {
|
|
const completions = await pb
|
|
.collection('completions')
|
|
.getFullList({ filter: `famId = '${famId}' && id = '${completionId}'` });
|
|
if (!completions?.length) throw new Error('Completion not found');
|
|
const completion = completions[0];
|
|
await pb.collection('completions').delete(completionId);
|
|
// If this was a shared chore, clear memberId to make it available again
|
|
const choreList = await pb.collection('assigned_chores')
|
|
.getFullList({ filter: `famId = '${famId}' && id = '${completion.assignedChoreId}'` });
|
|
const chore = choreList?.[0];
|
|
if (chore?.shared === true) {
|
|
await claimChore(chore.id, famId, '');
|
|
}
|
|
evaluateFam(pb, famId).catch(() => {});
|
|
return { revoked: true };
|
|
} |