Skip to main content

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) from src/lib/db/volunteer-events.ts when the History tab is active.
  • Hours table: Layout no longer uses table-fixed with 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 through sendVolunteerMagicLink -> edge function send-volunteer-magic-link -> Supabase Auth signInWithOtp.
  • 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_id when an auth user already exists for the email
  • creates a volunteer organization_users row 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 from src/lib/searchUtils.ts plus a volunteer-specific matcher in src/lib/db/volunteers.ts. globalSearch() now returns volunteer results, and VolunteersCRM.tsx can 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
  • Email
  • 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_checks table

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_checks table)
  • Onboarding checklist progress tracked per-volunteer
  • Waiver signatures linked to volunteer records
  • total_hours synced by trg_sync_volunteer_total_hours DB trigger

State Management

Local State

  • volunteers - List of volunteers
  • selectedVolunteer - Currently viewing/editing
  • view - 'list' or 'profile'
  • activeTab - Profile tab: 'hours' | 'history' | 'background-check' | 'onboarding' | 'notes' | 'waivers' | 'emergency' (default 'hours')
  • addVolunteerOpen - Add dialog state
  • searchQuery - Search input
  • filters - Type and status filters
  • sortBy - Sort option

Global State (Zustand Store)

  • selectedEntity - Current organization (from useAppStore)
  • pendingSearchResult - Deep-link target from Global Search so volunteer profiles can open directly

Dependencies

Internal Dependencies

  • useAppStore - Zustand global state
  • fetchVolunteersPaginated - Paginated volunteer data from Supabase
  • useCreateVolunteer, useUpdateVolunteer, useDeleteVolunteer - Mutation hooks
  • useCreateHourEntry, useUpdateHourEntry, useDeleteHourEntry - Hour entry hooks
  • fetchWaivers, fetchWaiverSignatures - Waiver data
  • fetchVolunteerSignupsByVolunteer - Event sign-ups for the History tab (src/lib/db/volunteer-events.ts)
  • UI components (Card, Button, Table, Dialog, etc.)

External Libraries

  • lucide-react - Icons
  • sonner - 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', records approved_by (current user ID) and approved_at timestamp
  • Reject — Sets status: 'rejected'
  • Edit / Delete — Existing functionality

RLS Policy

  • Only users with position of director or bookkeeper in organization_users can UPDATE or DELETE hour_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 to volunteer_events (used for profile History tab)
  • volunteer_events, volunteer_event_slots - Scheduler events and slots
  • waiver_signatures - Signed waivers linked to volunteers
  • waivers - 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 mutation
  • useUpdateVolunteer() - Update volunteer mutation
  • useDeleteVolunteer() - Delete volunteer mutation
  • useCreateHourEntry() - Create hour entry mutation
  • useUpdateHourEntry() - Update hour entry mutation
  • useDeleteHourEntry() - Delete hour entry mutation

Related Documentation


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

Ready to get started?Start Plus Trial