Add admin pet detail pages: profile, photos, medical, fosters (Task 32b)

Pet layout: tabbed nav with counts for photos/medical/fosters, status
badge in header. Detail overview: summary cards and compatibility grid.
Profile: full edit form including adoption info conditional on status,
audit logged. Photos: upload, set primary (star toggle), delete, reorder
with prev/next arrows. Medical: add/delete records grouped by date,
cost stored as cents. Fosters: assign by user account (linked to
foster-role users), set current, delete, audit logged throughout.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-26 12:33:24 -05:00
co-authored by Claude Opus 4.6
parent 55c1eb7a9f
commit 63f127dfca
12 changed files with 1707 additions and 0 deletions
@@ -0,0 +1,137 @@
import type { PageServerLoad, Actions } from './$types';
import { db } from '$lib/server/db';
import { fosterAssignments, users } from '$lib/server/schema';
import { eq, and, asc } from 'drizzle-orm';
import { error, fail } from '@sveltejs/kit';
import { logAudit } from '$lib/server/audit';
import { hasPermission, canViewPets } from '$lib/roles';
export const load: PageServerLoad = async ({ parent, locals }) => {
if (!canViewPets(locals.user!.role)) error(403, 'Access denied');
const { pet } = await parent();
const assignments = await db
.select({
id: fosterAssignments.id,
petId: fosterAssignments.petId,
userId: fosterAssignments.userId,
startDate: fosterAssignments.startDate,
endDate: fosterAssignments.endDate,
isCurrent: fosterAssignments.isCurrent,
notes: fosterAssignments.notes,
createdAt: fosterAssignments.createdAt,
fosterName: users.name,
fosterEmail: users.email
})
.from(fosterAssignments)
.leftJoin(users, eq(fosterAssignments.userId, users.id))
.where(eq(fosterAssignments.petId, pet.id))
.orderBy(asc(fosterAssignments.startDate));
// Load active foster-role users for the dropdown
const fosterUsers = await db
.select({ id: users.id, name: users.name, email: users.email })
.from(users)
.where(and(eq(users.role, 'foster'), eq(users.active, true)))
.orderBy(asc(users.name));
return { assignments, fosterUsers };
};
export const actions: Actions = {
add: async ({ request, params, locals }) => {
if (!hasPermission(locals.user!.role, 'pets')) error(403, 'Access denied');
const petId = parseInt(params.id, 10);
const fd = await request.formData();
const userIdStr = fd.get('userId')?.toString();
const userId = userIdStr ? parseInt(userIdStr, 10) : null;
if (!userId || isNaN(userId)) {
return fail(400, { error: 'Foster user is required' });
}
const startDateStr = fd.get('startDate')?.toString() || null;
const startDate = startDateStr ? new Date(startDateStr) : new Date();
const endDateStr = fd.get('endDate')?.toString() || null;
const endDate = endDateStr ? new Date(endDateStr) : null;
const notes = fd.get('notes')?.toString().trim() || null;
const isCurrent = fd.get('isCurrent') === 'on';
// If marking as current, unset all others
if (isCurrent) {
await db.update(fosterAssignments).set({ isCurrent: false }).where(eq(fosterAssignments.petId, petId));
}
const [inserted] = await db.insert(fosterAssignments).values({
petId,
userId,
startDate,
endDate,
notes,
isCurrent
}).$returningId();
await logAudit({
userId: locals.user!.id,
action: 'create',
entity: 'foster_assignment',
entityId: inserted.id,
diff: { petId, userId, isCurrent }
});
return { success: true };
},
setCurrent: async ({ request, params, locals }) => {
if (!hasPermission(locals.user!.role, 'pets')) error(403, 'Access denied');
const petId = parseInt(params.id, 10);
const fd = await request.formData();
const assignmentId = parseInt(fd.get('assignmentId') as string, 10);
if (!assignmentId || isNaN(assignmentId)) {
return fail(400, { error: 'Invalid assignment ID' });
}
const isCurrent = fd.get('isCurrent') === 'on';
if (isCurrent) {
await db.update(fosterAssignments).set({ isCurrent: false }).where(eq(fosterAssignments.petId, petId));
}
await db.update(fosterAssignments).set({ isCurrent }).where(eq(fosterAssignments.id, assignmentId));
await logAudit({
userId: locals.user!.id,
action: 'update',
entity: 'foster_assignment',
entityId: assignmentId,
diff: { petId, isCurrent }
});
return { success: true };
},
delete: async ({ request, params, locals }) => {
if (!hasPermission(locals.user!.role, 'pets')) error(403, 'Access denied');
const petId = parseInt(params.id, 10);
const fd = await request.formData();
const assignmentId = parseInt(fd.get('assignmentId') as string, 10);
if (!assignmentId || isNaN(assignmentId)) {
return fail(400, { error: 'Invalid assignment ID' });
}
await db.delete(fosterAssignments).where(eq(fosterAssignments.id, assignmentId));
await logAudit({
userId: locals.user!.id,
action: 'delete',
entity: 'foster_assignment',
entityId: assignmentId,
diff: { petId }
});
return { success: true };
}
};