diff --git a/app/api/hackathons/[id]/registrations/export/route.ts b/app/api/hackathons/[id]/registrations/export/route.ts new file mode 100644 index 00000000..5292b37b --- /dev/null +++ b/app/api/hackathons/[id]/registrations/export/route.ts @@ -0,0 +1,125 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createClient } from '@/lib/supabase/server'; + +export const runtime = 'nodejs'; + +// GET: Export registrations as CSV +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const supabase = await createClient(); + + // Get the current user + const { data: { user }, error: authError } = await supabase.auth.getUser(); + + if (authError || !user) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 } + ); + } + + // Get the hackathon by slug + const { data: hackathon, error: hackathonError } = await supabase + .from('hackathons') + .select('id, title, company_id, slug') + .eq('slug', id) + .single(); + + if (hackathonError || !hackathon) { + return NextResponse.json( + { error: 'Hackathon not found' }, + { status: 404 } + ); + } + + // Check if user has access to this hackathon's company + const { data: membership } = await supabase + .from('company_members') + .select('role') + .eq('company_id', hackathon.company_id) + .eq('user_id', user.id) + .single(); + + if (!membership) { + return NextResponse.json( + { error: 'Unauthorized access' }, + { status: 403 } + ); + } + + // Get all registrations for this hackathon + const { data: registrations, error: regError } = await supabase + .from('master_registrations') + .select('*') + .eq('activity_type', 'hackathon') + .eq('activity_id', hackathon.id.toString()) + .order('created_at', { ascending: false }); + + if (regError) { + console.error('Error fetching registrations:', regError); + return NextResponse.json( + { error: 'Failed to fetch registrations' }, + { status: 500 } + ); + } + + // Convert to CSV + const headers = [ + 'ID', + 'Full Name', + 'Email', + 'Phone', + 'Institution', + 'Department', + 'Year of Study', + 'Experience Level', + 'Status', + 'Payment Status', + 'Payment Amount', + 'Registration Date', + 'Created At' + ]; + + const csvRows = [headers.join(',')]; + + registrations?.forEach(reg => { + const row = [ + reg.id, + `"${reg.full_name || ''}"`, + `"${reg.email || ''}"`, + `"${reg.phone || ''}"`, + `"${reg.institution || ''}"`, + `"${reg.department || ''}"`, + `"${reg.year_of_study || ''}"`, + `"${reg.experience_level || ''}"`, + reg.status, + reg.payment_status, + reg.payment_amount || '', + reg.registration_date, + reg.created_at + ]; + csvRows.push(row.join(',')); + }); + + const csv = csvRows.join('\n'); + const filename = `${hackathon.slug}-registrations-${new Date().toISOString().split('T')[0]}.csv`; + + return new NextResponse(csv, { + headers: { + 'Content-Type': 'text/csv', + 'Content-Disposition': `attachment; filename="${filename}"` + } + }); + + } catch (error) { + console.error('Error exporting registrations:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/api/hackathons/[id]/registrations/route.ts b/app/api/hackathons/[id]/registrations/route.ts new file mode 100644 index 00000000..92aef39e --- /dev/null +++ b/app/api/hackathons/[id]/registrations/route.ts @@ -0,0 +1,274 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { createClient as createServerClient } from '@/lib/supabase/server'; +import { createClient } from '@supabase/supabase-js'; + +export const runtime = 'nodejs'; + +// Create Supabase client with service role key to bypass RLS for master_registrations +const getServiceRoleClient = () => { + return createClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { + auth: { + autoRefreshToken: false, + persistSession: false + } + } + ); +}; + +// GET: Fetch all registrations for a hackathon +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + + // Use server client for authentication + const serverClient = await createServerClient(); + const { data: { user }, error: authError } = await serverClient.auth.getUser(); + + if (authError || !user) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 } + ); + } + + // Use service role client for querying (bypasses RLS) + const supabase = getServiceRoleClient(); + + // Get the hackathon by slug + const { data: hackathon, error: hackathonError } = await supabase + .from('hackathons') + .select('id, title, company_id, slug') + .eq('slug', id) + .single(); + + if (hackathonError || !hackathon) { + return NextResponse.json( + { error: 'Hackathon not found' }, + { status: 404 } + ); + } + + // Check if user has access to this hackathon's company + const { data: membership } = await supabase + .from('company_members') + .select('role') + .eq('company_id', hackathon.company_id) + .eq('user_id', user.id) + .single(); + + if (!membership) { + return NextResponse.json( + { error: 'Unauthorized access' }, + { status: 403 } + ); + } + + // Get search and filter parameters + const searchParams = request.nextUrl.searchParams; + const search = searchParams.get('search') || ''; + const status = searchParams.get('status') || ''; + const paymentStatus = searchParams.get('payment_status') || ''; + const limit = parseInt(searchParams.get('limit') || '100'); + const offset = parseInt(searchParams.get('offset') || '0'); + + // Build query for registrations + let query = supabase + .from('master_registrations') + .select('*', { count: 'exact' }) + .eq('activity_type', 'hackathon') + .eq('activity_id', hackathon.id.toString()) + .order('created_at', { ascending: false }); + + // Apply filters + if (status) { + query = query.eq('status', status); + } + + if (paymentStatus) { + query = query.eq('payment_status', paymentStatus); + } + + // Apply search (search in full_name, email, phone) + if (search) { + query = query.or(`full_name.ilike.%${search}%,email.ilike.%${search}%,phone.ilike.%${search}%`); + } + + // Apply pagination + query = query.range(offset, offset + limit - 1); + + const { data: registrations, error: regError, count } = await query; + + // Debug logging + console.log('=== HACKATHON REGISTRATIONS DEBUG ==='); + console.log('Hackathon ID:', hackathon.id); + console.log('Hackathon ID type:', typeof hackathon.id); + console.log('Activity ID being queried:', hackathon.id.toString()); + console.log('Registrations found:', registrations?.length); + console.log('Total count:', count); + console.log('Error:', regError); + if (registrations && registrations.length > 0) { + console.log('Sample registration:', registrations[0]); + } + console.log('====================================='); + + if (regError) { + console.error('Error fetching registrations:', regError); + return NextResponse.json( + { error: 'Failed to fetch registrations' }, + { status: 500 } + ); + } + + // Get user profiles for registrations that have user_id + const userIds = registrations + ?.filter(r => r.user_id) + .map(r => r.user_id) || []; + + let profiles: { id: string; first_name: string | null; last_name: string | null; email: string | null; avatar_url: string | null }[] = []; + if (userIds.length > 0) { + const { data: profilesData } = await supabase + .from('profiles') + .select('id, first_name, last_name, email, avatar_url') + .in('id', userIds); + + profiles = profilesData || []; + } + + // Merge profile data with registrations + const enrichedRegistrations = registrations?.map(reg => { + const profile = profiles.find(p => p.id === reg.user_id); + const profileName = profile + ? `${profile.first_name || ''} ${profile.last_name || ''}`.trim() + : null; + + return { + ...reg, + profile_name: profileName || reg.full_name, + profile_avatar: profile?.avatar_url, + email: profile?.email || reg.email, // Use profile email if available + }; + }); + + return NextResponse.json({ + registrations: enrichedRegistrations || [], + total: count || 0, + hackathon: { + id: hackathon.id, + title: hackathon.title, + slug: hackathon.slug + } + }); + + } catch (error) { + console.error('Error in hackathon registrations API:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +// PATCH: Update registration status +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + try { + const { id } = await params; + const body = await request.json(); + const { registration_id, status, payment_status } = body; + + if (!registration_id) { + return NextResponse.json( + { error: 'Registration ID is required' }, + { status: 400 } + ); + } + + // Use server client for authentication + const serverClient = await createServerClient(); + const { data: { user }, error: authError } = await serverClient.auth.getUser(); + + if (authError || !user) { + return NextResponse.json( + { error: 'Authentication required' }, + { status: 401 } + ); + } + + // Use service role client for querying + const supabase = getServiceRoleClient(); + + // Get the hackathon by slug + const { data: hackathon, error: hackathonError } = await supabase + .from('hackathons') + .select('id, company_id') + .eq('slug', id) + .single(); + + if (hackathonError || !hackathon) { + return NextResponse.json( + { error: 'Hackathon not found' }, + { status: 404 } + ); + } + + // Check if user has access to this hackathon's company + const { data: membership } = await supabase + .from('company_members') + .select('role') + .eq('company_id', hackathon.company_id) + .eq('user_id', user.id) + .single(); + + if (!membership || !['owner', 'admin', 'editor'].includes(membership.role)) { + return NextResponse.json( + { error: 'Unauthorized access' }, + { status: 403 } + ); + } + + // Update registration + const updates: Record = { + updated_at: new Date().toISOString() + }; + + if (status) updates.status = status; + if (payment_status) updates.payment_status = payment_status; + + const { data: updatedRegistration, error: updateError } = await supabase + .from('master_registrations') + .update(updates) + .eq('id', registration_id) + .eq('activity_type', 'hackathon') + .eq('activity_id', hackathon.id.toString()) + .select() + .single(); + + if (updateError) { + console.error('Error updating registration:', updateError); + return NextResponse.json( + { error: 'Failed to update registration' }, + { status: 500 } + ); + } + + return NextResponse.json({ + success: true, + registration: updatedRegistration + }); + + } catch (error) { + console.error('Error updating registration:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/app/dashboard/company/[slug]/hackathons/[hackathonSlug]/registrations/page.tsx b/app/dashboard/company/[slug]/hackathons/[hackathonSlug]/registrations/page.tsx new file mode 100644 index 00000000..5a333a0a --- /dev/null +++ b/app/dashboard/company/[slug]/hackathons/[hackathonSlug]/registrations/page.tsx @@ -0,0 +1,400 @@ +'use client' + +import { useState, useEffect, useCallback } from 'react' +import { useParams } from 'next/navigation' +import { useCompanyContext } from '@/contexts/CompanyContext' +import { Button } from '@/components/ui/button' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Badge } from '@/components/ui/badge' +import { Input } from '@/components/ui/input' +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' +import { + ArrowLeft, + Search, + Download, + Users, + CheckCircle, + Clock, + XCircle, + DollarSign, + Calendar, +} from 'lucide-react' +import { toast } from 'sonner' +import Link from 'next/link' +import { MasterRegistration } from '@/lib/services/master-registrations' + +interface EnrichedRegistration extends MasterRegistration { + profile_name?: string + profile_avatar?: string +} + +export default function HackathonRegistrationsPage() { + const params = useParams() + const { currentCompany } = useCompanyContext() + const hackathonSlug = params.hackathonSlug as string + + const [registrations, setRegistrations] = useState([]) + const [loading, setLoading] = useState(true) + const [searchTerm, setSearchTerm] = useState('') + const [statusFilter, setStatusFilter] = useState('all') + const [paymentFilter, setPaymentFilter] = useState('all') + const [total, setTotal] = useState(0) + const [hackathonTitle, setHackathonTitle] = useState('') + const [exporting, setExporting] = useState(false) + + const fetchRegistrations = useCallback(async () => { + if (!currentCompany || !hackathonSlug) return + + try { + setLoading(true) + const params = new URLSearchParams() + + if (searchTerm) params.append('search', searchTerm) + if (statusFilter && statusFilter !== 'all') params.append('status', statusFilter) + if (paymentFilter && paymentFilter !== 'all') params.append('payment_status', paymentFilter) + params.append('limit', '100') + + const response = await fetch(`/api/hackathons/${hackathonSlug}/registrations?${params.toString()}`) + + if (!response.ok) { + throw new Error('Failed to fetch registrations') + } + + const data = await response.json() + setRegistrations(data.registrations || []) + setTotal(data.total || 0) + setHackathonTitle(data.hackathon?.title || '') + } catch (error) { + console.error('Error fetching registrations:', error) + toast.error('Failed to load registrations') + } finally { + setLoading(false) + } + }, [currentCompany, hackathonSlug, searchTerm, statusFilter, paymentFilter]) + + useEffect(() => { + fetchRegistrations() + }, [fetchRegistrations]) + + const handleExportCSV = async () => { + try { + setExporting(true) + const response = await fetch(`/api/hackathons/${hackathonSlug}/registrations/export`) + + if (!response.ok) { + throw new Error('Failed to export registrations') + } + + const blob = await response.blob() + const url = window.URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + a.download = `${hackathonSlug}-registrations-${new Date().toISOString().split('T')[0]}.csv` + document.body.appendChild(a) + a.click() + window.URL.revokeObjectURL(url) + document.body.removeChild(a) + + toast.success('Registrations exported successfully') + } catch (error) { + console.error('Error exporting registrations:', error) + toast.error('Failed to export registrations') + } finally { + setExporting(false) + } + } + + const handleUpdateStatus = async (registrationId: number, status: string) => { + try { + const response = await fetch(`/api/hackathons/${hackathonSlug}/registrations`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ registration_id: registrationId, status }) + }) + + if (!response.ok) { + throw new Error('Failed to update status') + } + + toast.success('Status updated successfully') + fetchRegistrations() + } catch (error) { + console.error('Error updating status:', error) + toast.error('Failed to update status') + } + } + + const getStatusBadge = (status: string) => { + switch (status) { + case 'registered': + return ( + + + Registered + + ) + case 'pending': + return ( + + + Pending + + ) + case 'cancelled': + return ( + + + Cancelled + + ) + case 'attended': + return ( + + + Attended + + ) + case 'no_show': + return ( + + No Show + + ) + default: + return {status} + } + } + + const getPaymentBadge = (paymentStatus: string) => { + switch (paymentStatus) { + case 'paid': + return ( + + + Paid + + ) + case 'pending': + return ( + + + Pending + + ) + case 'failed': + return ( + + Failed + + ) + case 'not_applicable': + return ( + + N/A + + ) + default: + return {paymentStatus} + } + } + + if (!currentCompany) { + return ( +
+
+
+ ) + } + + return ( +
+ {/* Header */} +
+
+ + + +
+

Hackathon Registrations

+

+ {hackathonTitle} +

+
+
+ +
+ + {/* Stats Card */} + + + Total Registrations + + + +
{total}
+
+
+ + {/* Filters */} +
+
+ + setSearchTerm(e.target.value)} + className="pl-10" + /> +
+ + +
+ + {/* Registrations Table */} + + + Registrations ({registrations.length}) + + Manage and view all hackathon registrations + + + + {loading ? ( +
+
+
+ ) : registrations.length === 0 ? ( +
+ +

+ No registrations found +

+

+ {searchTerm || statusFilter !== 'all' || paymentFilter !== 'all' + ? 'Try adjusting your filters' + : 'No one has registered for this hackathon yet'} +

+
+ ) : ( +
+ + + + Participant + Email + Phone + Status + Payment + Registered + Actions + + + + {registrations.map((registration) => ( + + +
+ + {registration.profile_name || registration.full_name || registration.email || `User ${registration.user_id.substring(0, 8)}`} + + {registration.experience_level && ( + + {registration.experience_level} + + )} +
+
+ + {registration.email ? ( + {registration.email} + ) : ( + - + )} + + + {registration.phone ? ( + {registration.phone} + ) : ( + - + )} + + {getStatusBadge(registration.status)} + +
+ {getPaymentBadge(registration.payment_status)} + {registration.payment_amount && ( + + ₹{registration.payment_amount / 100} + + )} +
+
+ +
+ + {new Date(registration.created_at).toLocaleDateString()} +
+
+ + + +
+ ))} +
+
+
+ )} +
+
+
+ ) +}