diff --git a/src/routes/foster/+layout.server.ts b/src/routes/foster/+layout.server.ts
new file mode 100644
index 0000000..82a7ffd
--- /dev/null
+++ b/src/routes/foster/+layout.server.ts
@@ -0,0 +1,40 @@
+import type { LayoutServerLoad } from './$types';
+import { redirect } from '@sveltejs/kit';
+import { db } from '$lib/server/db';
+import { fosterAssignments, pets, petPhotos } from '$lib/server/schema';
+import { eq, and } from 'drizzle-orm';
+
+export const load: LayoutServerLoad = async ({ locals }) => {
+ if (!locals.user) {
+ throw redirect(303, '/login');
+ }
+
+ if (locals.user.role !== 'foster') {
+ throw redirect(303, locals.user.role === 'sysadmin' || locals.user.role === 'director' ? '/admin' : '/login');
+ }
+
+ const userId = locals.user.id;
+
+ // Load current foster assignments with pet details for sidebar/context
+ const assignments = await db
+ .select({
+ assignmentId: fosterAssignments.id,
+ petId: pets.id,
+ petName: pets.name,
+ petSpecies: pets.species,
+ petBreed: pets.breed,
+ petAge: pets.age,
+ petStatus: pets.status,
+ photoUrl: petPhotos.url
+ })
+ .from(fosterAssignments)
+ .innerJoin(pets, eq(fosterAssignments.petId, pets.id))
+ .leftJoin(petPhotos, and(eq(petPhotos.petId, pets.id), eq(petPhotos.isPrimary, true)))
+ .where(and(eq(fosterAssignments.userId, userId), eq(fosterAssignments.isCurrent, true)));
+
+ return {
+ user: locals.user,
+ orgConfig: locals.orgConfig,
+ assignments
+ };
+};
diff --git a/src/routes/foster/+layout.svelte b/src/routes/foster/+layout.svelte
new file mode 100644
index 0000000..ce96b66
--- /dev/null
+++ b/src/routes/foster/+layout.svelte
@@ -0,0 +1,91 @@
+
+
+
+ {@html ``}
+
+
+
+
+
+
+ {#if data.orgConfig.branding.logoUrl}
+

+ {/if}
+
{orgName} Foster Portal
+
+
+
+
+
+
+
+
+
+ {#if sidebarOpen}
+
sidebarOpen = false}
+ role="button"
+ tabindex="-1"
+ onkeydown={() => {}}
+ >
+ {/if}
+
+
+
+ {@render children()}
+
+
+
diff --git a/src/routes/foster/+page.server.ts b/src/routes/foster/+page.server.ts
new file mode 100644
index 0000000..e381097
--- /dev/null
+++ b/src/routes/foster/+page.server.ts
@@ -0,0 +1,70 @@
+import type { PageServerLoad } from './$types';
+import { db } from '$lib/server/db';
+import { fosterAssignments, fosterSubmissions, supplyRequests, pets, petPhotos } from '$lib/server/schema';
+import { eq, and, desc, count } from 'drizzle-orm';
+
+export const load: PageServerLoad = async ({ locals }) => {
+ const userId = locals.user!.id;
+
+ // Load current foster assignments with pet info and primary photo
+ const assignments = await db
+ .select({
+ assignmentId: fosterAssignments.id,
+ startDate: fosterAssignments.startDate,
+ notes: fosterAssignments.notes,
+ petId: pets.id,
+ petName: pets.name,
+ petBreed: pets.breed,
+ petAge: pets.age,
+ petStatus: pets.status,
+ petSpecies: pets.species,
+ photoUrl: petPhotos.url
+ })
+ .from(fosterAssignments)
+ .innerJoin(pets, eq(fosterAssignments.petId, pets.id))
+ .leftJoin(petPhotos, and(eq(petPhotos.petId, pets.id), eq(petPhotos.isPrimary, true)))
+ .where(and(eq(fosterAssignments.userId, userId), eq(fosterAssignments.isCurrent, true)));
+
+ // Recent submissions (last 10)
+ const recentSubmissions = await db
+ .select({
+ id: fosterSubmissions.id,
+ type: fosterSubmissions.type,
+ content: fosterSubmissions.content,
+ status: fosterSubmissions.status,
+ createdAt: fosterSubmissions.createdAt,
+ petName: pets.name
+ })
+ .from(fosterSubmissions)
+ .innerJoin(pets, eq(fosterSubmissions.petId, pets.id))
+ .where(eq(fosterSubmissions.userId, userId))
+ .orderBy(desc(fosterSubmissions.createdAt))
+ .limit(10);
+
+ // Pending supply requests
+ const pendingRequests = await db
+ .select({
+ id: supplyRequests.id,
+ items: supplyRequests.items,
+ urgency: supplyRequests.urgency,
+ status: supplyRequests.status,
+ createdAt: supplyRequests.createdAt
+ })
+ .from(supplyRequests)
+ .where(and(eq(supplyRequests.userId, userId), eq(supplyRequests.status, 'pending')))
+ .orderBy(desc(supplyRequests.createdAt));
+
+ // Count pending submissions
+ const [pendingSubRow] = await db
+ .select({ total: count() })
+ .from(fosterSubmissions)
+ .where(and(eq(fosterSubmissions.userId, userId), eq(fosterSubmissions.status, 'pending')));
+ const pendingSubmissionCount = pendingSubRow?.total ?? 0;
+
+ return {
+ assignments,
+ recentSubmissions,
+ pendingRequests,
+ pendingSubmissionCount
+ };
+};
diff --git a/src/routes/foster/+page.svelte b/src/routes/foster/+page.svelte
new file mode 100644
index 0000000..45539b2
--- /dev/null
+++ b/src/routes/foster/+page.svelte
@@ -0,0 +1,159 @@
+
+
+
+ Foster Dashboard — {orgName}
+
+
+Welcome, {data.user.name}
+Here is an overview of your foster animals and activity.
+
+
+
+
+
Active Fosters
+
{data.assignments.length}
+
+
+
Pending Submissions
+
{data.pendingSubmissionCount}
+
+
+
Pending Supply Requests
+
{data.pendingRequests.length}
+
+
+
+
+
+
My Animals
+ {#if data.assignments.length > 0}
+
+ {/if}
+
+
+{#if data.assignments.length === 0}
+
+
No animals currently assigned to you.
+
Your foster coordinator will assign animals when they are ready.
+
+{:else}
+
+ {#each data.assignments as a}
+
+ {#if a.photoUrl}
+

+ {:else}
+
+ {/if}
+
+
{a.petName}
+
+ {a.petBreed ?? 'Unknown breed'}
+ {#if a.petAge}
+ · {a.petAge}
+ {/if}
+
+
In foster since {formatDate(a.startDate)}
+
+
+
+ {/each}
+
+{/if}
+
+
+Recent Submissions
+
+ {#if data.recentSubmissions.length === 0}
+
+ No recent submissions.
+ {#if data.assignments.length > 0}
+
+ Submit your first update
+
+ {/if}
+
+ {:else}
+
+ {#each data.recentSubmissions as sub}
+ -
+
+ {sub.status}
+
+
+
+ {typeLabel(sub.type)}
+ for {sub.petName}
+ {#if sub.content}
+ — {sub.content.length > 60 ? sub.content.slice(0, 60) + '...' : sub.content}
+ {/if}
+
+
+ {formatDate(sub.createdAt)}
+
+ {/each}
+
+ {/if}
+
diff --git a/src/routes/foster/submit/+page.server.ts b/src/routes/foster/submit/+page.server.ts
new file mode 100644
index 0000000..e46c815
--- /dev/null
+++ b/src/routes/foster/submit/+page.server.ts
@@ -0,0 +1,111 @@
+import type { PageServerLoad, Actions } from './$types';
+import { db } from '$lib/server/db';
+import { fosterSubmissions, fosterAssignments, pets } from '$lib/server/schema';
+import { eq, and } from 'drizzle-orm';
+import { fail } from '@sveltejs/kit';
+import { saveUpload } from '$lib/server/upload';
+import { getOrgConfig } from '$lib/server/config';
+import { sendFosterSubmissionNotification } from '$lib/server/email';
+
+export const load: PageServerLoad = async ({ locals, url }) => {
+ const userId = locals.user!.id;
+ const preselectedPetId = url.searchParams.get('petId') ? parseInt(url.searchParams.get('petId')!) : null;
+
+ const assignments = await db
+ .select({
+ petId: pets.id,
+ petName: pets.name
+ })
+ .from(fosterAssignments)
+ .innerJoin(pets, eq(fosterAssignments.petId, pets.id))
+ .where(and(eq(fosterAssignments.userId, userId), eq(fosterAssignments.isCurrent, true)));
+
+ return {
+ assignments,
+ preselectedPetId
+ };
+};
+
+export const actions: Actions = {
+ default: async ({ request, locals }) => {
+ const userId = locals.user!.id;
+ const fd = await request.formData();
+
+ const petId = parseInt(fd.get('petId')?.toString() ?? '0', 10);
+ const type = fd.get('type')?.toString() ?? 'update';
+ const content = fd.get('content')?.toString().trim() || null;
+ const photoFile = fd.get('photo') as File | null;
+
+ if (!petId) {
+ return fail(400, { error: 'Please select a pet.' });
+ }
+
+ if (!['note', 'photo', 'update'].includes(type)) {
+ return fail(400, { error: 'Invalid submission type.' });
+ }
+
+ if (!content && !photoFile?.size) {
+ return fail(400, { error: 'Please add a note or upload a photo.' });
+ }
+
+ // Verify pet is assigned to this foster
+ const [assignment] = await db
+ .select({ id: fosterAssignments.id })
+ .from(fosterAssignments)
+ .where(
+ and(
+ eq(fosterAssignments.petId, petId),
+ eq(fosterAssignments.userId, userId),
+ eq(fosterAssignments.isCurrent, true)
+ )
+ );
+
+ if (!assignment) {
+ return fail(403, { error: 'This pet is not assigned to you.' });
+ }
+
+ // Handle photo upload
+ let photoUrl: string | null = null;
+ if (photoFile && photoFile.size > 0) {
+ try {
+ const uploaded = await saveUpload(photoFile, 'foster-submissions');
+ photoUrl = uploaded.url;
+ } catch (err) {
+ return fail(400, { error: err instanceof Error ? err.message : 'Photo upload failed.' });
+ }
+ }
+
+ // Get pet name for notification
+ const [pet] = await db
+ .select({ name: pets.name })
+ .from(pets)
+ .where(eq(pets.id, petId));
+
+ await db.insert(fosterSubmissions).values({
+ userId,
+ petId,
+ type: type as 'note' | 'photo' | 'update',
+ content,
+ photoUrl,
+ status: 'pending'
+ });
+
+ // Send notification email
+ try {
+ const config = await getOrgConfig();
+ if (config.orgInfo.email) {
+ await sendFosterSubmissionNotification(
+ config.orgInfo.name,
+ config.orgInfo.email,
+ locals.user!.name,
+ pet?.name ?? 'Unknown',
+ type
+ );
+ }
+ } catch {
+ // Email notification not critical
+ }
+
+ return { success: true };
+ }
+};
diff --git a/src/routes/foster/submit/+page.svelte b/src/routes/foster/submit/+page.svelte
new file mode 100644
index 0000000..cc47626
--- /dev/null
+++ b/src/routes/foster/submit/+page.svelte
@@ -0,0 +1,142 @@
+
+
+
+ Submit Update — {orgName} Foster Portal
+
+
+← Back to Dashboard
+
+Submit an Update
+
+{#if showSuccess}
+
+
+ Your update has been submitted and is pending review.
+
+{/if}
+
+{#if form?.error}
+
+ {form.error}
+
+{/if}
+
+
+ {#if data.assignments.length === 0}
+
+
No animals are currently assigned to you.
+
Contact your foster coordinator to be assigned a pet.
+
+ {:else}
+
+ {/if}
+
diff --git a/src/routes/foster/supplies/+page.server.ts b/src/routes/foster/supplies/+page.server.ts
new file mode 100644
index 0000000..3cf313f
--- /dev/null
+++ b/src/routes/foster/supplies/+page.server.ts
@@ -0,0 +1,110 @@
+import type { PageServerLoad, Actions } from './$types';
+import { db } from '$lib/server/db';
+import { supplyRequests, fosterAssignments, pets } from '$lib/server/schema';
+import { eq, and, desc } from 'drizzle-orm';
+import { fail } from '@sveltejs/kit';
+import { getOrgConfig } from '$lib/server/config';
+import { sendSupplyRequestNotification } from '$lib/server/email';
+
+export const load: PageServerLoad = async ({ locals, url }) => {
+ const userId = locals.user!.id;
+ const preselectedPetId = url.searchParams.get('petId') ? parseInt(url.searchParams.get('petId')!) : null;
+
+ // All supply requests for this user
+ const requests = await db
+ .select({
+ id: supplyRequests.id,
+ items: supplyRequests.items,
+ urgency: supplyRequests.urgency,
+ notes: supplyRequests.notes,
+ status: supplyRequests.status,
+ createdAt: supplyRequests.createdAt,
+ petId: supplyRequests.petId,
+ petName: pets.name
+ })
+ .from(supplyRequests)
+ .leftJoin(pets, eq(supplyRequests.petId, pets.id))
+ .where(eq(supplyRequests.userId, userId))
+ .orderBy(desc(supplyRequests.createdAt));
+
+ // Current foster assignments for pet dropdown
+ const assignments = await db
+ .select({
+ petId: pets.id,
+ petName: pets.name
+ })
+ .from(fosterAssignments)
+ .innerJoin(pets, eq(fosterAssignments.petId, pets.id))
+ .where(and(eq(fosterAssignments.userId, userId), eq(fosterAssignments.isCurrent, true)));
+
+ return {
+ requests,
+ assignments,
+ preselectedPetId
+ };
+};
+
+export const actions: Actions = {
+ create: async ({ request, locals }) => {
+ const userId = locals.user!.id;
+ const fd = await request.formData();
+
+ const petId = fd.get('petId')?.toString() ? parseInt(fd.get('petId')!.toString(), 10) : null;
+ const items = fd.get('items')?.toString().trim() ?? '';
+ const urgency = fd.get('urgency')?.toString() ?? 'medium';
+ const notes = fd.get('notes')?.toString().trim() || null;
+
+ if (!items) {
+ return fail(400, { error: 'Please list the supplies you need.' });
+ }
+
+ if (!['low', 'medium', 'high'].includes(urgency)) {
+ return fail(400, { error: 'Invalid urgency level.' });
+ }
+
+ // If petId provided, verify pet is assigned to this foster
+ if (petId) {
+ const [assignment] = await db
+ .select({ id: fosterAssignments.id })
+ .from(fosterAssignments)
+ .where(
+ and(
+ eq(fosterAssignments.petId, petId),
+ eq(fosterAssignments.userId, userId),
+ eq(fosterAssignments.isCurrent, true)
+ )
+ );
+
+ if (!assignment) {
+ return fail(403, { error: 'This pet is not assigned to you.' });
+ }
+ }
+
+ await db.insert(supplyRequests).values({
+ userId,
+ petId: petId || null,
+ items,
+ urgency: urgency as 'low' | 'medium' | 'high',
+ notes,
+ status: 'pending'
+ });
+
+ // Send notification email
+ try {
+ const config = await getOrgConfig();
+ if (config.orgInfo.email) {
+ await sendSupplyRequestNotification(
+ config.orgInfo.name,
+ config.orgInfo.email,
+ locals.user!.name,
+ items,
+ urgency
+ );
+ }
+ } catch {
+ // Email notification not critical
+ }
+
+ return { success: true };
+ }
+};
diff --git a/src/routes/foster/supplies/+page.svelte b/src/routes/foster/supplies/+page.svelte
new file mode 100644
index 0000000..cbe0e23
--- /dev/null
+++ b/src/routes/foster/supplies/+page.svelte
@@ -0,0 +1,214 @@
+
+
+
+ Request Supplies — {orgName} Foster Portal
+
+
+← Back to Dashboard
+
+Request Supplies
+
+{#if showSuccess}
+
+
+ Supply request submitted. Your coordinator will be notified.
+
+{/if}
+
+{#if form?.error}
+
+ {form.error}
+
+{/if}
+
+
+
+
New Request
+
+
+
+
+
+Your Requests
+
+ {#if data.requests.length === 0}
+
No supply requests yet.
+ {:else}
+
+
+ {#each data.requests as req}
+ -
+
+ {req.items.length > 50 ? req.items.slice(0, 50) + '...' : req.items}
+ {req.status}
+
+
+ {#if req.petName}
+ {req.petName}
+ ·
+ {/if}
+ {req.urgency}
+ ·
+ {formatDate(req.createdAt)}
+
+
+ {/each}
+
+
+
+
+
+
+
+ | Date |
+ Items |
+ Pet |
+ Urgency |
+ Status |
+
+
+
+ {#each data.requests as req}
+
+ | {formatDate(req.createdAt)} |
+ {req.items} |
+ {req.petName ?? '—'} |
+
+ {req.urgency}
+ |
+
+ {req.status}
+ |
+
+ {/each}
+
+
+
+ {/if}
+