diff --git a/.claude/hooks/plan-iterator.sh b/.claude/hooks/plan-iterator.sh
index 5158df1..bcba71e 100755
--- a/.claude/hooks/plan-iterator.sh
+++ b/.claude/hooks/plan-iterator.sh
@@ -82,25 +82,34 @@ IFS=$'\t' read -r REMAINING COMPLETED NEXT_TASK LAST_DONE TO_COMPLETE < <(
}
# Main task: incomplete (strict [ ])
- match($0, /^([0-9]+)\.[[:space:]]*\[ \][[:space:]]*(.*)$/, m) {
- n = m[1]
+ /^[0-9]+\.[[:space:]]*\[ \][[:space:]]*/ {
+ n = $0
+ sub(/\..*$/, "", n)
+ text = $0
+ sub(/^[0-9]+\.[[:space:]]*\[ \][[:space:]]*/, "", text)
main_state[n] = "incomplete"
- main_text[n] = trim(m[2])
+ main_text[n] = trim(text)
next
}
# Main task: complete (strict [x])
- match($0, /^([0-9]+)\.[[:space:]]*\[x\][[:space:]]*(.*)$/, m) {
- n = m[1]
+ /^[0-9]+\.[[:space:]]*\[x\][[:space:]]*/ {
+ n = $0
+ sub(/\..*$/, "", n)
+ text = $0
+ sub(/^[0-9]+\.[[:space:]]*\[x\][[:space:]]*/, "", text)
main_state[n] = "complete"
- main_text[n] = trim(m[2])
+ main_text[n] = trim(text)
next
}
# Subtask: strict [ ] or [x], and requires a leading main number like 1.1, 2.3 etc
- match($0, /^[[:space:]]*-[[:space:]]*\[([ x])\][[:space:]]*([0-9]+)\.[0-9]+[[:space:]]*(.*)$/, m) {
- status = m[1]
- n = m[2]
+ /^[[:space:]]*-[[:space:]]*\[[ x]\][[:space:]]*[0-9]+\.[0-9]+[[:space:]]*/ {
+ status = ($0 ~ /^[[:space:]]*-[[:space:]]*\[x\]/) ? "x" : " "
+ tmp = $0
+ sub(/^[[:space:]]*-[[:space:]]*\[[ x]\][[:space:]]*/, "", tmp)
+ n = tmp
+ sub(/\..*$/, "", n)
sub_any[n] = 1
if (status == "x") {
@@ -168,11 +177,32 @@ if [[ -n "${TO_COMPLETE:-}" ]]; then
BEGIN { remaining=0; completed=0; next_task=""; last_done="" }
- match($0, /^([0-9]+)\.[[:space:]]*\[ \][[:space:]]*(.*)$/, m) { main_state[m[1]]="incomplete"; main_text[m[1]]=trim(m[2]); next }
- match($0, /^([0-9]+)\.[[:space:]]*\[x\][[:space:]]*(.*)$/, m) { main_state[m[1]]="complete"; main_text[m[1]]=trim(m[2]); next }
+ /^[0-9]+\.[[:space:]]*\[ \][[:space:]]*/ {
+ n = $0
+ sub(/\..*$/, "", n)
+ text = $0
+ sub(/^[0-9]+\.[[:space:]]*\[ \][[:space:]]*/, "", text)
+ main_state[n]="incomplete"
+ main_text[n]=trim(text)
+ next
+ }
+ /^[0-9]+\.[[:space:]]*\[x\][[:space:]]*/ {
+ n = $0
+ sub(/\..*$/, "", n)
+ text = $0
+ sub(/^[0-9]+\.[[:space:]]*\[x\][[:space:]]*/, "", text)
+ main_state[n]="complete"
+ main_text[n]=trim(text)
+ next
+ }
- match($0, /^[[:space:]]*-[[:space:]]*\[([ x])\][[:space:]]*([0-9]+)\.[0-9]+[[:space:]]*(.*)$/, m) {
- status=m[1]; n=m[2]; sub_any[n]=1
+ /^[[:space:]]*-[[:space:]]*\[[ x]\][[:space:]]*[0-9]+\.[0-9]+[[:space:]]*/ {
+ status = ($0 ~ /^[[:space:]]*-[[:space:]]*\[x\]/) ? "x" : " "
+ tmp = $0
+ sub(/^[[:space:]]*-[[:space:]]*\[[ x]\][[:space:]]*/, "", tmp)
+ n = tmp
+ sub(/\..*$/, "", n)
+ sub_any[n]=1
if (status=="x") { completed++; last_done=after_checkbox($0) }
else { remaining++; if (next_task=="") next_task=after_checkbox($0) }
next
diff --git a/README.md b/README.md
index 7e840f6..7011005 100644
--- a/README.md
+++ b/README.md
@@ -28,14 +28,15 @@ The app uses **magic link authentication** with a single login page for all user
|------|-------------------|--------------|
| Staff | Staff table (collaborator email) | `/admin` |
| Student | Apprentices table (learner email) | `/checkin` |
+| External | Staff table (external access fields) | `/admin` |
### How It Works
1. User enters email at `/login`
-2. Server checks Staff table first, then Apprentices table
+2. Server checks Staff table first, then external access fields, then Apprentices table
3. JWT token generated (15-minute expiry) and emailed via Resend
4. User clicks link → token verified → session cookie set (90-day expiry)
-5. User redirected based on type: staff → `/admin`, students → `/checkin`
+5. User redirected based on type: staff/external → `/admin`, students → `/checkin`
### Route Protection
@@ -64,6 +65,39 @@ See [Staff Who Are Also Apprentices](#staff-who-are-also-apprentices) for setup
2. Add a record in the **Staff - Apprentice Pulse** table, selecting their collaborator profile
3. They can now log in at `/login` using their collaborator email
+### Adding External Staff Access
+
+External staff are people who need login access to view attendance data but are not regular staff members or apprentices. Examples include external trainers, consultants, or partner organization staff.
+
+To grant external staff access:
+
+1. **In the Staff - Apprentice Pulse table**, find any existing staff record (or create a dummy one)
+2. **Add the external person's email** to the `Attendace access` field (this field can contain multiple emails, one per line)
+3. **Add their name** to the `Name - Attendance access` field (this should match the order of emails in step 2)
+
+**Example setup:**
+- `Attendace access` field: `external.trainer@company.com`
+- `Name - Attendance access` field: `External Trainer`
+
+**How it works:**
+- External staff can log in at `/login` using their email
+- They receive the same magic link authentication as regular staff
+- They have **full admin access** - same permissions as regular staff members
+- No Airtable workspace collaboration required
+
+**Multiple external users:**
+You can add multiple external users to the same staff record by putting each email on a new line:
+- `Attendace access` field:
+ ```
+ trainer1@company.com
+ trainer2@company.com
+ ```
+- `Name - Attendance access` field:
+ ```
+ External Trainer 1
+ External Trainer 2
+ ```
+
### Staff Who Are Also Apprentices
Some staff members may also be apprentices (e.g., apprentice coaches). These users need to:
@@ -193,10 +227,4 @@ Event types are cached for performance (5-minute cache) and include automatic co
### Default Values
-Default values used in forms are stored in `src/lib/airtable/config.ts` under `DEFAULTS`:
-
-| Key | Description |
-|-----|-------------|
-| `SURVEY_URL` | Default survey URL pre-filled when creating events |
-
-To change the default survey URL, update `DEFAULTS.SURVEY_URL` in the config file.
+Default values for event creation are now managed dynamically through Airtable's "Event types - Apprentice Pulse" table. Each event type can have its own default survey URL, providing more flexibility than hardcoded values.
diff --git a/docs/plan-search-apprentice.md b/docs/plan-search-apprentice.md
deleted file mode 100644
index 1dcafee..0000000
--- a/docs/plan-search-apprentice.md
+++ /dev/null
@@ -1,204 +0,0 @@
-# Search Apprentice Feature - Implementation Plan
-
-## Overview
-Add a Search Apprentice card to the admin dashboard that allows users to quickly search for apprentices by name and navigate directly to their detail page.
-
-## User Flow
-1. User types in search field on admin dashboard
-2. System shows live search results matching the query
-3. User clicks on an apprentice from results
-4. System navigates to apprentice detail page (`/admin/apprentices/[id]`)
-
-## Technical Architecture
-
-### 1. Search API Endpoint
-**File**: `src/routes/api/apprentices/search/+server.ts`
-
-- **Method**: GET
-- **Query params**: `q` (search query)
-- **Returns**: Array of matching apprentices with id, name, email, cohort
-- **Search logic**:
- - Case-insensitive search
- - Matches against apprentice name
- - Returns up to 10 results
- - Orders by relevance/alphabetical
-
-### 2. Search Component
-**File**: `src/lib/components/SearchApprentice.svelte`
-
-**Features**:
-- Text input field with search icon
-- Debounced search (300ms) to avoid excessive API calls
-- Loading state while searching
-- Dropdown results list
-- Keyboard navigation (arrow keys + enter)
-- Click outside to close
-- Empty state when no results
-- Error handling
-
-**Props**:
-- No props needed (self-contained)
-
-**State**:
-- `searchQuery`: Current search text
-- `searchResults`: Array of apprentice results
-- `isSearching`: Loading state
-- `showResults`: Dropdown visibility
-- `selectedIndex`: For keyboard navigation
-
-### 3. Dashboard Integration
-**File**: `src/routes/admin/+page.svelte`
-
-**Add new card**:
-```svelte
-
-
Welcome, {data.user?.email}
+Welcome, {data.user?.email}
+ {#if data.user?.type === 'staff'} + + Staff + + {:else if data.user?.type === 'external'} + + External Staff + + {/if} +Welcome back{data.user?.name ? `, ${data.user.name}` : ''}!
+Logged as: {data.user?.email}
{formatDate(event.dateTime)}
-{formatDate(event.dateTime)}
+Check in to an event as a guest
-Guest access - no account required
+Enter the 4-digit event code displayed at the venue.
- - - {#if guestError} -
- Have an account?
-
-
Enter the 4-digit event code displayed at the venue.
+ + + {#if guestError} +
+ Have an account?
+
+
Select the event you want to check in to:
+Select the event you want to check in to:
-{formatDate(event.dateTime)}
-{formatDate(event.dateTime)}
+{guestSelectedEvent ? formatDate(guestSelectedEvent.dateTime) : ''}
- {#if guestTimeStatus} - - {guestTimeStatus.text} - - {/if} -{guestSelectedEvent ? formatDate(guestSelectedEvent.dateTime) : ''}
+ {#if guestTimeStatus} + + {guestTimeStatus.text} + + {/if} +Welcome to {guestSelectedEvent?.name}
+Welcome to {guestSelectedEvent?.name}
+