feat(admin): Task 34 — donations, expenses, and financial reports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 12:30:55 -05:00
co-authored by Claude Sonnet 4.6
parent f06e85e6f8
commit d1e7a52eec
14 changed files with 1458 additions and 0 deletions
@@ -0,0 +1,62 @@
import type { PageServerLoad, Actions } from './$types';
import { db } from '$lib/server/db';
import { donations, pets } from '$lib/server/schema';
import { error, fail, redirect } from '@sveltejs/kit';
import { logAudit } from '$lib/server/audit';
import { hasPermission } from '$lib/roles';
import type { Role } from '$lib/roles';
export const load: PageServerLoad = async ({ locals }) => {
if (!hasPermission(locals.user!.role as Role, 'donations')) error(403, 'Access denied');
const petList = await db.select({ id: pets.id, name: pets.name }).from(pets).orderBy(pets.name);
return { pets: petList };
};
export const actions: Actions = {
default: async ({ request, locals }) => {
if (!hasPermission(locals.user!.role as Role, 'donations')) error(403, 'Access denied');
const fd = await request.formData();
const source = fd.get('source')?.toString() ?? '';
const amountStr = fd.get('amount')?.toString().trim() ?? '';
const donorName = fd.get('donorName')?.toString().trim() || null;
const donorEmail = fd.get('donorEmail')?.toString().trim() || null;
const petId = fd.get('petId')?.toString() ? parseInt(fd.get('petId') as string, 10) : null;
const isRecurring = fd.get('isRecurring') === 'on';
const notes = fd.get('notes')?.toString().trim() || null;
const purpose = fd.get('purpose')?.toString().trim() || null;
const validSources = ['stripe', 'paypal', 'venmo', 'givebutter', 'cash', 'check', 'zelle', 'google_pay', 'square', 'other'];
if (!validSources.includes(source)) {
return fail(400, { error: 'Please select a valid source', values: Object.fromEntries(fd) });
}
const parsedAmount = parseFloat(amountStr);
if (!amountStr || isNaN(parsedAmount) || parsedAmount <= 0) {
return fail(400, { error: 'Amount must be a positive number', values: Object.fromEntries(fd) });
}
const amountCents = Math.round(parsedAmount * 100);
const [result] = await db.insert(donations).values({
source: source as typeof donations.source.enumValues[number],
amount: amountCents,
donorName,
donorEmail,
petId,
isRecurring,
notes,
purpose
});
await logAudit({
userId: locals.user!.id,
action: 'create_donation',
entity: 'donation',
entityId: result.insertId,
diff: { source, amount: parsedAmount, donorName }
});
redirect(303, '/admin/donations');
}
};