826 lines
33 KiB
TypeScript
826 lines
33 KiB
TypeScript
import React, { useState, useEffect, useCallback } from 'react';
|
|
import WelcomeScreen from './components/WelcomeScreen';
|
|
import LoginForm from './components/LoginForm';
|
|
import Dashboard from './components/Dashboard';
|
|
import AdminLandingPage from './components/AdminLandingPage';
|
|
import FacultyDashboard from './components/FacultyDashboard';
|
|
import TicketVerificationView from './components/TicketVerificationView';
|
|
import { AppState, UserRole, Event, EventSchedule, Announcement, Ticket } from './types';
|
|
import { supabase } from './supabase';
|
|
|
|
const App: React.FC = () => {
|
|
const [appState, setAppState] = useState<AppState>('WELCOME');
|
|
const [userRole, setUserRole] = useState<UserRole | null>(null);
|
|
const [events, setEvents] = useState<Event[]>([]);
|
|
const [announcements, setAnnouncements] = useState<Announcement[]>([]);
|
|
const [specialEvents, setSpecialEvents] = useState<any[]>([]);
|
|
const [domains, setDomains] = useState<any[]>([]);
|
|
const [userRegistrations, setUserRegistrations] = useState<any[]>([]);
|
|
const [allRegistrations, setAllRegistrations] = useState<any[]>([]);
|
|
const [bookedEventIds, setBookedEventIds] = useState<string[]>([]);
|
|
const [currentUserName, setCurrentUserName] = useState<string>('User');
|
|
const [currentUserDept, setCurrentUserDept] = useState<string>('');
|
|
const [currentUserEmail, setCurrentUserEmail] = useState<string>('');
|
|
const [currentUserPhone, setCurrentUserPhone] = useState<string>('');
|
|
const [currentUserPhoto, setCurrentUserPhoto] = useState<string>('');
|
|
const [currentUserId, setCurrentUserId] = useState<string>('');
|
|
const [currentUserFacultyRole, setCurrentUserFacultyRole] = useState<string>('');
|
|
const [ticketToVerify, setTicketToVerify] = useState<{ ticket: Ticket, event: Event } | null>(null);
|
|
const [isAuthenticating, setIsAuthenticating] = useState(false);
|
|
const [globalError, setGlobalError] = useState<string | null>(null);
|
|
const [studentDocuments, setStudentDocuments] = useState<Record<string, string>>({});
|
|
const [showStudentHubForAdmin, setShowStudentHubForAdmin] = useState(false);
|
|
const [intendedRole, setIntendedRole] = useState<UserRole | null>(null);
|
|
|
|
const mapDbToEvent = useCallback((dbEvent: any, registrations: any[] = []): Event => {
|
|
const eventRegs = registrations.filter(r => String(r.event_id) === String(dbEvent.id));
|
|
const deptCounts: Record<string, number> = {};
|
|
const deptSectionCounts: Record<string, Record<string, number>> = {};
|
|
|
|
eventRegs.forEach(reg => {
|
|
if (reg.dept) {
|
|
deptCounts[reg.dept] = (deptCounts[reg.dept] || 0) + 1;
|
|
if (reg.section) {
|
|
if (!deptSectionCounts[reg.dept]) {
|
|
deptSectionCounts[reg.dept] = {};
|
|
}
|
|
deptSectionCounts[reg.dept][reg.section] = (deptSectionCounts[reg.dept][reg.section] || 0) + 1;
|
|
}
|
|
}
|
|
});
|
|
|
|
return {
|
|
id: String(dbEvent.id),
|
|
title: dbEvent.title,
|
|
location: dbEvent.location,
|
|
date: dbEvent.date,
|
|
category: dbEvent.category,
|
|
domain: dbEvent.domain,
|
|
pricingType: dbEvent.pricing_type,
|
|
coordinator: dbEvent.coordinator,
|
|
image: dbEvent.image,
|
|
status: dbEvent.status,
|
|
maxParticipants: dbEvent.max_participants,
|
|
registrationDeadline: dbEvent.registration_deadline,
|
|
durationDays: dbEvent.duration_days,
|
|
club: dbEvent.club,
|
|
deptLimits: dbEvent.deptLimits || {},
|
|
deptSectionLimits: dbEvent.deptSectionLimits || {},
|
|
event_summary: dbEvent.event_summary,
|
|
isTeamEvent: dbEvent.is_team_event,
|
|
teamSizeLimit: dbEvent.team_size_limit,
|
|
teamComposition: dbEvent.team_composition,
|
|
currentParticipants: eventRegs.length,
|
|
currentDeptCounts: deptCounts,
|
|
currentDeptSectionCounts: deptSectionCounts,
|
|
created_by: dbEvent.created_by,
|
|
participantType: dbEvent.participant_type as any,
|
|
verificationStatus: dbEvent.verification_status as any,
|
|
refreshment_expense: dbEvent.refreshment_expense ?? 0,
|
|
transportation_expense: dbEvent.transportation_expense ?? 0,
|
|
session_coverage_fee: dbEvent.session_coverage_fee ?? 0,
|
|
total_expense: dbEvent.total_expense ?? 0,
|
|
conducting_dept: dbEvent.conducting_dept ?? null,
|
|
request_by_faculty: dbEvent.request_by_faculty ?? null,
|
|
request_by_HOD: dbEvent.request_by_hod ?? null,
|
|
};
|
|
}, []);
|
|
|
|
const fetchAnnouncements = useCallback(async () => {
|
|
try {
|
|
const { data, error } = await supabase
|
|
.from('announcements')
|
|
.select('*')
|
|
.order('timestamp', { ascending: false });
|
|
|
|
if (data) {
|
|
setAnnouncements(data.map(ann => ({
|
|
id: String(ann.id),
|
|
title: ann.title,
|
|
message: ann.message,
|
|
type: ann.type,
|
|
eventId: ann.event_id ? String(ann.event_id) : undefined,
|
|
timestamp: ann.timestamp || new Date().toISOString(),
|
|
expiresAt: ann.expires_at
|
|
})));
|
|
}
|
|
} catch (err) {
|
|
console.error("Announcement Sync Error:", err);
|
|
}
|
|
}, []);
|
|
|
|
const fetchSpecialEvents = useCallback(async () => {
|
|
try {
|
|
let query = supabase.from('special_events').select('*').eq('is_active', true);
|
|
|
|
// SECURITY: Public users and Students only see Approved special events
|
|
if (!userRole || userRole === 'STUDENT') {
|
|
query = query.eq('verification_status', 'APPROVED');
|
|
}
|
|
|
|
const { data } = await query.order('created_at', { ascending: false });
|
|
if (data) {
|
|
setSpecialEvents(data.map((se: any) => ({
|
|
...se,
|
|
verificationStatus: se.verification_status
|
|
})));
|
|
}
|
|
} catch (err) {
|
|
console.error("Special Events Sync Error:", err);
|
|
}
|
|
}, [userRole]);
|
|
|
|
const fetchDomains = useCallback(async () => {
|
|
try {
|
|
const { data } = await supabase.from('domains').select('*').order('name', { ascending: true });
|
|
if (data) setDomains(data);
|
|
} catch (err) {
|
|
console.error("Domains Sync Error:", err);
|
|
}
|
|
}, []);
|
|
|
|
const fetchEvents = useCallback(async (registrations: any[] = []) => {
|
|
const { data } = await supabase.from('events').select('*').order('date', { ascending: true });
|
|
if (data) {
|
|
// Fetch all schedule + resource_persons + dept limits in bulk
|
|
const { data: scheduleData } = await supabase.from('event_schedule').select('*').order('day_idx', { ascending: true }).order('batch_idx', { ascending: true });
|
|
const { data: rpData } = await supabase.from('resource_persons').select('*');
|
|
const { data: deptLimitsData } = await supabase.from('event_dept_limits').select('*');
|
|
|
|
const mapped = data.map(e => {
|
|
const event = mapDbToEvent(e, registrations);
|
|
|
|
// Attach schedule rows for this event
|
|
const eventSchedule: EventSchedule[] = (scheduleData || []).filter(s => s.event_id === e.id).map(s => {
|
|
const rp = (rpData || []).find(r => r.schedule_id === s.id);
|
|
return {
|
|
...s,
|
|
resource_person: rp ? { id: rp.id, type: rp.type, name: rp.name, dept: rp.dept, college_name: rp.college_name, phone: rp.phone, email: rp.email } : undefined,
|
|
};
|
|
});
|
|
event.schedule = eventSchedule;
|
|
|
|
// Attach department limits for this event
|
|
const eventDeptLimits: Record<string, number> = {};
|
|
const eventDeptSectionLimits: Record<string, Record<string, number>> = {};
|
|
(deptLimitsData || []).filter(dl => dl.event_id === e.id).forEach(dl => {
|
|
eventDeptLimits[dl.department] = dl.max_seats;
|
|
eventDeptSectionLimits[dl.department] = dl.section_limits || {};
|
|
});
|
|
event.deptLimits = eventDeptLimits;
|
|
event.deptSectionLimits = eventDeptSectionLimits;
|
|
|
|
return event;
|
|
});
|
|
// SECURITY: Public users and Students only see Approved events
|
|
if (!userRole || userRole === 'STUDENT') {
|
|
setEvents(mapped.filter(e => e.verificationStatus === 'APPROVED'));
|
|
} else {
|
|
setEvents(mapped);
|
|
}
|
|
}
|
|
}, [mapDbToEvent, userRole]);
|
|
|
|
const fetchAllRegistrations = useCallback(async () => {
|
|
const { data } = await supabase.from('registrations').select('*').order('registered_at', { ascending: false });
|
|
if (data) {
|
|
setAllRegistrations(data);
|
|
fetchEvents(data);
|
|
}
|
|
}, [fetchEvents]);
|
|
|
|
const fetchMyRegs = useCallback(async (userId: string) => {
|
|
const { data } = await supabase.from('registrations').select('*').eq('user_id', userId);
|
|
if (data) {
|
|
setUserRegistrations(data);
|
|
setBookedEventIds(data.map((r: any) => String(r.event_id)));
|
|
}
|
|
}, []);
|
|
|
|
const handleAuthChange = useCallback(async (session: any) => {
|
|
if (!session?.user) {
|
|
setAppState(prev => {
|
|
if (prev !== 'WELCOME' && prev !== 'LOGIN') return 'WELCOME';
|
|
return prev;
|
|
});
|
|
setIsAuthenticating(false);
|
|
return;
|
|
}
|
|
|
|
setIsAuthenticating(true);
|
|
const user = session.user;
|
|
|
|
const HIGH_AUTH_ADMINS = [
|
|
'adrit1highauth@gmail.com',
|
|
'adrit2highauth@gmail.com',
|
|
'adrit3highauth@gmail.com',
|
|
'adrit4highauth@gmail.com',
|
|
'adrit5highauth@gmail.com'
|
|
];
|
|
|
|
try {
|
|
let profile = null;
|
|
let finalRole: UserRole | null = null;
|
|
|
|
// Check for fixed high-auth admins first
|
|
if (user.email && HIGH_AUTH_ADMINS.includes(user.email)) {
|
|
const { data: admin } = await supabase.from('Adminusers').select('*').eq('id', user.id).single();
|
|
if (admin) {
|
|
profile = admin;
|
|
} else {
|
|
// If they exist in auth but not in Adminusers yet, we create and insert a profile
|
|
const newProfile = {
|
|
id: user.id,
|
|
name: 'System Admin',
|
|
email: user.email,
|
|
dept: 'Administration',
|
|
updated_at: new Date().toISOString()
|
|
};
|
|
const { data: inserted, error: insertError } = await supabase.from('Adminusers').insert(newProfile).select().single();
|
|
if (!insertError) {
|
|
profile = inserted;
|
|
} else {
|
|
profile = newProfile; // Fallback to local object if insert fails
|
|
}
|
|
}
|
|
finalRole = 'ADMIN'; // Principal/Director is ADMIN
|
|
} else {
|
|
// Check Adminusers (Admin Hub / System Admins)
|
|
const { data: adminProfile } = await supabase.from('Adminusers').select('*').eq('id', user.id).single();
|
|
if (adminProfile) {
|
|
profile = adminProfile;
|
|
finalRole = 'ADMIN';
|
|
} else {
|
|
// Check Facultyusers (Event Coordinator Hub)
|
|
const { data: facultyProfile } = await supabase.from('Facultyusers').select('*').eq('id', user.id).single();
|
|
if (facultyProfile) {
|
|
profile = facultyProfile;
|
|
finalRole = 'COORDINATOR';
|
|
} else {
|
|
const { data: student } = await supabase.from('Studentusers').select('*').eq('id', user.id).single();
|
|
if (student) {
|
|
profile = student;
|
|
finalRole = 'STUDENT';
|
|
} else {
|
|
const { data: externalStudent } = await supabase.from('externalusers').select('*').eq('id', user.id).single();
|
|
if (externalStudent) {
|
|
profile = externalStudent;
|
|
finalRole = 'STUDENT';
|
|
} else {
|
|
// FALLBACK: Auto-sync profile from metadata
|
|
const meta = user.user_metadata;
|
|
// ... (rest of metadata sync logic)
|
|
if (meta && (meta.role === 'STUDENT' || meta.regNo)) {
|
|
const isExternal = meta.signUpType === 'EXTERNAL';
|
|
const table = isExternal ? 'externalusers' : 'Studentusers';
|
|
|
|
const syncData: any = {
|
|
id: user.id,
|
|
name: meta.name || user.email?.split('@')[0],
|
|
email: user.email,
|
|
reg_no: meta.regNo || meta.reg_no,
|
|
phone: meta.phone,
|
|
department: meta.department || meta.dept,
|
|
year: meta.year,
|
|
section: meta.section,
|
|
updated_at: new Date().toISOString()
|
|
};
|
|
|
|
if (isExternal) {
|
|
syncData.college = meta.collegeName || meta.college;
|
|
syncData.college_location = meta.collegeLocation;
|
|
syncData.gender = meta.gender;
|
|
}
|
|
|
|
const { data: synced, error: syncError } = await supabase
|
|
.from(table)
|
|
.upsert(syncData)
|
|
.select()
|
|
.single();
|
|
|
|
if (!syncError && synced) {
|
|
profile = synced;
|
|
finalRole = 'STUDENT';
|
|
} else {
|
|
console.warn("Auto-sync profile failed:", syncError?.message);
|
|
profile = syncData; // Use metadata-based object as semi-functional fallback
|
|
finalRole = 'STUDENT';
|
|
}
|
|
} else if (meta && (meta.role === 'ADMIN' || meta.role === 'COORDINATOR')) {
|
|
// If metadata says Admin/Coordinator but they weren't in tables, check HIGH_AUTH_ADMINS
|
|
if (user.email && HIGH_AUTH_ADMINS.includes(user.email)) {
|
|
finalRole = 'ADMIN';
|
|
} else {
|
|
finalRole = 'COORDINATOR';
|
|
}
|
|
|
|
// Try to find in Facultyusers or Adminusers as fallback
|
|
const { data: f } = await supabase.from('Facultyusers').select('*').eq('id', user.id).single();
|
|
if (f) profile = f;
|
|
else {
|
|
const { data: a } = await supabase.from('Adminusers').select('*').eq('id', user.id).single();
|
|
if (a) profile = a;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (profile && finalRole) {
|
|
// PORTAL SECURITY GUARD: Ensure actual role matches the intended portal
|
|
if (intendedRole && finalRole !== intendedRole) {
|
|
// Exception: ADMINs can access COORDINATOR portal if needed, but not vice-versa
|
|
const isPermittedOverload = (intendedRole === 'COORDINATOR' && finalRole === 'ADMIN');
|
|
|
|
if (!isPermittedOverload) {
|
|
setGlobalError(`Access Denied: Your account (${finalRole}) is not authorized for the ${intendedRole} Portal.`);
|
|
await supabase.auth.signOut();
|
|
setIsAuthenticating(false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
setCurrentUserName(profile.name || 'User');
|
|
setCurrentUserDept(profile.department || profile.dept || '');
|
|
setCurrentUserEmail(profile.email || user.email || '');
|
|
setCurrentUserPhone(profile.phone || '');
|
|
setCurrentUserPhoto(profile.profile_photo || '');
|
|
setCurrentUserId(user.id);
|
|
setCurrentUserFacultyRole(profile.role || '');
|
|
setUserRole(finalRole);
|
|
|
|
// Only redirect if we are in a state that warrants it (e.g. just logged in)
|
|
setAppState(prev => {
|
|
if (prev === 'WELCOME' || prev === 'LOGIN' || prev === 'VERIFY') {
|
|
if (finalRole === 'ADMIN') {
|
|
if (intendedRole === 'COORDINATOR') {
|
|
return 'ADMIN_LANDING'; // Event Co Hub for Admins who chose it
|
|
}
|
|
return 'FACULTY_DASHBOARD'; // Default Admin Hub
|
|
}
|
|
if (finalRole === 'COORDINATOR') return 'ADMIN_LANDING';
|
|
return 'DASHBOARD';
|
|
}
|
|
return prev;
|
|
});
|
|
|
|
// Fetch data but don't let it block the UI if it takes too long
|
|
Promise.all([
|
|
fetchAllRegistrations(),
|
|
fetchMyRegs(user.id),
|
|
fetchAnnouncements(),
|
|
fetchSpecialEvents(),
|
|
fetchDomains()
|
|
]).catch(err => console.error("Post-auth fetch error:", err));
|
|
|
|
} else {
|
|
setGlobalError("Access Denied: Account structure invalid.");
|
|
await supabase.auth.signOut();
|
|
}
|
|
} catch (err) {
|
|
console.error("Auth routing error:", err);
|
|
} finally {
|
|
setIsAuthenticating(false);
|
|
}
|
|
}, [fetchAllRegistrations, fetchMyRegs, fetchAnnouncements, fetchSpecialEvents, fetchDomains, intendedRole]);
|
|
|
|
useEffect(() => {
|
|
// Initial public data fetch
|
|
fetchAllRegistrations();
|
|
fetchAnnouncements();
|
|
fetchSpecialEvents();
|
|
fetchDomains();
|
|
|
|
// 1. Initial Session Recovery & Error Handling
|
|
const initAuth = async () => {
|
|
try {
|
|
const { data: { session }, error } = await supabase.auth.getSession();
|
|
if (error) {
|
|
console.warn("[Auth System] Session recovery failed:", error.message);
|
|
// Broaden the error detection for refresh token issues
|
|
const isRefreshTokenError =
|
|
error.message.includes('Refresh Token') ||
|
|
error.message.includes('not found') ||
|
|
error.message.includes('Invalid Refresh Token') ||
|
|
error.status === 400 ||
|
|
error.status === 401;
|
|
|
|
if (isRefreshTokenError) {
|
|
console.log("[Auth System] Stale or invalid session detected. Clearing storage...");
|
|
// Explicitly clear the storage key used in supabase.ts
|
|
localStorage.removeItem('rit-events-hub-auth');
|
|
// Attempt a clean sign out
|
|
await supabase.auth.signOut().catch(() => {});
|
|
setAppState('WELCOME');
|
|
}
|
|
}
|
|
if (session) {
|
|
handleAuthChange(session);
|
|
}
|
|
} catch (err) {
|
|
console.error("[Auth System] Critical recovery error:", err);
|
|
}
|
|
};
|
|
|
|
initAuth();
|
|
|
|
// 2. Real-time Auth State Subscription
|
|
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
|
|
handleAuthChange(session);
|
|
});
|
|
return () => subscription.unsubscribe();
|
|
}, [handleAuthChange]);
|
|
|
|
// NOTE: CreateEventForm already handles the Supabase insert.
|
|
// This callback only refreshes the local events list.
|
|
const handleAddEvent = async (_ev: Event) => {
|
|
await fetchAllRegistrations();
|
|
};
|
|
|
|
const handleDeleteEvent = async (id: string) => {
|
|
// Step 1: Delete all registrations for this event first (FK constraint: NO ACTION)
|
|
const { error: regError } = await supabase
|
|
.from('registrations')
|
|
.delete()
|
|
.eq('event_id', id);
|
|
if (regError) {
|
|
console.error('[Delete Event] Failed to delete registrations:', regError);
|
|
}
|
|
// Step 2: Delete any announcements linked to this event
|
|
await supabase.from('announcements').delete().eq('event_id', id);
|
|
// Step 3: Now delete the event itself
|
|
const { error } = await supabase.from('events').delete().eq('id', id);
|
|
if (error) {
|
|
console.error('[Delete Event] Supabase error:', error);
|
|
alert(`Failed to delete event: ${error.message}`);
|
|
} else {
|
|
await fetchAllRegistrations(); // Refresh so all hubs update
|
|
}
|
|
};
|
|
|
|
// NOTE: CreateEventForm handles event_schedule + resource_persons writes.
|
|
// This only updates the events table columns.
|
|
const handleUpdateEvent = async (ev: Event) => {
|
|
const { error } = await supabase.from('events').update({
|
|
title: ev.title,
|
|
location: ev.location,
|
|
date: ev.date,
|
|
category: ev.category,
|
|
domain: ev.domain,
|
|
pricing_type: ev.pricingType,
|
|
coordinator: ev.coordinator,
|
|
image: ev.image,
|
|
status: ev.status,
|
|
max_participants: ev.maxParticipants,
|
|
registration_deadline: ev.registrationDeadline,
|
|
duration_days: ev.durationDays,
|
|
club: ev.club,
|
|
event_summary: ev.event_summary,
|
|
is_team_event: ev.isTeamEvent,
|
|
team_size_limit: ev.teamSizeLimit,
|
|
team_composition: ev.teamComposition,
|
|
participant_type: ev.participantType,
|
|
verification_status: ev.verificationStatus,
|
|
refreshment_expense: ev.refreshment_expense || 0,
|
|
transportation_expense: ev.transportation_expense || 0,
|
|
session_coverage_fee: (ev as any).session_coverage_fee || 0,
|
|
total_expense: ev.total_expense || 0,
|
|
request_by_faculty: ev.request_by_faculty || null,
|
|
request_by_hod: ev.request_by_HOD || null
|
|
}).eq('id', ev.id);
|
|
|
|
if (error) {
|
|
console.error('[Update Event] Supabase error:', error);
|
|
alert(`Failed to update event: ${error.message}`);
|
|
} else {
|
|
await fetchAllRegistrations();
|
|
}
|
|
};
|
|
|
|
const handleUpdateSpecialEvent = async (se: any) => {
|
|
const { error } = await supabase.from('special_events').update({
|
|
title: se.title,
|
|
description: se.description,
|
|
link: se.link,
|
|
is_active: se.is_active,
|
|
verification_status: se.verificationStatus
|
|
}).eq('id', se.id);
|
|
|
|
if (error) {
|
|
console.error('[Update Special Event] Supabase error:', error);
|
|
alert(`Failed to update special event: ${error.message}`);
|
|
} else {
|
|
await fetchSpecialEvents();
|
|
}
|
|
};
|
|
|
|
const handleAddAnnouncement = async (ann: Announcement) => {
|
|
const { error } = await supabase.from('announcements').insert({
|
|
title: ann.title,
|
|
message: ann.message,
|
|
type: ann.type,
|
|
event_id: ann.eventId,
|
|
expires_at: ann.expiresAt,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
if (!error) {
|
|
if (ann.type === 'ONGOING' && ann.eventId) {
|
|
// Sync participants table when event starts
|
|
const { data: regs } = await supabase
|
|
.from('registrations')
|
|
.select('*')
|
|
.eq('event_id', ann.eventId)
|
|
.eq('payment_status', 'COMPLETED');
|
|
|
|
if (regs && regs.length > 0) {
|
|
const participantData = regs.map(r => ({
|
|
user_id: r.user_id,
|
|
event_id: r.event_id,
|
|
user_name: r.user_name,
|
|
reg_no: r.reg_no,
|
|
dept: r.dept,
|
|
year: r.year,
|
|
section: r.section,
|
|
participation_date: new Date().toISOString()
|
|
}));
|
|
|
|
await supabase.from('participants').upsert(participantData);
|
|
}
|
|
}
|
|
await fetchAnnouncements();
|
|
}
|
|
};
|
|
|
|
const handleUpdateAnnouncement = async (ann: Announcement) => {
|
|
const { error } = await supabase.from('announcements')
|
|
.update({
|
|
title: ann.title,
|
|
message: ann.message,
|
|
type: ann.type,
|
|
event_id: ann.eventId,
|
|
expires_at: ann.expiresAt
|
|
})
|
|
.eq('id', ann.id);
|
|
|
|
if (!error) {
|
|
await fetchAnnouncements();
|
|
} else {
|
|
console.error("[Database Hub] Update Rejection:", error);
|
|
alert(`Update failed: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
const handleDeleteAnnouncement = async (id: string) => {
|
|
// 1. Retrieve notice details before deletion to check type
|
|
const { data: targetAnn } = await supabase.from('announcements').select('*').eq('id', id).single();
|
|
|
|
const { error } = await supabase.from('announcements')
|
|
.delete()
|
|
.eq('id', id);
|
|
|
|
if (!error) {
|
|
// 2. If it was an 'ENDED' or 'ONGOING' notice, revert event status and wipe records
|
|
if (targetAnn && (targetAnn.type === 'ENDED' || targetAnn.type === 'ONGOING') && targetAnn.event_id) {
|
|
await supabase.from('events').update({ status: 'Scheduled' }).eq('id', targetAnn.event_id);
|
|
|
|
// Wipe attendance and participants for a clean restart
|
|
await supabase.from('attendance_records').delete().eq('event_id', targetAnn.event_id);
|
|
await supabase.from('participants').delete().eq('event_id', targetAnn.event_id);
|
|
|
|
await fetchAllRegistrations(); // Sync the event list
|
|
}
|
|
|
|
setAnnouncements(prev => prev.filter(ann => ann.id !== id));
|
|
await fetchAnnouncements();
|
|
} else {
|
|
console.error("[Database Hub] Delete Rejection:", error);
|
|
alert(`Delete rejected: ${error.message}`);
|
|
}
|
|
};
|
|
|
|
const toggleBooking = async (eventId: string) => {
|
|
if (userRole !== 'STUDENT') {
|
|
alert("Event Coordinators and Admins cannot register for events.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { data: { user } } = await supabase.auth.getUser();
|
|
if (!user) {
|
|
alert("You must be logged in to register.");
|
|
return;
|
|
}
|
|
|
|
const registrationId = `${user.id}_${eventId}`;
|
|
if (bookedEventIds.includes(eventId)) {
|
|
const { error } = await supabase.from('registrations').delete().eq('id', registrationId);
|
|
if (error) throw error;
|
|
|
|
setBookedEventIds(prev => prev.filter(id => id !== eventId));
|
|
await fetchAllRegistrations();
|
|
} else {
|
|
// Block registration if there is any unfinalized completed event (missing certificate upload or missing OD approval)
|
|
const unfinalized = userRegistrations.find(reg => {
|
|
const ev = events.find(e => String(e.id) === String(reg.event_id));
|
|
if (ev && ev.status === 'Completed') {
|
|
const hasUploadedCert = !!reg.certification_url;
|
|
const hasOd = !!(reg.od_url || reg.od);
|
|
return !hasUploadedCert || !hasOd;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (unfinalized) {
|
|
const ev = events.find(e => String(e.id) === String(unfinalized.event_id));
|
|
alert(`Registration Blocked: You must upload your certificate and obtain OD approval for your completed event "${ev?.title || 'Past Event'}" before you can register for another event.`);
|
|
return;
|
|
}
|
|
|
|
const targetEvent = events.find(e => e.id === eventId);
|
|
if (!targetEvent) {
|
|
alert("Event not found.");
|
|
return;
|
|
}
|
|
|
|
// Try to fetch from Studentusers first, then externalusers
|
|
let student = null;
|
|
const { data: internalStudent } = await supabase.from('Studentusers').select('*').eq('id', user.id).single();
|
|
if (internalStudent) {
|
|
student = internalStudent;
|
|
} else {
|
|
const { data: externalStudent } = await supabase.from('externalusers').select('*').eq('id', user.id).single();
|
|
if (externalStudent) student = externalStudent;
|
|
}
|
|
|
|
if (!student) {
|
|
console.error("Student profile not found for user:", user.id);
|
|
alert("Your student profile was not found. Please contact support.");
|
|
return;
|
|
}
|
|
|
|
// Validate overall registration capacity
|
|
if (targetEvent.maxParticipants && (targetEvent.currentParticipants || 0) >= targetEvent.maxParticipants) {
|
|
alert("This event has reached its maximum registration limit.");
|
|
return;
|
|
}
|
|
|
|
// Validate department/section quota limits (Internal only, external has no sections)
|
|
if (student.department && targetEvent.deptLimits?.[student.department]) {
|
|
const sectionLimits = targetEvent.deptSectionLimits?.[student.department];
|
|
const hasSectionLimits = sectionLimits && Object.keys(sectionLimits).length > 0;
|
|
|
|
if (hasSectionLimits) {
|
|
const studentSection = student.section || '';
|
|
const limit = sectionLimits[studentSection];
|
|
if (!limit || limit <= 0) {
|
|
alert(`Registration Blocked: Section ${studentSection} of ${student.department} department is not allowed to register for this event.`);
|
|
return;
|
|
}
|
|
const currentSectionCount = targetEvent.currentDeptSectionCounts?.[student.department]?.[studentSection] || 0;
|
|
if (currentSectionCount >= limit) {
|
|
alert(`Registration Blocked: The quota for Section ${studentSection} of department ${student.department} is full.`);
|
|
return;
|
|
}
|
|
} else {
|
|
const currentDeptCount = targetEvent.currentDeptCounts?.[student.department] || 0;
|
|
if (currentDeptCount >= targetEvent.deptLimits[student.department]) {
|
|
alert(`Registration Blocked: The quota for department ${student.department} is full.`);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const { error } = await supabase.from('registrations').insert({
|
|
id: registrationId,
|
|
user_id: user.id,
|
|
event_id: eventId,
|
|
user_email: user.email,
|
|
user_name: student.name,
|
|
reg_no: student.reg_no,
|
|
phone: student.phone,
|
|
gender: student.gender,
|
|
dept: student.department,
|
|
section: student.section,
|
|
year: student.year,
|
|
college: student.college_name || student.college || 'Rajalakshmi Institute of Technology',
|
|
payment_status: targetEvent?.pricingType === 'FREE' ? 'COMPLETED' : 'PENDING'
|
|
});
|
|
|
|
if (error) {
|
|
if (error.code === '23505') {
|
|
alert("You are already registered for this event.");
|
|
} else {
|
|
throw error;
|
|
}
|
|
} else {
|
|
await fetchMyRegs(user.id);
|
|
await fetchAllRegistrations();
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
console.error("Booking Error:", err);
|
|
alert(`Registration failed: ${err.message || 'Unknown error'}`);
|
|
}
|
|
};
|
|
|
|
const handleLogout = async () => {
|
|
await supabase.auth.signOut();
|
|
setAppState('WELCOME');
|
|
setUserRole(null);
|
|
setCurrentUserId('');
|
|
setCurrentUserFacultyRole('');
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen relative">
|
|
{isAuthenticating && (
|
|
<div className="fixed inset-0 z-[9999] bg-white/95 backdrop-blur-xl flex flex-col items-center justify-center animate-in fade-in duration-300">
|
|
<div className="w-16 h-16 border-4 border-slate-200 border-t-[#f97316] rounded-full animate-spin mb-6 shadow-2xl"></div>
|
|
<h2 className="text-xl font-black text-slate-800 uppercase tracking-widest">Verifying Portal</h2>
|
|
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-[0.4em] mt-2">Authenticating Academic Credentials</p>
|
|
</div>
|
|
)}
|
|
|
|
{globalError && (
|
|
<div className="fixed top-10 left-1/2 -translate-x-1/2 z-[10000] bg-rose-600 text-white px-8 py-4 rounded-2xl shadow-2xl font-black text-xs uppercase tracking-widest flex items-center gap-3 animate-in slide-in-from-top-10">
|
|
<i className="fas fa-exclamation-triangle"></i>
|
|
{globalError}
|
|
<button onClick={() => setGlobalError(null)} className="ml-4 hover:opacity-50"><i className="fas fa-times"></i></button>
|
|
</div>
|
|
)}
|
|
|
|
{appState === 'WELCOME' && <WelcomeScreen onSelectRole={(role) => { setIntendedRole(role); setUserRole(role); setAppState('LOGIN'); }} events={events} />}
|
|
{appState === 'LOGIN' && userRole && (
|
|
<LoginForm
|
|
role={userRole}
|
|
onSuccess={() => setIsAuthenticating(true)}
|
|
onBack={() => setAppState('WELCOME')}
|
|
/>
|
|
)}
|
|
{appState === 'DASHBOARD' && (userRole === 'STUDENT' || userRole === 'COORDINATOR' || userRole === 'ADMIN') && (
|
|
<Dashboard
|
|
userRole={userRole}
|
|
events={events}
|
|
announcements={announcements}
|
|
bookedEventIds={bookedEventIds}
|
|
userRegistrations={userRegistrations}
|
|
onToggleBooking={toggleBooking}
|
|
onLogout={handleLogout}
|
|
currentUserName={currentUserName}
|
|
onUploadCertificate={async () => ""}
|
|
studentDocuments={studentDocuments}
|
|
onUploadDoc={(studentId, data) => setStudentDocuments(prev => ({ ...prev, [studentId]: data }))}
|
|
onBackToCoordinatorHub={(userRole === 'COORDINATOR' || userRole === 'ADMIN') ? () => setAppState('ADMIN_LANDING') : undefined}
|
|
specialEvents={specialEvents}
|
|
/>
|
|
)}
|
|
{appState === 'ADMIN_LANDING' && (userRole === 'COORDINATOR' || userRole === 'ADMIN') && (
|
|
<AdminLandingPage
|
|
onBack={userRole === 'ADMIN' ? () => setAppState('FACULTY_DASHBOARD') : handleLogout}
|
|
onAddEvent={handleAddEvent}
|
|
onUpdateEvent={handleUpdateEvent}
|
|
onDeleteEvent={handleDeleteEvent}
|
|
events={events} announcements={announcements}
|
|
onAddAnnouncement={handleAddAnnouncement}
|
|
onDeleteAnnouncement={handleDeleteAnnouncement}
|
|
onUpdateAnnouncement={handleUpdateAnnouncement}
|
|
studentDocuments={studentDocuments} onApproveCertificate={async (id) => { await supabase.from('registrations').update({ certification_status: 'APPROVED' }).eq('id', id); fetchAllRegistrations(); }}
|
|
localRegistrations={allRegistrations}
|
|
onViewStudentHub={() => setAppState('DASHBOARD')}
|
|
currentUserDept={currentUserDept}
|
|
currentUserEmail={currentUserEmail}
|
|
currentUserName={currentUserName}
|
|
currentUserPhone={currentUserPhone}
|
|
currentUserPhoto={currentUserPhoto}
|
|
currentUserId={currentUserId}
|
|
currentUserFacultyRole={currentUserFacultyRole}
|
|
domains={domains}
|
|
/>
|
|
)}
|
|
{appState === 'FACULTY_DASHBOARD' && userRole === 'ADMIN' && (
|
|
<FacultyDashboard
|
|
onLogout={handleLogout}
|
|
events={events}
|
|
specialEvents={specialEvents}
|
|
domains={domains}
|
|
announcements={announcements}
|
|
studentDocuments={studentDocuments}
|
|
onApproveCertificate={async (id) => { await supabase.from('registrations').update({ certification_status: 'APPROVED' }).eq('id', id); fetchAllRegistrations(); }}
|
|
onUpdateEvent={handleUpdateEvent}
|
|
onUpdateSpecialEvent={handleUpdateSpecialEvent}
|
|
localRegistrations={allRegistrations}
|
|
onViewCoordinatorHub={() => setAppState('ADMIN_LANDING')}
|
|
onViewStudentHub={() => setAppState('DASHBOARD')}
|
|
currentUserName={currentUserName}
|
|
currentUserDept={currentUserDept}
|
|
currentUserEmail={currentUserEmail}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App; |