From 9e0352cd76ea24fbbe6451fed032e4416dbcd077 Mon Sep 17 00:00:00 2001 From: Jaz-spec Date: Fri, 30 Jan 2026 15:24:54 +0000 Subject: [PATCH 1/3] fix: ensures case insensitive email matching between stored and entered email --- src/lib/airtable/airtable.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/airtable/airtable.ts b/src/lib/airtable/airtable.ts index 3c19bd4..dcd42cc 100644 --- a/src/lib/airtable/airtable.ts +++ b/src/lib/airtable/airtable.ts @@ -199,7 +199,7 @@ export function createAirtableClient(apiKey: string, baseId: string) { } /** - * Check if email exists in Apprentices table + * Check if email exists in Apprentices table (case-insensitive) * Note: filterByFormula requires field NAME "Learner email" - this would break if renamed in Airtable */ async function findApprenticeByEmail(email: string): Promise { @@ -207,7 +207,7 @@ export function createAirtableClient(apiKey: string, baseId: string) { const apprenticeRecords = await apprenticesTable .select({ - filterByFormula: `{Learner email} = "${email}"`, + filterByFormula: `LOWER({Learner email}) = LOWER("${email}")`, maxRecords: 1, returnFieldsByFieldId: true, }) @@ -224,7 +224,7 @@ export function createAirtableClient(apiKey: string, baseId: string) { const records = await apprenticesTable .select({ - filterByFormula: `{Learner email} = "${email}"`, + filterByFormula: `LOWER({Learner email}) = LOWER("${email}")`, maxRecords: 1, returnFieldsByFieldId: true, }) From 3cb6642e770197eb727dc6e1cec8bed63bcaeb5c Mon Sep 17 00:00:00 2001 From: Jaz-spec Date: Fri, 30 Jan 2026 15:35:20 +0000 Subject: [PATCH 2/3] fix: mutiple cohort support --- src/lib/airtable/airtable.ts | 8 +++--- src/lib/airtable/attendance.ts | 34 +++++++++++++----------- src/routes/api/checkin/events/+server.ts | 4 +-- src/routes/checkin/+page.server.ts | 6 ++--- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/src/lib/airtable/airtable.ts b/src/lib/airtable/airtable.ts index dcd42cc..161ed41 100644 --- a/src/lib/airtable/airtable.ts +++ b/src/lib/airtable/airtable.ts @@ -14,7 +14,7 @@ export interface ApprenticeRecord { id: string; name: string; email: string; - cohortId: string | null; // Record ID of cohort + cohortIds: string[]; // Record IDs of cohorts (apprentice may belong to multiple) } export interface Cohort { @@ -242,7 +242,7 @@ export function createAirtableClient(apiKey: string, baseId: string) { id: record.id, name: record.get(APPRENTICE_FIELDS.NAME) as string, email: emailLookup?.[0] ?? email, - cohortId: cohortLink?.[0] ?? null, + cohortIds: cohortLink ?? [], }; } @@ -332,7 +332,7 @@ export function createAirtableClient(apiKey: string, baseId: string) { id: record.id, name: record.get(APPRENTICE_FIELDS.NAME) as string, email: emailLookup?.[0] ?? '', - cohortId: cohortLink?.[0] ?? null, + cohortIds: cohortLink ?? [], }; }); } @@ -362,7 +362,7 @@ export function createAirtableClient(apiKey: string, baseId: string) { id: record.id, name: record.get(APPRENTICE_FIELDS.NAME) as string, email: emailLookup?.[0] ?? '', - cohortId: cohortLink?.[0] ?? null, + cohortIds: cohortLink ?? [], }; }); } diff --git a/src/lib/airtable/attendance.ts b/src/lib/airtable/attendance.ts index e59cea6..1cde6c7 100644 --- a/src/lib/airtable/attendance.ts +++ b/src/lib/airtable/attendance.ts @@ -397,20 +397,21 @@ export function createAttendanceClient(apiKey: string, baseId: string) { // ============================================ /** - * Get events for a specific cohort, optionally filtered by date range + * Get events for cohorts, optionally filtered by date range * This is THE source of truth for which events count toward an apprentice's stats + * Accepts multiple cohort IDs - returns events that match ANY of the cohorts */ - function getEventsForCohort( + function getEventsForCohorts( allEvents: EventForStats[], - cohortId: string | null, + cohortIds: string[], options?: { startDate?: Date; endDate?: Date }, ): EventForStats[] { - // Filter by cohort (if no cohort, return empty - apprentice must belong to a cohort) - if (!cohortId) { + // Filter by cohorts (if no cohorts, return empty - apprentice must belong to at least one cohort) + if (cohortIds.length === 0) { return []; } - let events = allEvents.filter(e => e.cohortIds.includes(cohortId)); + let events = allEvents.filter(e => e.cohortIds.some(c => cohortIds.includes(c))); // Filter by date range if provided if (options?.startDate && options?.endDate) { @@ -475,14 +476,15 @@ export function createAttendanceClient(apiKey: string, baseId: string) { const apprentice = apprenticeRecords[0]; const apprenticeName = apprentice.get(APPRENTICE_FIELDS.NAME) as string; const cohortLink = apprentice.get(APPRENTICE_FIELDS.COHORT) as string[] | undefined; - const cohortId = cohortLink?.[0] ?? null; + const cohortIds = cohortLink ?? []; + const primaryCohortId = cohortIds[0] ?? null; // For display purposes - // Get cohort name if available + // Get primary cohort name if available (for display) let cohortName: string | null = null; - if (cohortId) { + if (primaryCohortId) { const cohortRecords = await cohortsTable .select({ - filterByFormula: `RECORD_ID() = "${cohortId}"`, + filterByFormula: `RECORD_ID() = "${primaryCohortId}"`, maxRecords: 1, returnFieldsByFieldId: true, }) @@ -492,9 +494,9 @@ export function createAttendanceClient(apiKey: string, baseId: string) { } } - // Get events for this cohort (with optional date filter) + // Get events for all of the apprentice's cohorts (with optional date filter) const allEvents = await getAllEvents(); - const relevantEvents = getEventsForCohort(allEvents, cohortId, options); + const relevantEvents = getEventsForCohorts(allEvents, cohortIds, options); const relevantEventIds = new Set(relevantEvents.map(e => e.id)); // Get attendance for this apprentice, filtered to cohort events only @@ -542,7 +544,7 @@ export function createAttendanceClient(apiKey: string, baseId: string) { ...stats, apprenticeId, apprenticeName, - cohortId, + cohortId: primaryCohortId, cohortName, trend: calculateTrend(recentRate, previousRate), }; @@ -827,11 +829,11 @@ export function createAttendanceClient(apiKey: string, baseId: string) { const apprentice = apprenticeRecords[0]; const cohortLink = apprentice.get(APPRENTICE_FIELDS.COHORT) as string[] | undefined; - const cohortId = cohortLink?.[0] ?? null; + const cohortIds = cohortLink ?? []; - // Get events for this cohort (with optional date filter) + // Get events for all of the apprentice's cohorts (with optional date filter) const allEvents = await getAllEvents(); - const relevantEvents = getEventsForCohort(allEvents, cohortId, options); + const relevantEvents = getEventsForCohorts(allEvents, cohortIds, options); const relevantEventIds = new Set(relevantEvents.map(e => e.id)); // Get attendance for this apprentice, filtered to cohort events only diff --git a/src/routes/api/checkin/events/+server.ts b/src/routes/api/checkin/events/+server.ts index 39bf107..e2061d9 100644 --- a/src/routes/api/checkin/events/+server.ts +++ b/src/routes/api/checkin/events/+server.ts @@ -25,9 +25,9 @@ export const GET: RequestHandler = async ({ cookies }) => { const allEvents = await listEvents({ startDate: today.toISOString() }); if (apprentice) { - // User has apprentice record: show cohort + public events + // User has apprentice record: show events from any of their cohorts + public events const relevantEvents = allEvents.filter( - event => event.isPublic || (apprentice.cohortId && event.cohortIds.includes(apprentice.cohortId)), + event => event.isPublic || event.cohortIds.some(c => apprentice.cohortIds.includes(c)), ); // Check attendance status for each event diff --git a/src/routes/checkin/+page.server.ts b/src/routes/checkin/+page.server.ts index 3ddbac1..b5f5aed 100644 --- a/src/routes/checkin/+page.server.ts +++ b/src/routes/checkin/+page.server.ts @@ -56,10 +56,10 @@ export const load: PageServerLoad = async ({ locals }) => { // Filter events based on user type let availableEvents; - if (apprentice?.cohortId) { - // User with apprentice record: show cohort events + public events + if (apprentice?.cohortIds.length) { + // User with apprentice record: show events from any of their cohorts + public events availableEvents = allEvents.filter( - event => event.cohortIds.includes(apprentice.cohortId!) || event.isPublic, + event => event.cohortIds.some(c => apprentice.cohortIds.includes(c)) || event.isPublic, ); } else { From f147068cacac50e31acd776bb4bb165f110f8eca Mon Sep 17 00:00:00 2001 From: Jaz Date: Fri, 30 Jan 2026 15:54:51 +0000 Subject: [PATCH 3/3] Update src/lib/airtable/attendance.ts Co-authored-by: Jason Warren --- src/lib/airtable/attendance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/airtable/attendance.ts b/src/lib/airtable/attendance.ts index 1cde6c7..b28df35 100644 --- a/src/lib/airtable/attendance.ts +++ b/src/lib/airtable/attendance.ts @@ -407,7 +407,7 @@ export function createAttendanceClient(apiKey: string, baseId: string) { options?: { startDate?: Date; endDate?: Date }, ): EventForStats[] { // Filter by cohorts (if no cohorts, return empty - apprentice must belong to at least one cohort) - if (cohortIds.length === 0) { + if (cohortIds.length <= 0) { return []; }