Volunteer Weekly Check In Portal

import React, { useState, useEffect, useMemo, useCallback } from 'react'; import BoardSDK from '@api/BoardSDK.js'; import { Button } from '@components/ui/button'; import { Input } from '@components/ui/input'; import { Label } from '@components/ui/label'; import { Textarea } from '@components/ui/textarea'; import { Calendar } from '@components/ui/calendar'; import { Popover, PopoverContent, PopoverTrigger } from '@components/ui/popover'; import { KPICard } from '@components/KPICard'; import { Skeleton } from '@components/ui/skeleton'; import { CalendarIcon, Clock, Users, ClipboardCheck, ChevronDown, ChevronUp } from 'lucide-react'; import { cn } from '@lib/utils'; const board = new BoardSDK(); export default function App() { const [items, setItems] = useState([]); const [loading, setLoading] = useState(true); const [submitting, setSubmitting] = useState(false); const [cursor, setCursor] = useState(null); const [hasMore, setHasMore] = useState(false); // Form state const [name, setName] = useState(''); const [email, setEmail] = useState(''); const [date, setDate] = useState(new Date()); const [hours, setHours] = useState(''); const [tasks, setTasks] = useState(''); const [feedback, setFeedback] = useState(''); const [isCalendarOpen, setIsCalendarOpen] = useState(false); // Weekly stats const [weeklyStats, setWeeklyStats] = useState({ totalHours: 0, volunteerCount: 0, checkInCount: 0 }); const [statsLoading, setStatsLoading] = useState(true); // Load saved volunteer info from localStorage useEffect(() => { const savedName = localStorage.getItem('volunteer_name'); const savedEmail = localStorage.getItem('volunteer_email'); if (savedName) setName(savedName); if (savedEmail) setEmail(savedEmail); }, []); // Fetch initial items useEffect(() => { loadItems(); loadWeeklyStats(); }, []); async function loadItems(loadMore = false) { try { setLoading(true); const query = board.items() .withColumns(['dateOfVolunteering', 'emailAddress', 'hoursVolunteered', 'feedback', 'tasksCompleted']) .orderBy({ column: 'dateOfVolunteering', direction: 'desc' }); if (loadMore && cursor) { query.withPagination({ cursor }); } else { query.withPagination({ limit: 100 }); } const result = await query.execute(); if (loadMore) { setItems((prev) => [...prev, ...result.items]); } else { setItems(result.items); } setCursor(result.cursor); setHasMore(!!result.cursor); } catch (error) { console.error('Failed to load check-ins:', error); } finally { setLoading(false); } } async function loadWeeklyStats() { try { setStatsLoading(true); const startOfWeek = getStartOfWeek(new Date()); const endOfWeek = new Date(startOfWeek); endOfWeek.setDate(endOfWeek.getDate() + 6); const startStr = startOfWeek.toISOString().split('T')[0]; const endStr = endOfWeek.toISOString().split('T')[0]; // Get sum of hours and count for this week const stats = await board.aggregate() .sum('hoursVolunteered', 'totalHours') .countItems('checkInCount') .where({ dateOfVolunteering: { between: { from: startStr, to: endStr } } }) .execute(); // Count unique volunteers const weekItems = await board.items() .withColumns(['emailAddress']) .where({ dateOfVolunteering: { between: { from: startStr, to: endStr } } }) .withPagination({ limit: 500 }) .execute(); const uniqueEmails = new Set( weekItems.items .map(item => item.emailAddress?.email) .filter(Boolean) ); setWeeklyStats({ totalHours: stats[0]?.totalHours || 0, checkInCount: stats[0]?.checkInCount || 0, volunteerCount: uniqueEmails.size }); } catch (error) { console.error('Failed to load weekly stats:', error); } finally { setStatsLoading(false); } } const handleSubmit = useCallback(async (e) => { e.preventDefault(); if (!name.trim() || !email.trim() || !hours) return; // Save volunteer info to localStorage localStorage.setItem('volunteer_name', name.trim()); localStorage.setItem('volunteer_email', email.trim()); const newEntry = { id: `temp-${Date.now()}`, name: name.trim(), dateOfVolunteering: date, emailAddress: { email: email.trim(), label: 'Work' }, hoursVolunteered: parseFloat(hours), feedback: feedback.trim() || null, tasksCompleted: tasks.trim() || null, createdAt: new Date().toISOString() }; // Optimistic update (SNAP) const prevItems = items; setItems([newEntry, ...items]); // Update weekly stats optimistically const prevStats = weeklyStats; if (isThisWeek(date)) { setWeeklyStats({ totalHours: weeklyStats.totalHours + parseFloat(hours), checkInCount: weeklyStats.checkInCount + 1, volunteerCount: weeklyStats.volunteerCount // Will update on next load }); } setSubmitting(true); try { const created = await board.item().create({ name: name.trim(), dateOfVolunteering: date, emailAddress: { email: email.trim(), label: 'Work' }, hoursVolunteered: parseFloat(hours), feedback: feedback.trim() || null, tasksCompleted: tasks.trim() || null }) .returnColumns(['dateOfVolunteering', 'emailAddress', 'hoursVolunteered', 'feedback', 'tasksCompleted']) .inGroup('topics') .execute(); // Replace temp with real item setItems(prevItems => prevItems.map(item => item.id === newEntry.id ? created : item)); // Clear form (but keep name/email) setDate(new Date()); setHours(''); setTasks(''); setFeedback(''); // Reload stats to get accurate volunteer count loadWeeklyStats(); } catch (error) { console.error('Failed to create check-in:', error); // Rollback setItems(prevItems); setWeeklyStats(prevStats); } finally { setSubmitting(false); } }, [name, email, date, hours, tasks, feedback, items, weeklyStats]); // Group items by week const groupedByWeek = useMemo(() => { const groups = {}; for (const item of items) { if (!item.dateOfVolunteering) continue; const itemDate = new Date(item.dateOfVolunteering); const weekStart = getStartOfWeek(itemDate); const weekKey = weekStart.toISOString().split('T')[0]; if (!groups[weekKey]) { groups[weekKey] = { weekStart, entries: [], totalHours: 0 }; } groups[weekKey].entries.push(item); groups[weekKey].totalHours += item.hoursVolunteered || 0; } return Object.entries(groups) .sort(([a], [b]) => b.localeCompare(a)) .map(([key, data]) => ({ weekKey: key, weekLabel: getWeekLabel(data.weekStart), ...data })); }, [items]); return (
{/* Header */}

Volunteer Check-In

Log your volunteer hours whenever you work

{/* Check-in Form */}

Quick Check-In

setName(e.target.value)} required className="h-11" />
setEmail(e.target.value)} required className="h-11" />
{ setDate(newDate || new Date()); setIsCalendarOpen(false); }} initialFocus />
setHours(e.target.value)} required className="h-11" />