Volunteers CRM
Volunteers CRM
Component File: src/features/volunteers/components/VolunteersCRM.tsx Route / navigation: Path /people, Zustand volunteerTool = volunteer-table. See 00-PEOPLE-HUB.md. Access Level: Parent Org and Fund Users with People Hub access (position-based) Last Updated: April 10, 2026
> Data Layer: This component uses Supabase client functions in src/lib/db.ts (e.g., fetchVolunteers(), createVolunteer(), updateVolunteer()). There is no internal REST API; use Supabase clients, RPC, and edge functions.
Recent Updates (March 30, 2026)
Volunteer profile: Hours vs History tabs + event sign-ups
- Change: The first profile tab is now Hours (default)—volunteer hour entries with add / edit / delete / approve / reject (same content as before). A new History tab to its right lists event sign-ups from the volunteer scheduler: Upcoming events and Past events, with event name, date, time, location (or Virtual), and signup status. Data loads via
fetchVolunteerSignupsByVolunteer(volunteerId, true)fromsrc/lib/db/volunteer-events.tswhen the History tab is active. - Hours table: Layout no longer uses
table-fixedwith tight pixel columns; Actions is right-aligned with a stable column width to avoid header/body misalignment (especially when the Notes column is hidden on small screens).
Operational Note (April 7, 2026)
Volunteer login email troubleshooting
- Login-link sends from this page (
Create Login/Resend Login Link) go throughsendVolunteerMagicLink-> edge functionsend-volunteer-magic-link-> Supabase AuthsignInWithOtp. - These are not marketing sends (
send-marketing-email); they rely on Supabase Auth mailer/SMTP. - The resend/create-login path now heals incomplete volunteer setup before sending:
- links
volunteers.user_idwhen an auth user already exists for the email - creates a volunteer
organization_usersrow only when the user has no existing org membership - preserves existing staff memberships such as
fund_user/parent_org - Volunteers with no auth account yet still require the send action to create the auth user; fixing code alone does not backfill missing accounts automatically.
- For production incidents (emails not received), follow `VOLUNTEER-LOGIN-EMAIL-TRIAGE-RUNBOOK.md`.
Recent Updates (March 6, 2026)
Robust Volunteer Search + Global Search Deep-Linking (P1)
- Issue: Volunteer list search was still effectively name/email-only, and global search could not open volunteer profiles directly.
- Fix:
fetchVolunteersPaginated()now uses shared search helpers fromsrc/lib/searchUtils.tsplus a volunteer-specific matcher insrc/lib/db/volunteers.ts.globalSearch()now returns volunteer results, andVolunteersCRM.tsxcan open a volunteer profile directly from a global-search hit. - Search now matches: volunteer name, email, phone, address, skills, availability, status, total hours, notes, emergency contact fields, tags, last activity date, and created date.
- Files Changed:
src/lib/db/volunteers.ts,src/lib/db.ts,src/components/shared/GlobalSearch.tsx,src/features/volunteers/components/VolunteersCRM.tsx,src/lib/searchUtils.ts
Overview
The Volunteers CRM manages volunteer information, tracks volunteer hours, and coordinates volunteer activities. Nonprofits can add, edit, and manage volunteer hour entries directly from volunteer profiles.
UI Features
Main Features
- Volunteer List View:
- Searchable and sortable table
- Server-side search by name, email, phone, address, skills, availability, status, hours, notes, emergency contacts, tags, and activity dates
- Filter by volunteer type (Regular, Occasional, Event-based)
- Quick actions menu
- Volunteer Profile View:
- Personal information (name, email, phone, address)
- Tabbed detail: Hours (hour log and approvals), History (scheduler event sign-ups), background check, onboarding, notes, waivers, emergency contact
- Total hours contributed (summary cards)
- Background check status (dedicated tab)
- Add Volunteer:
- Name, email, phone, address
- Organization assignment
- Optional: Create login account (OTP-based)
- Hour Management (in profile, Hours tab):
- Add new hour entries
- Edit existing hour entries
- Delete hour entries
- View and manage the full hour log
- Approve or reject pending hour entries
- Status badges (Approved, Pending, Rejected) on each entry
Volunteer Table Columns
- Name (with avatar)
- Volunteer Type Badge
- Phone
- Total Hours
- Last Activity Date
- Actions dropdown
Profile Tabs (pill navigation, left to right)
- Hours (default) — Volunteer hour table: date, hours, activity, status, notes (on wider breakpoints), row actions (approve / reject / edit / delete). Add Hours in the card header.
- History — Scheduler event sign-ups for this volunteer: Upcoming events (on or after today) and Past events (before today), each in its own table (event, date, time range, location, signup status).
- Background Check — Status, provider, dates, request flow, manual updates
- Onboarding — Org onboarding steps and completion
- Notes — Shared notes component / activity
- Waivers — Org waivers and this volunteer’s signatures
- Emergency — Emergency contact information
Summary Cards (Profile)
- Total Hours: Lifetime volunteer hours
- Sessions: Count of hour entries (sessions)
- Last Visit: Most recent volunteer date from hour entries
Action Buttons (Profile)
- Note - Add notes to volunteer profile
- Edit - Edit volunteer information
Data Requirements
Volunteer Data
- id (uuid) - Unique identifier
- organization_id (uuid) - Organization owner
- first_name (string) - First name
- last_name (string) - Last name
- email (string) - Email address
- phone (string, nullable) - Phone number
- address (json, nullable) - Mailing address
- date_of_birth (date, nullable) - Date of birth
- emergency_contact (json, nullable) - Emergency contact info
- volunteer_type (string) - 'regular', 'occasional', 'event_based'
- status (string) - 'active', 'inactive'
- start_date (date) - Volunteer start date
- skills (array, nullable) - Skills and certifications
- interests (array, nullable) - Areas of interest
- availability (json, nullable) - Availability schedule
- background_check_status (string, nullable) - 'pending', 'approved', 'expired'
- background_check_date (date, nullable) - When completed
- total_hours (decimal) - Lifetime hours
- ytd_hours (decimal) - Year-to-date hours
- last_activity_date (date, nullable) - Most recent activity
- notes (text, nullable) - Internal notes
- created_at (datetime) - When created
- updated_at (datetime) - When updated
Activity Assignment Data
- id (uuid) - Assignment ID
- volunteer_id (uuid) - Volunteer reference
- activity_name (string) - Activity name
- activity_type (string) - Type of activity
- scheduled_date (date) - When scheduled
- hours_expected (decimal, nullable) - Expected hours
- hours_actual (decimal, nullable) - Actual hours worked
- status (string) - 'scheduled', 'completed', 'cancelled'
- notes (text, nullable) - Activity notes
Recognition Data
- id (uuid) - Recognition ID
- volunteer_id (uuid) - Volunteer reference
- recognition_type (string) - 'milestone', 'award', 'thank_you'
- title (string) - Recognition title
- description (text, nullable) - Description
- date (date) - Recognition date
- given_by_id (uuid) - Who gave recognition
Data Mutations
- Create Volunteer: Add new volunteer
- Update Volunteer: Edit volunteer details
- Delete Volunteer: Remove volunteer (soft delete)
- Assign Activity: Assign volunteer to activity
- Complete Activity: Mark activity as completed
- Add Recognition: Give award or recognition
- Update Background Check: Update background check status
Data Access Layer
> Architecture: All data access is via Supabase client functions in src/lib/db.ts; use RPC and edge functions where server-side logic is required.
Authentication & Authorization
Role-Based Access (3-Layer Model)
- Parent Org Admin: Full access to all volunteers across child orgs
- Fund User (director/bookkeeper): Full access within assigned org(s)
- Fund User (assistant): View + edit within assigned org(s)
- Volunteer: Self-service portal only (cannot access CRM)
RLS Policies
All queries are scoped by organization_id via Row Level Security. Parent org admins bypass via is_parent_org_admin() function.
Business Logic & Validations
Frontend Validations
- Email format validation
- Phone format validation
- Email uniqueness checked via
checkEmailExists() - Background check status managed via dedicated
background_checkstable
Business Rules
- Hour entries submitted by volunteers default to
'pending'status - Only directors/bookkeepers can approve/reject hour entries
- Background check tracking is per-person (shared
background_checkstable) - Onboarding checklist progress tracked per-volunteer
- Waiver signatures linked to volunteer records
total_hourssynced bytrg_sync_volunteer_total_hoursDB trigger
State Management
Local State
volunteers- List of volunteersselectedVolunteer- Currently viewing/editingview- 'list' or 'profile'activeTab- Profile tab:'hours' | 'history' | 'background-check' | 'onboarding' | 'notes' | 'waivers' | 'emergency'(default'hours')addVolunteerOpen- Add dialog statesearchQuery- Search inputfilters- Type and status filterssortBy- Sort option
Global State (Zustand Store)
selectedEntity- Current organization (fromuseAppStore)pendingSearchResult- Deep-link target from Global Search so volunteer profiles can open directly
Dependencies
Internal Dependencies
useAppStore- Zustand global statefetchVolunteersPaginated- Paginated volunteer data from SupabaseuseCreateVolunteer,useUpdateVolunteer,useDeleteVolunteer- Mutation hooksuseCreateHourEntry,useUpdateHourEntry,useDeleteHourEntry- Hour entry hooksfetchWaivers,fetchWaiverSignatures- Waiver datafetchVolunteerSignupsByVolunteer- Event sign-ups for the History tab (src/lib/db/volunteer-events.ts)- UI components (Card, Button, Table, Dialog, etc.)
External Libraries
lucide-react- Iconssonner- Toast notifications
Error Handling
Error Scenarios
1. Network Error: Show toast "Unable to load volunteers", retry 2. Validation Error: Show inline field errors 3. Email Taken: Show error "Email already in use" 4. Cannot Assign: Show error "Cannot assign activity to inactive volunteer" 5. Background Check Expired: Show warning "Background check expired" 6. Permission Error: Show toast "You don't have permission"
Loading States
- Initial load: Skeleton table
- Profile load: Loading spinner
- Form submission: Disable buttons, show spinner
- Activity assignment: Show confirmation with spinner
Hour Approval/Rejection Workflow
Staff can approve or reject volunteer-submitted hour entries directly from the volunteer profile view.
How It Works
1. Volunteer submits hours via the Volunteer Portal (status: pending) 2. Staff opens volunteer profile in CRM → Hours tab 3. Each hour entry shows a Status badge (Approved/Pending/Rejected) 4. Staff clicks the actions dropdown (⋯) on any entry:
5. On approval, the database trigger trg_sync_volunteer_total_hours automatically recalculates volunteers.total_hours
- Approve — Sets
status: 'approved', recordsapproved_by(current user ID) andapproved_attimestamp - Reject — Sets
status: 'rejected' - Edit / Delete — Existing functionality
RLS Policy
- Only users with
positionofdirectororbookkeeperinorganization_userscan UPDATE or DELETEhour_entries - Volunteers with
position: 'custom'can only INSERT (submit) and SELECT (view) their own entries
Database Functions Used
// Approve hour entry
updateHourEntry(id, { status: 'approved', approved_by: userId, approved_at: timestamp })
// Reject hour entry
updateHourEntry(id, { status: 'rejected' })Implementation Status
Last Updated: March 30, 2026
| Feature | Status | Notes |
|---------|--------|-------|
| Volunteer List (paginated) | ✅ Complete | Server-side search, sort, filter |
| Add Volunteer | ✅ Complete | Creates in `volunteers` table |
| Create Login Account | ✅ Complete | Sends magic link via `sendVolunteerMagicLink()` |
| Resend Login Link | ✅ Complete | Shows "Resend Login Link" for volunteers with existing userId (Feb 8, 2026) |
| Edit Volunteer Profile | ✅ Complete | Updates via `updateVolunteer()` |
| Delete Volunteer | ✅ Complete | Deletes from database |
| Notes System | ✅ Complete | Persisted as JSON in `notes` field |
| Add Hour Entry | ✅ Complete | Creates in `hour_entries` table |
| Edit Hour Entry | ✅ Complete | Updates via `updateHourEntry()` with real ID |
| Delete Hour Entry | ✅ Complete | Deletes via `deleteHourEntry()` with real ID |
| Approve/Reject Hours | ✅ Complete | Status badges + dropdown actions (Feb 7, 2026) |
| Profile Hours / History tabs | ✅ Complete | **Hours** default; **History** shows upcoming/past scheduler sign-ups (Mar 30, 2026) |
| Export CSV | ✅ Complete | Client-side export |
| Waiver Signatures | ✅ Complete | Fetches from `waiver_signatures` table |
Database Tables Used
volunteers Table
| Column | Type | Description |
|--------|------|-------------|
| `id` | uuid | Primary key |
| `organization_id` | uuid | FK to organizations |
| `user_id` | uuid | Optional FK to users (for login) |
| `first_name` | text | First name |
| `last_name` | text | Last name |
| `email` | text | Email address |
| `phone` | text | Phone number |
| `skills` | text[] | Array of skills |
| `availability` | text | Availability description |
| `status` | text | 'active' or 'inactive' |
| `total_hours` | numeric | Lifetime volunteer hours |
| `notes` | text | JSON array of notes |
| `created_at` | timestamp | Created timestamp |
| `updated_at` | timestamp | Updated timestamp |
hour_entries Table
| Column | Type | Description |
|--------|------|-------------|
| `id` | uuid | Primary key |
| `organization_id` | uuid | FK to organizations |
| `volunteer_id` | uuid | FK to volunteers |
| `date` | date | Date of volunteer work |
| `hours` | numeric | Hours worked |
| `activity` | text | Activity description |
| `description` | text | Additional notes |
| `status` | text | 'pending', 'approved', 'rejected' |
| `approved_by` | uuid | FK to users (approver) |
| `approved_at` | timestamp | Approval timestamp |
| `created_at` | timestamp | Created timestamp |
Related Tables
volunteer_signups- Links volunteers tovolunteer_events(used for profile History tab)volunteer_events,volunteer_event_slots- Scheduler events and slotswaiver_signatures- Signed waivers linked to volunteerswaivers- Waiver templates
Database Functions (src/lib/db.ts)
| Function | Purpose |
|----------|---------|
| `fetchVolunteersPaginated()` | Server-side search, pagination, filtering |
| `fetchVolunteers()` | Simple fetch with status filter |
| `fetchVolunteerById()` | Get single volunteer |
| `createVolunteer()` | Create new volunteer (with optional login account creation) |
| `updateVolunteer()` | Update volunteer fields |
| `deleteVolunteer()` | Delete volunteer |
| `sendVolunteerMagicLink()` | Send magic link email for volunteer portal access |
| `fetchHourEntriesByVolunteer()` | Get hour entries for a volunteer |
| `createHourEntry()` | Add hour entry |
| `updateHourEntry()` | Update hour entry |
| `deleteHourEntry()` | Delete hour entry |
| `fetchVolunteerSignupsByVolunteer()` | All sign-ups for a volunteer (optional upcoming-only filter when second arg is `false`) |
React Hooks (src/hooks/useSupabaseData.ts)
useCreateVolunteer()- Create volunteer mutationuseUpdateVolunteer()- Update volunteer mutationuseDeleteVolunteer()- Delete volunteer mutationuseCreateHourEntry()- Create hour entry mutationuseUpdateHourEntry()- Update hour entry mutationuseDeleteHourEntry()- Delete hour entry mutation
Related Documentation
- 01-PEOPLE-CRM.md - Staff management
- 08-VOLUNTEER-SCHEDULER.md - Volunteer events and sign-ups (feeds profile History tab)
- 04-VOLUNTEER-PORTAL.md - Volunteer self-service portal
- ../reports/05-VOLUNTEER-HOURS-REPORT.md - Hour reports
- 01-DATA-SCHEMA.md - Historical volunteer data model
Synced from IFMmvp-Frontend documentation: pages/people/03-VOLUNTEERS-CRM.md
Ready to Get Started?
See how Alignmint can simplify your nonprofit's operations. Schedule a free demo with our team and we'll walk you through everything.
Questions? Email us at steven@getalignmint.org