Skip to main content

EventCheckIn Component

EventCheckIn Component

Component: src/features/events/components/EventCheckIn.tsx Route: /app/fundraising/events/:id/check-in Access: Authenticated users with Fundraising Hub access Last Updated: February 14, 2026 Status: ✅ Production Ready

---

Recent Updates (February 2026)

Styling Guide Alignment (Feb 14, 2026)

  • Removed: Stat card icons — tiles now use standard text-center layout with no icons per STYLING-GUIDE.md
  • Changed: text-3xl font-boldtext-2xl font-semibold on all stat numbers
  • Added: 4th stat tile "Check-In Rate" (was separate progress bar card) — grid now grid-cols-2 md:grid-cols-4
  • Removed: Separate full-width progress bar card
  • Replaced: Back button with <Breadcrumb> component (Marketing > Events & Ticketing > Event Name > Check-In)
  • Centered: Title/subtitle block
  • Replaced: Local formatTime (raw toLocaleTimeString) with shared useDateFormat hook
  • Removed: Unused imports (Separator, useTranslation, ArrowLeft, Collapsible components)
  • Kept: Green financial color on Checked In number (text-green-600 dark:text-green-400)

Recent Updates (January 2026)

Bug Fix: Supabase Client Auth (Jan 13, 2026)

  • Fixed: Component was using raw REST API calls with anon key instead of authenticated Supabase client
  • Solution: Replaced all fetch() calls with supabase.from() queries for proper JWT authentication and RLS enforcement
  • Affected functions: fetchRegistrationsQuery(), handleCheckIn(), handleUndoCheckIn()

---

Overview

The EventCheckIn component provides a real-time check-in interface for event day operations. It supports individual and guest check-ins with live updates via Supabase Realtime.

Features

  • Real-time updates - Supabase Realtime subscription
  • Stats cards - Total, checked in, remaining with progress bar
  • Search - Filter by name or email
  • Status filter - All, checked in, not checked in
  • Guest support - Expandable rows showing guests
  • Undo capability - Reverse accidental check-ins

UI Layout

┌─────────────────────────────────────────────────────────┐
│ ← Back to Event                                         │
├─────────────────────────────────────────────────────────┤
│ Event Name - Check-In                                   │
├─────────────────────────────────────────────────────────┤
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐   │
│ │    Total      │ │  Checked In   │ │   Remaining   │   │
│ │      45       │ │      35       │ │      10       │   │
│ └───────────────┘ └───────────────┘ └───────────────┘   │
│                                                         │
│ ████████████████████████░░░░░░░░░░  78%                 │
├─────────────────────────────────────────────────────────┤
│ [Search by name or email...]  [Status: All ▼]          │
├─────────────────────────────────────────────────────────┤
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ▶ John Doe                    ✓ Checked in 2:30 PM  │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ ▶ Jane Smith                  [ Check In ]          │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ ▼ Bob Wilson (2 guests)       ✓ Checked in 2:45 PM  │ │
│ │   └ Guest 1: Alice Wilson     ✓ Checked in 2:45 PM  │ │
│ │   └ Guest 2: Tom Wilson       [ Check In ]          │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘

State Management

const [registrations, setRegistrations] = useState<Registration[]>([]);
const [searchQuery, setSearchQuery] = useState('');
const [statusFilter, setStatusFilter] = useState<'all' | 'checked_in' | 'not_checked_in'>('all');
const [expandedRows, setExpandedRows] = useState<Set<string>>(new Set());

Real-time Subscription

useEffect(() => {
  const channel = supabase
    .channel('event-checkin')
    .on(
      'postgres_changes',
      {
        event: '*',
        schema: 'public',
        table: 'event_registrations',
        filter: `event_id=eq.${eventId}`
      },
      (payload) => {
        // Update local state with new data
        handleRealtimeUpdate(payload);
      }
    )
    .subscribe();

  return () => {
    supabase.removeChannel(channel);
  };
}, [eventId]);

Check-In Logic

const handleCheckIn = async (registrationId: string, isGuest: boolean = false) => {
  const table = isGuest ? 'registration_guests' : 'event_registrations';
  
  await supabase
    .from(table)
    .update({
      checked_in_at: new Date().toISOString(),
      checked_in_by: currentUserId
    })
    .eq('id', registrationId);
};

const handleUndoCheckIn = async (registrationId: string, isGuest: boolean = false) => {
  const table = isGuest ? 'registration_guests' : 'event_registrations';
  
  await supabase
    .from(table)
    .update({
      checked_in_at: null,
      checked_in_by: null
    })
    .eq('id', registrationId);
};

Stats Calculation

const stats = useMemo(() => {
  const total = registrations.length + registrations.reduce(
    (sum, r) => sum + (r.guests?.length || 0), 0
  );
  
  const checkedIn = registrations.filter(r => r.checked_in_at).length +
    registrations.reduce(
      (sum, r) => sum + (r.guests?.filter(g => g.checked_in_at).length || 0), 0
    );
  
  return {
    total,
    checkedIn,
    remaining: total - checkedIn,
    percentage: total > 0 ? Math.round((checkedIn / total) * 100) : 0
  };
}, [registrations]);

Filtering

const filteredRegistrations = registrations.filter(reg => {
  // Search filter
  const searchLower = searchQuery.toLowerCase();
  const matchesSearch = !searchQuery || 
    reg.registrant_first_name.toLowerCase().includes(searchLower) ||
    reg.registrant_last_name.toLowerCase().includes(searchLower) ||
    reg.registrant_email.toLowerCase().includes(searchLower);
  
  // Status filter
  const matchesStatus = statusFilter === 'all' ||
    (statusFilter === 'checked_in' && reg.checked_in_at) ||
    (statusFilter === 'not_checked_in' && !reg.checked_in_at);
  
  return matchesSearch && matchesStatus;
});

Data Fetching

Uses React Query for data fetching with automatic cache invalidation:

const { data: registrations = [], isLoading: loading, refetch } = useQuery({
  queryKey: ['eventRegistrations', event.id],
  queryFn: fetchRegistrationsQuery,
  staleTime: 10000,
});

Bug Fixes

December 30, 2025

  • Fixed initialLoad is not defined error - changed to use loading from React Query

Related Components

  • EventDashboard - Parent metrics view
  • EventsManager - Event list
  • RegistrationRow - Individual registration display
  • GuestRow - Guest check-in row

Related Documentation

  • Main Events Doc: ../06-EVENTS-TICKETING.md
  • Event Dashboard: ./03-EVENT-DASHBOARD.md

Synced from IFMmvp-Frontend documentation: pages/marketing/events/04-EVENT-CHECK-IN.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