-
-
-
- Showing:
- {#if showAll}
- All Cohorts
- {:else}
- {#each selectedCohortIds as cohortId (cohortId)}
- {@const cohort = cohorts.find(c => c.id === cohortId)}
- {#if cohort}
- {cohort.name}
- {/if}
- {/each}
- {/if}
-
- Change selection
-
-
+
+
+
+
-
+
+
+
+
+
+
+
+
Individual Apprentices
+
{sortedApprentices.length} apprentice{sortedApprentices.length !== 1 ? 's' : ''}
-
+
@@ -381,7 +398,7 @@
>
Attendance Rate{getSortIndicator('attendanceRate')}
-
Attended
+
Lateness Rate
Actions
@@ -398,12 +415,9 @@
{apprentice.attendanceRate.toFixed(0)}%
- {#if isLowAttendance(apprentice.attendanceRate)}
-
⚠
- {/if}
-
- {apprentice.attended}/{apprentice.totalEvents}
+
+ {apprentice.totalEvents > 0 ? ((apprentice.late / apprentice.totalEvents) * 100).toFixed(0) : 0}%
('');
let statusUpdateLoading = $state(false);
+ // Reason editing state (separate from status editing)
+ let editingReasonFor = $state(null);
+ let reasonInput = $state('');
+
// When status changes to Present/Late and no check-in time is set, populate with event start time
$effect(() => {
if (editingEntryId && (editingStatus === 'Present' || editingStatus === 'Late') && !editingCheckinTime) {
@@ -113,6 +120,60 @@
editingCheckinTime = '';
}
+ // Start editing reason only (separate from status editing)
+ function startEditingReason(entry: AttendanceHistoryEntry) {
+ editingReasonFor = entry.eventId;
+ reasonInput = entry.reason || '';
+ }
+
+ // Save reason only
+ async function saveReasonChange() {
+ if (!editingReasonFor) return;
+
+ const entry = history.find(h => h.eventId === editingReasonFor);
+ if (!entry || !entry.attendanceId) return;
+
+ statusUpdateLoading = true;
+ try {
+ const response = await fetch(`/api/attendance/${entry.attendanceId}`, {
+ method: 'PATCH',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ status: entry.status,
+ reason: reasonInput.trim() || null,
+ }),
+ });
+
+ const result = await response.json();
+
+ if (response.ok && result.success) {
+ // Update the history entry
+ history = history.map(entry =>
+ entry.eventId === editingReasonFor
+ ? { ...entry, reason: reasonInput.trim() || null }
+ : entry,
+ );
+ cancelReasonEditing();
+ }
+ else {
+ console.error('Failed to update reason:', result.error);
+ // Could add error handling here
+ }
+ }
+ catch (error) {
+ console.error('Network error updating reason:', error);
+ }
+ finally {
+ statusUpdateLoading = false;
+ }
+ }
+
+ // Cancel reason editing
+ function cancelReasonEditing() {
+ editingReasonFor = null;
+ reasonInput = '';
+ }
+
// Handle Escape key to cancel editing
$effect(() => {
if (!editingEntryId) return;
@@ -215,7 +276,7 @@
}
-
+
{#if isLoading}
@@ -230,7 +291,7 @@
-
← Back to Cohort Attendance
+
← Back to {fromSearch ? 'Admin Dashboard' : 'Cohort Attendance'}
{stats.apprenticeName} - Attendance
{#if stats.cohortName}
{stats.cohortName}
@@ -240,7 +301,7 @@
-
-
Attendance History
+
+
Attendance History
+ {history.length} event{history.length !== 1 ? 's' : ''}
+
{#if history.length === 0}
@@ -268,7 +332,8 @@
Event
Date & Time
Status
-
Check-in Time
+
Check-in Time
+
Reason
@@ -300,7 +365,7 @@
{/if}
-
+
{#if editingEntryId === entry.eventId}
{#if editingStatus === 'Present' || editingStatus === 'Late'}
@@ -332,6 +397,46 @@
—
{/if}
+
+ {#if entry.status === 'Absent' || entry.status === 'Excused'}
+ {#if editingReasonFor === entry.eventId}
+
+
+
+
+ Cancel
+
+
+ {statusUpdateLoading ? 'Saving...' : 'Save'}
+
+
+
+ {:else}
+ startEditingReason(entry)}
+ class="text-gray-600 hover:text-blue-600 text-xs cursor-pointer transition-colors px-2 py-1 rounded hover:bg-gray-100"
+ >
+ {entry.reason ? entry.reason : 'Add reason...'}
+
+ {/if}
+ {:else}
+ —
+ {/if}
+
{/each}
diff --git a/src/routes/admin/events/+page.server.ts b/src/routes/admin/events/+page.server.ts
index 6f3618f..754b692 100644
--- a/src/routes/admin/events/+page.server.ts
+++ b/src/routes/admin/events/+page.server.ts
@@ -1,13 +1,15 @@
import type { PageServerLoad } from './$types';
import { listEvents, listCohorts } from '$lib/airtable/sveltekit-wrapper';
import { DEFAULTS } from '$lib/airtable/config';
+import { eventTypesService } from '$lib/services/event-types';
export const load: PageServerLoad = async ({ url }) => {
const cohortId = url.searchParams.get('cohort') ?? undefined;
- const [events, cohorts] = await Promise.all([
+ const [events, cohorts, eventTypes] = await Promise.all([
listEvents({ cohortId }),
listCohorts(),
+ eventTypesService.getEventTypes(),
]);
// Sort cohorts reverse alphabetically (newest cohorts first)
@@ -16,6 +18,7 @@ export const load: PageServerLoad = async ({ url }) => {
return {
events,
cohorts,
+ eventTypes,
selectedCohortId: cohortId,
defaultSurveyUrl: DEFAULTS.SURVEY_URL,
};
diff --git a/src/routes/admin/events/+page.svelte b/src/routes/admin/events/+page.svelte
index 5446853..8df24c7 100644
--- a/src/routes/admin/events/+page.svelte
+++ b/src/routes/admin/events/+page.svelte
@@ -4,7 +4,7 @@
import { tick } from 'svelte';
import { slide } from 'svelte/transition';
import { SvelteSet } from 'svelte/reactivity';
- import { EVENT_TYPES, EVENT_TYPE_COLORS, type EventType, type Event as AppEvent } from '$lib/types/event';
+ import { type EventType, type Event as AppEvent } from '$lib/types/event';
import { ATTENDANCE_STATUSES, getStatusBadgeClass, type AttendanceStatus } from '$lib/types/attendance';
import { Calendar, DayGrid, Interaction } from '@event-calendar/core';
import '@event-calendar/core/index.css';
@@ -13,6 +13,23 @@
let { data } = $props();
+ // Extract event types from server data
+ const eventTypes = $derived(data.eventTypes);
+ const eventTypeColors = $derived(() => {
+ const colorMap: Record
= {};
+ eventTypes.forEach((type) => {
+ colorMap[type.name] = { main: type.color, tailwind: type.tailwindClass };
+ });
+ return colorMap;
+ });
+
+ // Helper function to get default survey URL for an event type
+ function getDefaultSurveyUrl(eventTypeName: string): string {
+ if (!eventTypeName) return '';
+ const type = eventTypes.find(t => t.name === eventTypeName);
+ return type?.defaultSurveyUrl || data.defaultSurveyUrl;
+ }
+
// Sorting state
type SortColumn = 'name' | 'dateTime' | 'eventType' | 'cohort' | 'attendance';
type SortDirection = 'asc' | 'desc';
@@ -164,7 +181,7 @@
title: event.name || '(Untitled)',
start: start.toISOString().slice(0, 16).replace('T', ' '),
end: end.toISOString().slice(0, 16).replace('T', ' '),
- color: EVENT_TYPE_COLORS[event.eventType]?.main || '#3b82f6',
+ color: eventTypeColors()[event.eventType]?.main || '#3b82f6',
};
});
@@ -256,17 +273,16 @@
// Inline event creation state
let isAddingEvent = $state(false);
let tableContainer = $state(null);
- // svelte-ignore state_referenced_locally
let newEvent = $state({
name: '',
date: '',
startTime: '10:00',
endTime: '14:00',
cohortIds: [] as string[],
- eventType: EVENT_TYPES[0] as EventType,
+ eventType: '',
isPublic: false,
checkInCode: '' as string | number,
- surveyUrl: data.defaultSurveyUrl,
+ surveyUrl: '',
});
let addEventError = $state('');
let addEventSubmitting = $state(false);
@@ -283,6 +299,32 @@
}
});
+ // Auto-populate survey URL when event type changes for regular event
+ $effect(() => {
+ if (newEvent.eventType) {
+ newEvent.surveyUrl = getDefaultSurveyUrl(newEvent.eventType);
+ }
+ });
+
+ // ESC key handler for canceling forms
+ $effect(() => {
+ function handleKeydown(event: KeyboardEvent) {
+ if (event.key === 'Escape') {
+ if (isAddingEvent) {
+ cancelAddEvent();
+ }
+ else if (isCreatingSeries) {
+ cancelSeriesForm();
+ }
+ }
+ }
+
+ if (isAddingEvent || isCreatingSeries) {
+ document.addEventListener('keydown', handleKeydown);
+ return () => document.removeEventListener('keydown', handleKeydown);
+ }
+ });
+
// Auto-generate check-in code when isPublic is checked, clear when unchecked
let prevNewEventIsPublic = $state(false);
$effect(() => {
@@ -303,7 +345,7 @@
startTime: '',
endTime: '',
cohortIds: [] as string[],
- eventType: EVENT_TYPES[0] as EventType,
+ eventType: eventTypes[0]?.name || '',
isPublic: false,
checkInCode: '' as string | number,
surveyUrl: '',
@@ -349,11 +391,17 @@
let seriesTime = $state('10:00');
let seriesEndTime = $state('11:00');
let seriesCohortIds = $state([]);
- let seriesEventType = $state(EVENT_TYPES[0]);
+ let seriesEventType = $state('');
let seriesIsPublic = $state(false);
let seriesCheckInCode = $state('');
- // svelte-ignore state_referenced_locally
- let seriesSurveyUrl = $state(data.defaultSurveyUrl);
+ let seriesSurveyUrl = $state('');
+
+ // Auto-populate survey URL when event type changes for series
+ $effect(() => {
+ if (seriesEventType) {
+ seriesSurveyUrl = getDefaultSurveyUrl(seriesEventType);
+ }
+ });
let seriesError = $state('');
let seriesSubmitting = $state(false);
let seriesProgress = $state<{ created: number; total: number } | null>(null);
@@ -608,7 +656,7 @@
startTime: '10:00',
endTime: '14:00',
cohortIds: [],
- eventType: EVENT_TYPES[0],
+ eventType: eventTypes[0]?.name || '',
isPublic: false,
checkInCode: '' as string | number,
surveyUrl: data.defaultSurveyUrl,
@@ -622,10 +670,10 @@
seriesTime = '10:00';
seriesEndTime = '11:00';
seriesCohortIds = [];
- seriesEventType = EVENT_TYPES[0];
+ seriesEventType = '';
seriesIsPublic = false;
seriesCheckInCode = '';
- seriesSurveyUrl = data.defaultSurveyUrl;
+ seriesSurveyUrl = '';
selectedDates = [];
seriesError = '';
seriesProgress = null;
@@ -912,7 +960,7 @@
}
-
+
← Back to Admin
Events
@@ -959,7 +1007,8 @@
await tick();
tableContainer?.scrollTo({ top: 0, behavior: 'smooth' });
}}
- class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
+ disabled={isCreatingSeries}
+ class="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-blue-600"
>
+ Add Event
@@ -1144,8 +1193,9 @@
bind:value={newEvent.eventType}
class="w-full border rounded px-2 py-1 text-sm"
>
- {#each EVENT_TYPES as type (type)}
- {type}
+ Select event type...
+ {#each eventTypes as type (type.name)}
+ {type.name}
{/each}
@@ -1233,20 +1283,29 @@
{#if showNewEventSurvey}
-
-
+
-
showNewEventSurvey = false}
- class="mt-1 text-xs text-gray-500 hover:text-gray-700"
- >
- Done
-
+ class="w-full border border-gray-300 rounded px-3 py-2 text-sm resize-none"
+ rows="3"
+ >
+
+ showNewEventSurvey = false}
+ class="px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
+ >
+ Done
+
+ newEvent.surveyUrl = ''}
+ class="px-3 py-1 bg-gray-200 text-gray-700 text-xs rounded hover:bg-gray-300"
+ >
+ Clear
+
+
{/if}
@@ -1317,8 +1376,8 @@
class="w-full border rounded px-2 py-1 text-sm"
onclick={e => e.stopPropagation()}
>
- {#each EVENT_TYPES as type (type)}
-
{type}
+ {#each eventTypes as type (type.name)}
+
{type.name}
{/each}
@@ -1418,24 +1477,36 @@
{#if showEditEventSurvey}
-
-
+
+
+ {
+ e.stopPropagation();
+ showEditEventSurvey = false;
+ }}
+ class="px-3 py-1 bg-blue-600 text-white text-xs rounded hover:bg-blue-700"
+ >
+ Done
+
+ {
+ e.stopPropagation();
+ editEvent.surveyUrl = '';
+ }}
+ class="px-3 py-1 bg-gray-200 text-gray-700 text-xs rounded hover:bg-gray-300"
+ >
+ Clear
+
+
{/if}
@@ -1517,7 +1588,7 @@
{#if event.eventType}
-
+
{event.eventType}
{:else}
@@ -1717,7 +1788,8 @@
{#if !isCreatingSeries}
{ isCreatingSeries = true; }}
- class="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 text-sm"
+ disabled={isAddingEvent}
+ class="bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700 text-sm disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-green-600"
>
+ Create Series
@@ -1794,8 +1866,9 @@
required
class="w-full border rounded px-3 py-2"
>
- {#each EVENT_TYPES as type (type)}
- {type}
+ Select event type...
+ {#each eventTypes as type (type.name)}
+ {type.name}
{/each}
@@ -1863,13 +1936,24 @@
Survey URL
-
+
+
+ {#if seriesSurveyUrl}
+ seriesSurveyUrl = ''}
+ class="px-3 py-1 bg-gray-200 text-gray-700 text-xs rounded hover:bg-gray-300"
+ >
+ Clear
+
+ {/if}
+
{#if seriesIsPublic}
@@ -1956,13 +2040,13 @@
- {#each EVENT_TYPES as type (type)}
+ {#each eventTypes as type (type.name)}
- {type}
+ {type.name}
{/each}
{#if isCreatingSeries}
diff --git a/src/routes/api/apprentices/search/+server.ts b/src/routes/api/apprentices/search/+server.ts
new file mode 100644
index 0000000..549c1b3
--- /dev/null
+++ b/src/routes/api/apprentices/search/+server.ts
@@ -0,0 +1,76 @@
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { AIRTABLE_API_KEY, AIRTABLE_BASE_ID_LEARNERS } from '$env/static/private';
+import Airtable from 'airtable';
+import { TABLES, APPRENTICE_FIELDS, COHORT_FIELDS } from '$lib/airtable/config';
+
+export const GET: RequestHandler = async ({ url }) => {
+ const query = url.searchParams.get('q')?.trim();
+
+ // Require at least 2 characters to search
+ if (!query || query.length < 2) {
+ return json({ success: true, apprentices: [] });
+ }
+
+ try {
+ // Configure Airtable
+ Airtable.configure({ apiKey: AIRTABLE_API_KEY });
+ const base = Airtable.base(AIRTABLE_BASE_ID_LEARNERS);
+ const apprenticesTable = base(TABLES.APPRENTICES);
+ const cohortsTable = base(TABLES.COHORTS);
+
+ // Get all apprentices
+ const apprenticeRecords = await apprenticesTable
+ .select({
+ returnFieldsByFieldId: true,
+ })
+ .all();
+
+ // Get all cohorts to map cohort IDs to numbers
+ const cohortRecords = await cohortsTable
+ .select({
+ returnFieldsByFieldId: true,
+ })
+ .all();
+
+ const cohortMap = new Map
();
+ cohortRecords.forEach((record) => {
+ const cohortNumber = record.get(COHORT_FIELDS.NUMBER) as string;
+ cohortMap.set(record.id, parseInt(cohortNumber) || 0);
+ });
+
+ // Filter and format results
+ const searchResults = apprenticeRecords
+ .filter((record) => {
+ const name = record.get(APPRENTICE_FIELDS.NAME) as string;
+ return name && name.toLowerCase().includes(query.toLowerCase());
+ })
+ .slice(0, 10) // Limit to 10 results
+ .map((record) => {
+ // Email is a lookup field, returns array
+ const emailLookup = record.get(APPRENTICE_FIELDS.EMAIL) as string[] | undefined;
+ const cohortIds = record.get(APPRENTICE_FIELDS.COHORT) as string[] | undefined;
+ const cohortNumbers = cohortIds?.map(id => cohortMap.get(id)).filter(Boolean) || [];
+
+ return {
+ id: record.id,
+ name: record.get(APPRENTICE_FIELDS.NAME) as string,
+ email: emailLookup?.[0] || '',
+ status: (record.get(APPRENTICE_FIELDS.STATUS) as string) || 'Active',
+ cohortNumbers,
+ };
+ });
+
+ return json({
+ success: true,
+ apprentices: searchResults,
+ });
+ }
+ catch (error) {
+ console.error('Failed to search apprentices:', error);
+ return json({
+ success: false,
+ error: 'Failed to search apprentices',
+ }, { status: 500 });
+ }
+};
diff --git a/src/routes/api/attendance/[id]/+server.ts b/src/routes/api/attendance/[id]/+server.ts
index 5862168..dc1b571 100644
--- a/src/routes/api/attendance/[id]/+server.ts
+++ b/src/routes/api/attendance/[id]/+server.ts
@@ -30,6 +30,7 @@ export const PATCH: RequestHandler = async ({ params, request }) => {
const attendance = await updateAttendance(id, {
status: body.status,
checkinTime: body.checkinTime,
+ reason: body.reason,
});
return json({ success: true, attendance });
diff --git a/src/routes/api/checkin/absent/+server.ts b/src/routes/api/checkin/absent/+server.ts
index a3109e5..27b1b2b 100644
--- a/src/routes/api/checkin/absent/+server.ts
+++ b/src/routes/api/checkin/absent/+server.ts
@@ -10,7 +10,7 @@ export const POST: RequestHandler = async ({ cookies, request }) => {
return json({ success: false, error: 'Authentication required' }, { status: 401 });
}
- let body: { eventId?: string };
+ let body: { eventId?: string; reason?: string };
try {
body = await request.json();
}
@@ -18,7 +18,7 @@ export const POST: RequestHandler = async ({ cookies, request }) => {
return json({ success: false, error: 'Invalid JSON body' }, { status: 400 });
}
- const { eventId } = body;
+ const { eventId, reason } = body;
if (!eventId) {
return json({ success: false, error: 'eventId is required' }, { status: 400 });
@@ -47,6 +47,7 @@ export const POST: RequestHandler = async ({ cookies, request }) => {
const attendance = await markNotComing({
eventId,
apprenticeId: apprentice.id,
+ reason,
});
return json({
diff --git a/src/routes/api/event-types/+server.ts b/src/routes/api/event-types/+server.ts
new file mode 100644
index 0000000..7bb0537
--- /dev/null
+++ b/src/routes/api/event-types/+server.ts
@@ -0,0 +1,14 @@
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+import { eventTypesService } from '$lib/services/event-types';
+
+export const GET: RequestHandler = async () => {
+ try {
+ const eventTypes = await eventTypesService.getEventTypes();
+ return json({ success: true, eventTypes });
+ }
+ catch (error) {
+ console.error('Failed to fetch event types:', error);
+ return json({ success: false, error: 'Failed to fetch event types' }, { status: 500 });
+ }
+};
diff --git a/src/routes/api/events/+server.ts b/src/routes/api/events/+server.ts
index eaeea28..81d7905 100644
--- a/src/routes/api/events/+server.ts
+++ b/src/routes/api/events/+server.ts
@@ -1,7 +1,7 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { createEvent, listEvents } from '$lib/airtable/sveltekit-wrapper';
-import { EVENT_TYPES } from '$lib/types/event';
+import { eventTypesService } from '$lib/services/event-types';
export const GET: RequestHandler = async () => {
try {
@@ -29,12 +29,14 @@ export const POST: RequestHandler = async ({ request }) => {
return json({ success: false, error: 'Event type is required' }, { status: 400 });
}
- // Case-insensitive event type matching - normalize to Airtable's exact value
- const normalizedEventType = EVENT_TYPES.find(
- t => t.toLowerCase() === body.eventType.toLowerCase(),
- );
- if (!normalizedEventType) {
- return json({ success: false, error: 'Invalid event type' }, { status: 400 });
+ // Validate event type against Airtable data
+ const isValid = await eventTypesService.isValidEventType(body.eventType);
+ if (!isValid) {
+ const validTypes = await eventTypesService.getEventTypeNames();
+ return json({
+ success: false,
+ error: `Invalid event type. Must be one of: ${validTypes.join(', ')}`,
+ }, { status: 400 });
}
const event = await createEvent({
@@ -42,7 +44,7 @@ export const POST: RequestHandler = async ({ request }) => {
dateTime: body.dateTime,
endDateTime: body.endDateTime || undefined,
cohortIds: body.cohortIds || undefined,
- eventType: normalizedEventType,
+ eventType: body.eventType,
surveyUrl: body.surveyUrl || undefined,
isPublic: body.isPublic ?? false,
checkInCode: body.checkInCode || undefined,
diff --git a/src/routes/api/events/[id]/+server.ts b/src/routes/api/events/[id]/+server.ts
index bb775ad..369c0e2 100644
--- a/src/routes/api/events/[id]/+server.ts
+++ b/src/routes/api/events/[id]/+server.ts
@@ -1,7 +1,7 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { updateEvent, deleteEvent, getEvent } from '$lib/airtable/sveltekit-wrapper';
-import { EVENT_TYPES } from '$lib/types/event';
+import { eventTypesService } from '$lib/services/event-types';
export const PUT: RequestHandler = async ({ params, request }) => {
try {
@@ -16,8 +16,12 @@ export const PUT: RequestHandler = async ({ params, request }) => {
const body = await request.json();
// Validate event type if provided
- if (body.eventType && !EVENT_TYPES.includes(body.eventType)) {
- return json({ success: false, error: 'Invalid event type' }, { status: 400 });
+ if (body.eventType && !(await eventTypesService.isValidEventType(body.eventType))) {
+ const validTypes = await eventTypesService.getEventTypeNames();
+ return json({
+ success: false,
+ error: `Invalid event type. Must be one of: ${validTypes.join(', ')}`,
+ }, { status: 400 });
}
const event = await updateEvent(id, {
diff --git a/src/routes/checkin/+page.svelte b/src/routes/checkin/+page.svelte
index 2192d50..f8fc605 100644
--- a/src/routes/checkin/+page.svelte
+++ b/src/routes/checkin/+page.svelte
@@ -20,6 +20,10 @@
let markingNotComing = $state(null);
let checkInError = $state(null);
+ // Absence reason state
+ let showingReasonFor = $state(null);
+ let absenceReason = $state('');
+
// Guest check-in state
let guestStep = $state<'code' | 'events' | 'details' | 'success'>('code');
let guestCode = $state('');
@@ -125,8 +129,21 @@
}
}
+ // Show absence reason input
+ function showReasonInput(eventId: string) {
+ showingReasonFor = eventId;
+ absenceReason = '';
+ checkInError = null;
+ }
+
+ // Hide absence reason input
+ function hideReasonInput() {
+ showingReasonFor = null;
+ absenceReason = '';
+ }
+
// Authenticated user mark as absent
- async function handleNotComing(eventId: string) {
+ async function handleNotComing(eventId: string, reason?: string) {
// Prevent double-clicking by checking if already processing this event
if (markingNotComing === eventId || checkingIn === eventId) {
return;
@@ -139,7 +156,7 @@
const response = await fetch('/api/checkin/absent', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ eventId }),
+ body: JSON.stringify({ eventId, reason }),
});
const result = await response.json();
@@ -167,6 +184,7 @@
}
finally {
markingNotComing = null;
+ hideReasonInput();
}
}
@@ -268,111 +286,153 @@
Check In - Apprentice Pulse
-
+
{#if data.authenticated}
-