151 lines
5.3 KiB
TypeScript
151 lines
5.3 KiB
TypeScript
import { redirect } from '@sveltejs/kit';
|
|
import type { RequestEvent } from '@sveltejs/kit';
|
|
import { pbUser } from '$lib/server/pocketbase';
|
|
import { pbAdmin } from '$lib/server/pocketbase';
|
|
import { servicesFor } from '$lib/server/servicesFor';
|
|
import { issueAccess, createChild } from '$lib/server/member-otp';
|
|
import { slugify } from '@shared/slugify';
|
|
|
|
function famIdOf(event: RequestEvent): string {
|
|
if (!event.locals.user) throw redirect(303, '/login');
|
|
return event.locals.user.famId;
|
|
}
|
|
|
|
export async function load(event: RequestEvent) {
|
|
const famId = famIdOf(event);
|
|
const pb = pbUser(event);
|
|
const [members, fam, seasons] = await Promise.all([
|
|
pb.collection('users').getFullList({
|
|
filter: `famId = '${famId}' && role = 'child'`
|
|
}),
|
|
pbAdmin.getOne('fams', famId),
|
|
pb.collection('seasons').getFullList({ filter: `famId = '${famId}'` })
|
|
]);
|
|
return { members, fam, seasons };
|
|
}
|
|
|
|
export const actions = {
|
|
addMember: async (event: RequestEvent) => {
|
|
const famId = famIdOf(event);
|
|
const famSlug: string = event.params.fam as string;
|
|
const fd = await event.request.formData();
|
|
const name = (fd.get('name') as string) || '';
|
|
const colour = (fd.get('colour') as string) || '#6366f1';
|
|
if (!name) return { error: 'Name required' };
|
|
try {
|
|
await createChild({ famId, famSlug, name, colour });
|
|
return { ok: true };
|
|
} catch (e) {
|
|
return { error: e instanceof Error ? e.message : 'Failed to add member' };
|
|
}
|
|
},
|
|
|
|
issueAccess: async (event: RequestEvent) => {
|
|
const famId = famIdOf(event);
|
|
const famSlug: string = event.params.fam as string;
|
|
const fd = await event.request.formData();
|
|
const name = (fd.get('name') || '').toString().trim();
|
|
if (!name) return { error: 'Select a child to invite' };
|
|
try {
|
|
const result = await issueAccess({ famId, famSlug, name });
|
|
return { ok: true, ...result };
|
|
} catch (e) {
|
|
return { error: e instanceof Error ? e.message : 'Failed to issue access' };
|
|
}
|
|
},
|
|
|
|
renameFam: async (event: RequestEvent) => {
|
|
const famId = famIdOf(event);
|
|
const fd = await event.request.formData();
|
|
const name = fd.get('name') as string;
|
|
if (!name) return { error: 'Name required' };
|
|
const record = await pbUser(event)
|
|
.collection('fams')
|
|
.update(famId, { name, slug: slugify(name) });
|
|
return { name: record.name, slug: record.slug };
|
|
},
|
|
|
|
deleteMember: async (event: RequestEvent) => {
|
|
const famId = famIdOf(event);
|
|
const fd = await event.request.formData();
|
|
const id = fd.get('id') as string;
|
|
if (!id) return { error: 'Member ID required' };
|
|
await pbUser(event).collection('users').delete(id);
|
|
return { ok: true };
|
|
},
|
|
|
|
updatePayday: async (event: RequestEvent) => {
|
|
const famId = famIdOf(event);
|
|
const fd = await event.request.formData();
|
|
const payday = parseInt(fd.get('payday') as string, 10);
|
|
if (isNaN(payday) || payday < 0 || payday > 6) return { error: 'Payday must be 0-6' };
|
|
const paydayTime = fd.get('paydayTime') as string;
|
|
if (paydayTime && !/^\d{2}:\d{2}$/.test(paydayTime)) return { error: 'Payday time must be HH:MM' };
|
|
const timezone = fd.get('timezone') as string;
|
|
if (timezone && timezone !== 'auto' && !/^[A-Za-z_+-]+\/[A-Za-z_+-]+$/.test(timezone)) {
|
|
return { error: 'Timezone must be an IANA name or auto' };
|
|
}
|
|
const patch: Record<string, unknown> = { payday };
|
|
if (paydayTime) patch.paydayTime = paydayTime;
|
|
if (timezone) patch.timezone = timezone;
|
|
const record = await pbUser(event).collection('fams').update(famId, patch);
|
|
return { payday: record.payday, paydayTime: record.paydayTime, timezone: record.timezone };
|
|
},
|
|
|
|
createSeason: async (event: RequestEvent) => {
|
|
const famId = famIdOf(event);
|
|
const fd = await event.request.formData();
|
|
const name = fd.get('name') as string;
|
|
const color = fd.get('color') as string;
|
|
if (!name) return { error: 'Name required' };
|
|
return await pbUser(event)
|
|
.collection('seasons')
|
|
.create({ famId, name, color: color || '#6366f1', active: true });
|
|
},
|
|
|
|
deleteSeason: async (event: RequestEvent) => {
|
|
const famId = famIdOf(event);
|
|
const fd = await event.request.formData();
|
|
const id = fd.get('id') as string;
|
|
if (!id) return { error: 'Season ID required' };
|
|
|
|
const pb = pbUser(event);
|
|
const assigned = await pb.collection('assigned_chores').getFullList({ filter: `famId = '${famId}'` });
|
|
const toDelete = (Array.isArray(assigned) ? assigned : []).filter(
|
|
(a: any) => a.seasonIds?.includes(id)
|
|
);
|
|
const deletedIds = toDelete.map((a: any) => a.id);
|
|
|
|
await Promise.all(
|
|
toDelete.map((a: any) => pb.collection('assigned_chores').delete(a.id))
|
|
);
|
|
await pb.collection('seasons').delete(id);
|
|
|
|
return { deletedChoreIds: deletedIds };
|
|
},
|
|
|
|
// Compute endpoints — in-process.
|
|
completeWeek: async (event: RequestEvent) => {
|
|
const famId = famIdOf(event);
|
|
try {
|
|
const s = servicesFor(event);
|
|
const result = await s.fam.completeWeek(famId);
|
|
return { success: true, result };
|
|
} catch (e) {
|
|
return { error: e instanceof Error ? e.message : 'Failed to complete week' };
|
|
}
|
|
},
|
|
|
|
generateData: async (event: RequestEvent) => {
|
|
const famId = famIdOf(event);
|
|
const fd = await event.request.formData();
|
|
const days = parseInt((fd.get('days') as string) || '7', 10);
|
|
try {
|
|
const s = servicesFor(event);
|
|
const result = await s.debug.generateData(famId, days);
|
|
return { success: true, result };
|
|
} catch (e) {
|
|
return { error: e instanceof Error ? e.message : 'Failed to generate data' };
|
|
}
|
|
}
|
|
}; |