diff --git a/RIT-EVENT-MANAGEMENT--main/.gitignore b/RIT-EVENT-MANAGEMENT--main/.gitignore deleted file mode 100644 index de71847..0000000 --- a/RIT-EVENT-MANAGEMENT--main/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# Environment files -.env -.env.local -.env.*.local - -# SQL scripts (one-time migrations) -*.sql - -# OS files -Thumbs.db diff --git a/RIT-EVENT-MANAGEMENT--main/App.tsx b/RIT-EVENT-MANAGEMENT--main/App.tsx deleted file mode 100644 index 07e809a..0000000 --- a/RIT-EVENT-MANAGEMENT--main/App.tsx +++ /dev/null @@ -1,922 +0,0 @@ -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('WELCOME'); - const [userRole, setUserRole] = useState(null); - const [events, setEvents] = useState([]); - const [announcements, setAnnouncements] = useState([]); - const [specialEvents, setSpecialEvents] = useState([]); - const [domains, setDomains] = useState([]); - const [userRegistrations, setUserRegistrations] = useState([]); - const [allRegistrations, setAllRegistrations] = useState([]); - const [bookedEventIds, setBookedEventIds] = useState([]); - const [currentUserName, setCurrentUserName] = useState('User'); - const [currentUserDept, setCurrentUserDept] = useState(''); - const [currentUserEmail, setCurrentUserEmail] = useState(''); - const [currentUserPhone, setCurrentUserPhone] = useState(''); - const [currentUserPhoto, setCurrentUserPhoto] = useState(''); - const [currentUserId, setCurrentUserId] = useState(''); - const [currentUserFacultyRole, setCurrentUserFacultyRole] = useState(''); - const [ticketToVerify, setTicketToVerify] = useState<{ ticket: Ticket, event: Event } | null>(null); - const [isAuthenticating, setIsAuthenticating] = useState(false); - const [globalError, setGlobalError] = useState(null); - const [studentDocuments, setStudentDocuments] = useState>({}); - const [showStudentHubForAdmin, setShowStudentHubForAdmin] = useState(false); - const [intendedRole, setIntendedRole] = useState(null); - - const mapDbToEvent = useCallback((dbEvent: any, registrations: any[] = []): Event => { - const eventRegs = registrations.filter(r => String(r.event_id) === String(dbEvent.id)); - const deptCounts: Record = {}; - const deptSectionCounts: Record> = {}; - - 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 = {}; - const eventDeptSectionLimits: Record> = {}; - (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 or auto-register from Google email format - const meta = user.user_metadata; - const emailLower = (user.email || '').trim().toLowerCase(); - const studentMatch = emailLower.match(/^student\.(\d{6})@([a-zA-Z0-9&-_]+)\.ritchennai\.edu\.in$/); - - if (studentMatch) { - const rollNo = studentMatch[1]; - const rawDept = studentMatch[2].toLowerCase(); - - // Calculate joining year and academic year (current local time is 2026-06-19) - const joinYear = 2000 + parseInt(rollNo.substring(0, 2)); - const currentYear = new Date().getFullYear(); - const currentMonth = new Date().getMonth(); - const academicYearOffset = currentMonth >= 5 ? 1 : 0; - const yearIndex = currentYear - joinYear + academicYearOffset; - const years = ["1st Year", "2nd Year", "3rd Year", "4th Year"]; - const calculatedYear = years[yearIndex - 1] || "N/A"; - - // Map department - let dept = rawDept.toUpperCase(); - if (dept === 'AIDS') dept = 'AI&DS'; - else if (dept === 'AIML') dept = 'AI&ML'; - else if (dept === 'VLSI') dept = 'EE(VLSI)'; - else if (dept === 'BIOTECH' || dept === 'BIO-TECH') dept = 'BIOTECH'; - else if (dept === 'H&S') dept = 'H&S Dept'; - - const syncData = { - id: user.id, - name: meta?.name || `Student ${rollNo}`, - email: emailLower, - reg_no: rollNo, - phone: '', - department: dept, - year: calculatedYear, - section: 'A', - college_name: 'Rajalakshmi Institute of Technology', - updated_at: new Date().toISOString() - }; - - const { data: synced } = await supabase.from('Studentusers').upsert(syncData).select().single(); - profile = synced || syncData; - finalRole = 'STUDENT'; - } else if (emailLower && !emailLower.endsWith('@ritchennai.edu.in') && !emailLower.endsWith('@rit.edu') && !HIGH_AUTH_ADMINS.includes(emailLower)) { - // Auto-register external user if signing in via Google with a non-campus email - const syncData = { - id: user.id, - name: meta?.name || emailLower.split('@')[0], - email: emailLower, - reg_no: 'EXT-' + Math.floor(Math.random() * 100000), - phone: '', - department: 'Others', - year: 'N/A', - section: 'N/A', - college: 'External Institution', - college_location: 'N/A', - gender: 'Male', - updated_at: new Date().toISOString() - }; - - const { data: synced } = await supabase.from('externalusers').upsert(syncData).select().single(); - profile = synced || syncData; - finalRole = 'STUDENT'; - } else 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; - 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); - }); - - // 3. Real-time Database Subscriptions - const eventsChannel = supabase - .channel('events-sync') - .on('postgres_changes', { event: '*', schema: 'public', table: 'events' }, () => { - fetchAllRegistrations(); - }) - .subscribe(); - - const specialEventsChannel = supabase - .channel('special-events-sync') - .on('postgres_changes', { event: '*', schema: 'public', table: 'special_events' }, () => { - fetchSpecialEvents(); - }) - .subscribe(); - - const announcementsChannel = supabase - .channel('announcements-sync') - .on('postgres_changes', { event: '*', schema: 'public', table: 'announcements' }, () => { - fetchAnnouncements(); - }) - .subscribe(); - - return () => { - subscription.unsubscribe(); - supabase.removeChannel(eventsChannel); - supabase.removeChannel(specialEventsChannel); - supabase.removeChannel(announcementsChannel); - }; - }, [handleAuthChange, fetchAllRegistrations, fetchSpecialEvents, fetchAnnouncements]); - - // 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; - } - - // Validate registration deadline - if (targetEvent.registrationDeadline) { - const deadline = new Date(targetEvent.registrationDeadline); - if (new Date() > deadline) { - alert("Registration Blocked: The registration deadline for this event has passed."); - 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 ( -
- {isAuthenticating && ( -
-
-

Verifying Portal

-

Authenticating Academic Credentials

-
- )} - - {globalError && ( -
- - {globalError} - -
- )} - - {appState === 'WELCOME' && { setIntendedRole(role); setUserRole(role); setAppState('LOGIN'); }} events={events} />} - {appState === 'LOGIN' && userRole && ( - setIsAuthenticating(true)} - onBack={() => setAppState('WELCOME')} - /> - )} - {appState === 'DASHBOARD' && (userRole === 'STUDENT' || userRole === 'COORDINATOR' || userRole === 'ADMIN') && ( - ""} - 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') && ( - 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' && ( - { 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} - /> - )} -
- ); -}; - -export default App; \ No newline at end of file diff --git a/RIT-EVENT-MANAGEMENT--main/EMS_DOCUMENTATION.md b/RIT-EVENT-MANAGEMENT--main/EMS_DOCUMENTATION.md deleted file mode 100644 index 4526239..0000000 --- a/RIT-EVENT-MANAGEMENT--main/EMS_DOCUMENTATION.md +++ /dev/null @@ -1,358 +0,0 @@ -# Rajalakshmi Institute of Technology (RIT) Events Hub — System Documentation - -Welcome to the comprehensive system documentation for the **RIT Events Hub**, a premium, high-fidelity academic event management, tracking, and auditing portal designed for Rajalakshmi Institute of Technology. - -This document provides a complete breakdown of the website's architecture, user roles, file structures, database schema, operational workflows, and features, accompanied by a detailed site map. - ---- - -## 1. Overview & Core Mission - -The **RIT Events Hub** is an institutional web application designed to digitize and manage the entire lifecycle of college events. It replaces manual event scheduling, paper registrations, physical ticket checks, and fragmented certificate auditing with a unified, secure portal. - -### Key Objectives: -* **Decentralized Event Creation:** Enable faculty members to coordinate, schedule, and estimate budgets for departmental activities. -* **Hierarchical Approvals:** Ensure events undergo appropriate institutional oversight through a HOD-to-Admin approval pipeline. -* **Intelligent Resource Management:** Prevent venue conflicts and respect departmental/sectional seat quotas. -* **Continuous Verification for Students:** Link student attendance, certificate uploads, and On-Duty (OD) approvals step-by-step. -* **Security & Accessibility:** Provide separate, secure authentication models for internal students, external students, coordinators (faculty/HOD), and administrators. - ---- - -## 2. Technology Stack & Integrations - -The platform is built using a modern, fast, and secure web development stack: - -| Layer | Technology | Description | -| :--- | :--- | :--- | -| **Frontend Framework** | React (v19), TypeScript, Vite | Multi-page single-page application (SPA) state-managed structure. | -| **Styling & Icons** | Tailwind CSS (CDN), FontAwesome | Curated responsive styling using professional color palettes (Deep Blue `#004a99` and Academic Orange `#f97316`). | -| **Backend & Auth** | Supabase (PostgreSQL, Storage, Auth) | Manages authentication, row-level security (RLS), document uploads, and database queries. | -| **PDF Generation** | `jspdf` & `jspdf-autotable` | Compiles detailed, printable event proposals containing budget sheets, schedules, and resource details. | -| **Layout Tools** | `html2canvas` | Used to capture ticket layouts for student downloads. | -| **Fonts** | Google Fonts (Inter, Playfair Display) | Custom typography reflecting academic elegance. | - ---- - -## 3. Database Schema - -The database relies on a PostgreSQL schema managed via Supabase. Below are the primary tables and relations: - -### `Studentusers` (Internal Students) -* `id` (UUID, Primary Key, references auth.users) -* `name` (Text) -* `email` (Text, Unique) -* `reg_no` (Text, Unique) -* `phone` (Text) -* `department` (Text) -* `year` (Text) -* `section` (Text) -* `college_name` (Text - Default: 'RAJALAKSHMI INSTITUTE OF TECHNOLOGY') -* `updated_at` (Timestamp) - -### `externalusers` (External Students) -* `id` (UUID, Primary Key, references auth.users) -* `name` (Text) -* `email` (Text, Unique) -* `reg_no` (Text) -* `phone` (Text) -* `department` (Text) -* `year` (Text) -* `section` (Text) -* `college` (Text) -* `college_location` (Text) -* `gender` (Text) -* `updated_at` (Timestamp) - -### `Facultyusers` (Coordinators & HODs) -* `id` (UUID, Primary Key, references auth.users) -* `name` (Text) -* `email` (Text, Unique) -* `dept` (Text) -* `role` (Text - e.g., 'Faculty', 'HOD', 'System Admin') -* `phone` (Text) -* `profile_photo` (Text - URL) -* `updated_at` (Timestamp) - -### `Adminusers` (Principal / Director / Administrators) -* `id` (UUID, Primary Key, references auth.users) -* `name` (Text) -* `email` (Text, Unique) -* `dept` (Text - 'Administration') -* `updated_at` (Timestamp) - -### `events` -* `id` (UUID, Primary Key) -* `title` (Text) -* `location` (Text - Venue name) -* `date` (Text - Standard format) -* `category` (Text - 'TECHNICAL' \| 'NON-TECHNICAL' \| 'WORKSHOP' \| 'CENTRE-ACTIVITY') -* `domain` (Text - references domains.name) -* `pricing_type` (Text - 'FREE' \| 'PAID') -* `coordinator` (Text - Coordinator Name) -* `club` (Text) -* `image` (Text - Supabase storage URL) -* `status` (Text - 'Scheduled' \| 'Event Ongoing' \| 'Completed') -* `max_participants` (Integer) -* `registration_deadline` (Timestamp) -* `duration_days` (Integer) -* `event_summary` (Text) -* `is_team_event` (Boolean) -* `team_size_limit` (Integer) -* `team_composition` (Text - 'MIXED' \| 'INTER_DEPT') -* `participant_type` (Text - 'INTERNAL' \| 'EXTERNAL' \| 'BOTH') -* `verification_status` (Text - 'PENDING' \| 'PENDING_HOD' \| 'PENDING_ADMIN' \| 'APPROVED' \| 'REJECTED') -* `refreshment_expense` (Numeric) -* `transportation_expense` (Numeric) -* `session_coverage_fee` (Numeric) -* `total_expense` (Numeric) -* `conducting_dept` (Text) -* `created_by` (UUID, references auth.users) -* `request_by_faculty` (Timestamp) -* `request_by_hod` (Timestamp) - -### `event_schedule` (Session/Batch Slots) -* `id` (UUID, Primary Key) -* `event_id` (UUID, references events.id) -* `day_idx` (Integer) -* `batch_idx` (Integer) -* `date` (Text) -* `start_time` (Text) -* `end_time` (Text) - -### `resource_persons` (Guest Details) -* `id` (UUID, Primary Key) -* `event_id` (UUID, references events.id) -* `day_idx` (Integer) -* `batch_idx` (Integer) -* `schedule_id` (UUID, references event_schedule.id) -* `type` (Text - 'INTERNAL' \| 'EXTERNAL') -* `name` (Text) -* `dept` (Text) -* `college_name` (Text) -* `phone` (Text) -* `email` (Text) - -### `event_dept_limits` (Departmental Seat Allocations) -* `id` (UUID, Primary Key) -* `event_id` (UUID, references events.id) -* `department` (Text) -* `max_seats` (Integer) -* `section_limits` (JSONB - Maps sections (A, B, C...) to integer limits) - -### `registrations` (Student Bookings) -* `id` (Text, Primary Key - Structured as `${userId}_${eventId}`) -* `user_id` (UUID, references auth.users) -* `event_id` (UUID, references events.id) -* `user_email` (Text) -* `user_name` (Text) -* `reg_no` (Text) -* `phone` (Text) -* `gender` (Text) -* `dept` (Text) -* `section` (Text) -* `year` (Text) -* `college` (Text) -* `payment_status` (Text - 'PENDING' \| 'COMPLETED') -* `registered_at` (Timestamp) -* `team_code` (Text) -* `team_name` (Text) -* `is_team_leader` (Boolean) -* `certification_url` (Text) -* `certification_status` (Text - 'PENDING' \| 'APPROVED' \| 'REJECTED') -* `od_url` (Text) -* `od` (Boolean - OD approval) - -### `announcements` (Notice Board) -* `id` (UUID, Primary Key) -* `title` (Text) -* `message` (Text) -* `type` (Text - 'DELAY' \| 'INFO' \| 'URGENT' \| 'ENDED' \| 'ONGOING') -* `event_id` (UUID, references events.id) -* `timestamp` (Timestamp) -* `expires_at` (Timestamp) - -### `special_events` (Symposiums & External Links) -* `id` (UUID, Primary Key) -* `title` (Text) -* `description` (Text) -* `link` (Text) -* `created_by` (UUID) -* `is_active` (Boolean) -* `verification_status` (Text - 'PENDING' \| 'APPROVED' \| 'REJECTED') - -### `domains` -* `id` (UUID, Primary Key) -* `name` (Text, Unique) -* `category` (Text) -* `image` (Text) -* `status` (Text - 'PENDING' \| 'APPROVED' \| 'REJECTED') -* `description` (Text) - ---- - -## 4. Operational Workflows & Key Logic - -### A. Core Blockage (Preventing Registration Overhead) -To maintain academic integrity, a student **cannot register for any new events** if they have an unfinalized event from the past. -* **Condition:** If a student is registered for an event that has completed (`status === 'Completed'`), they must upload their certificate and obtain coordinator approval (`certification_status === 'APPROVED'`) and OD status before the system allows them to click "Register" on any future catalog events. - -### B. Overlap Prevention (Intelligent Venue Booking) -During event creation, coordinates are validated against active bookings: -* **Conflict Condition:** When a coordinator selects a venue and date, the system queries existing approved/pending events. If another event occupies the same location, and the date ranges overlap based on the `durationDays`, that venue will be flagged as "Booked" or "Under Verification" in the dropdown, disabling selection. - -### C. Quota-Constrained Bookings (Sectional & Departmental Caps) -The event organizer can set constraints for RIT students: -1. **Department Limits:** Limits registration to `X` seats per department. -2. **Section Limits:** Further restricts seats down to individual sections (e.g., CSE section `A` gets 10 seats, section `B` gets 5 seats). -3. **Validation:** When a student attempts registration, the system evaluates their profile (department, section) and checks the respective quota counts in the `registrations` table. If the quota is full, booking is blocked. - -### D. Team play Alliance (Team Registrations) -* For team events, the first student registers and clicks "Form Team", which inserts a random 6-character alphanumeric code (`team_code`) and designates them as `is_team_leader`. -* Subsequent students register for the event, click "Join Team", and input the code. The system verifies the team is not full (respecting `teamSizeLimit`) and matches `INTER_DEPT` composition requirements before assigning the `team_code` to their registration. - -### E. Proposal PDF Compile -Coordinators and Admin can generate a standardized PDF proposal containing: -1. General overview (departments, clubs, metadata). -2. Logistical details (location, dates, capacity, formats). -3. Day-by-day itineraries mapped to guest resource persons and timings. -4. Departmental seat allocation tables. -5. Financial projections (Refreshment + Transportation + Session Fees = Total projected budget). -6. Signature/Verification box showing HOD & Admin approval timestamps. - ---- - -## 5. Portal Flow & Visual Site Map - -### A. Navigational Site Map Structure - -``` -[WELCOME GATEWAY] - │ - ├── STUDENT PORTAL ────► [STUDENT AUTH (Login / Sign Up)] - │ │ - │ ├── HOME DASHBOARD (Notices, Banner, Stats) - │ ├── EVENTS CATALOG (Details, Booking, Team Play) - │ ├── REGISTRATIONS (QR Tickets, Track Progress) - │ ├── STATUS TRACKER (Detailed Lifecycle Step Visualizer) - │ └── PROFILE VIEW (Update Info, Uploaded Credentials) - │ - ├── FACULTY PORTAL ────► [COORDINATOR AUTH (Faculty / HOD)] - │ │ - │ ├── OVERVIEW (Metrics & Quick Gateways) - │ ├── GENERATE EVENTS (Normal, Special, Domains) - │ ├── CREATIONS REGISTRY (Manage Owned Events, Edit/Delete) - │ ├── STATUS TRACKER (Verify Certificates, Audit ODs) - │ ├── EVENT STATUS CONTROL (Announcements, Status Updates) - │ ├── ATTENDANCE CONSOLE (Mark Student Attendance) - │ ├── PARTICIPANTS console (Download Rosters) - │ ├── PROFILE / LOG OUT - │ └── HOD VERIFICATION (HOD only - Review Departmental Proposals) - │ - └── ADMIN PORTAL ──────► [ADMIN AUTH (Principal / Director / System Admin)] - │ - ├── OVERVIEW (System Metrics) - ├── VERIFICATION QUEUE (Standard Events, Special Events, Domains) - ├── USER MANAGEMENT CONSOLE (Update Profiles, Change Roles) - ├── VENUE TRACKER CALENDAR (Visual Schedule & Collision Map) - ├── STATUS TRACKER (Audit all college certifications) - ├── GLOBAL NOTICES (Institutional notices) - └── PROFILE / LOG OUT -``` - -### B. High-Fidelity Mermaid Routing Diagram - -```mermaid -graph TD - %% Base Gateways - Welcome[Welcome Screen] -->|Select STUDENT| StudentAuth[Student Authentication] - Welcome -->|Select COORDINATOR| FacultyAuth[Faculty / HOD Authentication] - Welcome -->|Select ADMIN| AdminAuth[System Admin Authentication] - - %% Student Pathways - StudentAuth -->|Authenticated| StudentDash[Student Dashboard] - StudentDash --> SHome[Home Dashboard] - StudentDash --> SEvents[Events Catalog] - StudentDash --> SRegs[Registrations & QR Tickets] - StudentDash --> STrack[Event Status Tracker] - StudentDash --> SProfile[Student Profile] - - SEvents -->|Register & Form/Join Team| STrack - SRegs -->|Verify Status / Upload Certificate| STrack - - %% Faculty Pathways - FacultyAuth -->|Select Access Level| FacultyLvl{Access Level} - FacultyLvl -->|Faculty| FacultyOverview[Faculty Dashboard] - FacultyLvl -->|HOD| HODOverview[Faculty Dashboard + HOD Verification] - - FacultyOverview --> FHome[Overview & Statistics] - FacultyOverview --> FCreate[Create Event Form] - FacultyOverview --> FCreations[Manage Creations] - FacultyOverview --> FTracker[Status Tracker - Certificate Audit] - FacultyOverview --> FStatus[Event Status & Notices] - FacultyOverview --> FAttendance[Attendance Console] - FacultyOverview --> FParticipants[Participants Console] - FacultyOverview --> FProfile[Faculty Profile] - - HODOverview --> HVerify[HOD Verification Panel] - HVerify -->|Accept Department Request| FTracker - HVerify -->|Reject Request| FCreations - - %% Admin Pathways - AdminAuth --> AdminOverview[Admin Hub] - AdminOverview --> AHome[System Overview] - AdminOverview --> AVerifyQueue[Verification Queue - Events/Domains/Special] - AdminOverview --> AUsers[User Management Console] - AdminOverview --> AVenue[Venue Tracker Calendar] - AdminOverview --> ATracker[Status Tracker] - AdminOverview --> ANotices[Global Announcements] - AdminOverview --> AProfile[Admin Profile] - - %% Cross-Links - FCreations -->|Submit Proposal| HVerify - HVerify -->|Send to Admin Queue| AVerifyQueue - AVerifyQueue -->|Approve Event| SEvents -``` - ---- - -## 6. Directory Layout & Components Guide - -### File & Component Inventory - -* `App.tsx`: Main routing and authentication portal manager. Resolves session state, verifies metadata credentials, performs portal security checks, and loads global states. -* `supabase.ts`: Supabase client configuration, administrative client helper, and helper functions for uploading media (images, certificates) directly to Supabase storage. -* `types.ts`: TypeScript interfaces defining `Event`, `AppState`, `UserRole`, `StudentRequest`, `Ticket`, `ResourcePerson`, `EventSchedule`, `Announcement`, and `UserProfile`. -* `constants.tsx`: Lists static categories, default college domain mappings, clubs, and default showcase events. -* `utils/pdfGenerator.ts`: Handles compiling and exporting the official document proposals for verified events using `jspdf`. - -#### Components Directory (`/components/`): -1. `WelcomeScreen.tsx`: Entrance portal page displaying entry gates for students, faculty coordinators, and admins. -2. `LoginForm.tsx`: sliding dual-panel login and sign up form handling internal/external student data collection and role checks. -3. `Dashboard.tsx`: Base container routing student pages. -4. `HomeDashboard.tsx`: Displays active announcements, stats, special events, and links to catalogs. -5. `Hero.tsx`: Dynamic slide search and welcome banner for students. -6. `UpcomingEventsSlider.tsx`: Auto-scrolling showcase slider displaying upcoming events. -7. `AboutHubSection.tsx`: Summary presentation card describing the Events Hub platform. -8. `StatsSection.tsx`: Shows real-time totals of events, participants, and clubs. -9. `AboutSection.tsx`: Institutional overview details, college details, and guidelines. -10. `ContactSection.tsx`: Map coordinates, support addresses, and telephone details for college portals. -11. `EventList.tsx`: Searchable list displaying approved events by domain filter and category. -12. `EventCard.tsx`: Display card for events containing metadata triggers, register commands, and status links. -13. `RegistrationsView.tsx`: Displays registered ticket vouchers. Generates ticket QR code cards capturing registration details, allowing downloading ticket PDFs. -14. `StatusTrackerView.tsx`: visual step progress checker showing the student their certificate review, attendance logs, and OD approvals. -15. `ProfileView.tsx`: Form for students to edit profile settings and view academic transcripts. -16. `AdminLandingPage.tsx`: Base dashboard routing all Coordinator actions. -17. `CreateEventForm.tsx`: Multi-step event proposal compiler managing budgets, itineraries, and quotas. -18. `CreateSpecialEventsView.tsx`: Allows coordinators to publish external program links. -19. `CreateDomainView.tsx`: Form to submit new technical/non-technical domains for college audit. -20. `AdminStatusTrackerView.tsx`: Review board for coordinators/admins to check student certificate uploads, verify details, and approve OD statuses. -21. `AdminEventStatusView.tsx`: Controls notifications, notice updates, and events status transitions. -22. `FacultyParticipantsView.tsx`: Table listing students registered for an event with search/filter. -23. `FacultyAttendanceView.tsx`: Logs session-by-session student attendance check-ins. -24. `FacultyProfileView.tsx`: Base profile for faculty. -25. `FacultyDashboard.tsx`: Console routing Admin actions. -26. `UserManagementView.tsx`: Table displaying all registered users. Admins can edit profile metadata, manually add students, or alter roles. -27. `TicketVerificationView.tsx`: Camera/code scanner allowing organizers to scan student ticket QR codes and update status in real-time. -28. `Footer.tsx`: Institutional styled footer. -29. `PortalAnimation.tsx`: Page transitions and loading animation overlays. diff --git a/RIT-EVENT-MANAGEMENT--main/README.md b/RIT-EVENT-MANAGEMENT--main/README.md deleted file mode 100644 index 23bfb69..0000000 --- a/RIT-EVENT-MANAGEMENT--main/README.md +++ /dev/null @@ -1,20 +0,0 @@ -
-GHBanner -
- -# Run and deploy your AI Studio app - -This contains everything you need to run your app locally. - -View your app in AI Studio: https://ai.studio/apps/18ff5fc9-efef-4851-8197-ca99100fcd5f - -## Run Locally - -**Prerequisites:** Node.js - - -1. Install dependencies: - `npm install` -2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key -3. Run the app: - `npm run dev` diff --git a/RIT-EVENT-MANAGEMENT--main/components/AboutHubSection.tsx b/RIT-EVENT-MANAGEMENT--main/components/AboutHubSection.tsx deleted file mode 100644 index 39a12b9..0000000 --- a/RIT-EVENT-MANAGEMENT--main/components/AboutHubSection.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from 'react'; -import { motion } from 'motion/react'; - -const AboutHubSection: React.FC = () => { - return ( -
-
- - {/* Left Side: Image & Floating Card */} -
- - - {/* Floating Card */} - -

- INNOVATION -

-

- Where creativity meets execution. Fueling the future of tech. -

-
-
- - {/* Right Side: Content */} -
- -

- Our Legacy -

-

- What is RIT EVENTS HUB? -

-
- - - The RIT Events Hub is your centralized gateway to campus life, designed to simplify how you discover and participate in events. Whether it's a technical hackathon, a cultural fest, or a workshop, our platform brings everything to your fingertips. With seamless registration, real-time updates, and personalized recommendations, we ensure you never miss an opportunity to learn, compete, and grow. Join a community where accessing innovation is as easy as a single click. - -
- -
-
- ); -}; - -export default AboutHubSection; diff --git a/RIT-EVENT-MANAGEMENT--main/components/AboutSection.tsx b/RIT-EVENT-MANAGEMENT--main/components/AboutSection.tsx deleted file mode 100644 index 918ac8e..0000000 --- a/RIT-EVENT-MANAGEMENT--main/components/AboutSection.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import React from 'react'; - -const AboutSection: React.FC = () => { - return ( -
-
-
-

- ABOUT - -

-

- Rajalakshmi Institute of Technology (RIT) -

-
- -
-
-

- Rajalakshmi Institute of Technology ( An Autonomous Institution) is one of the best engineering colleges in Chennai and is part of Rajalakshmi Institutions, which has been synonymous with providing excellence in higher education to students for many years. -

-

- Rajalakshmi Institute of Technology was established in 2008. RIT is accredited with highest grade of A++ by NAAC. RIT is affiliated with Anna University Chennai. It is one of the AICTE-approved colleges in Chennai New Delhi, and also offers NBA-approved courses. -

-
-
- Rajalakshmi Institute of Technology -
-
- - {/* Graduate Programmes Section */} -
-

- Graduate Programmes offered -

- -
-
- RIT Students -
- -
-
-

UG Programmes

-
    -
  • B.E. Computer Science & Engineering
  • -
  • B.E. Computer Science & Engineering(AI&ML)
  • -
  • B.E. Computer & Communication Engineering
  • -
  • B.E. Electronics & Communication Engineering
  • -
  • B.E. Mechanical Engineering
  • -
  • B.E. Electronic Engineering (VLSI)
  • -
  • B.Tech. Artificial Intelligence & Data Science
  • -
  • B.Tech. Computer Science and Business Systems
  • -
  • B.Tech Bio Technology
  • -
-
- -
-

PG Programmes

-
    -
  • M.E. Electronics and Communication Engineering (VLSI Design)
  • -
-
- -
-

Anna University Approved Research Institute

-

- Ph.D. Programmes are offered across all Engineering, Technology, Science & Humanities disciplines -

-
-
-
-
- - {/* Campus Life & Events Section */} -
-

- Campus Life & Events -

- -
- {/* Left Column: YouTube Videos */} -
-
- -
-
- -
-
- - {/* Right Column: Instagram Post */} -
- -
-
-
-
-
- ); -}; - -export default AboutSection; diff --git a/RIT-EVENT-MANAGEMENT--main/components/AccreditationsSection.tsx b/RIT-EVENT-MANAGEMENT--main/components/AccreditationsSection.tsx deleted file mode 100644 index 76906bc..0000000 --- a/RIT-EVENT-MANAGEMENT--main/components/AccreditationsSection.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react'; - -const AccreditationsSection: React.FC = () => { - // No longer needed: list of individual logos replaced by a single unified picture. - // const accreditations = [...]; - - return ( -
-
- -
-
-

Our Strategic Partners

-

Global Accreditations & Collaborations

-
-
- -
-
- Global Accreditations & Collaborations -
-
-
-
- ); -}; - -export default AccreditationsSection; diff --git a/RIT-EVENT-MANAGEMENT--main/components/AdminEventStatusView.tsx b/RIT-EVENT-MANAGEMENT--main/components/AdminEventStatusView.tsx deleted file mode 100644 index 595fda0..0000000 --- a/RIT-EVENT-MANAGEMENT--main/components/AdminEventStatusView.tsx +++ /dev/null @@ -1,429 +0,0 @@ -import React, { useState } from 'react'; -import { createPortal } from 'react-dom'; -import { Event, Announcement } from '../types'; - -interface AdminEventStatusViewProps { - events: Event[]; - announcements: Announcement[]; - onAddAnnouncement: (ann: Announcement) => Promise; - onUpdateAnnouncement?: (ann: Announcement) => Promise; - onDeleteAnnouncement: (id: string) => Promise; - onUpdateEvent: (event: Event) => void; - onBack: () => void; - currentUserId?: string; - localRegistrations?: any[]; -} - -type ExpiryOption = '1H' | '6H' | '24H' | 'NEVER' | 'CUSTOM'; - -const AdminEventStatusView: React.FC = ({ - events, - announcements, - onAddAnnouncement, - onUpdateAnnouncement, - onDeleteAnnouncement, - onUpdateEvent, - onBack, - currentUserId, - localRegistrations = [] -}) => { - const [selectedEventId, setSelectedEventId] = useState(''); - const [message, setMessage] = useState(''); - const [type, setType] = useState('INFO'); - const [expiryOption, setExpiryOption] = useState('NEVER'); - const [customExpiry, setCustomExpiry] = useState(''); - const [isSubmitting, setIsSubmitting] = useState(false); - const [deletingId, setDeletingId] = useState(null); - const [editingId, setEditingId] = useState(null); - const [showOverwriteConfirm, setShowOverwriteConfirm] = useState(null); - const [confirmDeleteId, setConfirmDeleteId] = useState(null); - - const calculateExpiry = (): string | null => { - const now = new Date(); - switch (expiryOption) { - case '1H': return new Date(now.getTime() + 60 * 60 * 1000).toISOString(); - case '6H': return new Date(now.getTime() + 6 * 60 * 60 * 1000).toISOString(); - case '24H': return new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(); - case 'CUSTOM': return customExpiry ? new Date(customExpiry).toISOString() : null; - default: return null; - } - }; - - const handleEdit = (ann: Announcement) => { - setEditingId(ann.id); - setSelectedEventId(ann.eventId || ''); - const cleanMessage = ann.message.includes(']') ? ann.message.split(']').slice(1).join(']').trim() : ann.message; - setMessage(cleanMessage); - setType(ann.type); - setExpiryOption(ann.expiresAt ? 'CUSTOM' : 'NEVER'); - if (ann.expiresAt) setCustomExpiry(new Date(ann.expiresAt).toISOString().slice(0, 16)); - window.scrollTo({ top: 0, behavior: 'smooth' }); - }; - - const cancelEdit = () => { - setEditingId(null); - setMessage(''); - setSelectedEventId(''); - setExpiryOption('NEVER'); - setCustomExpiry(''); - }; - - const processDelete = async () => { - if (!confirmDeleteId) return; - const id = confirmDeleteId; - setConfirmDeleteId(null); - setDeletingId(id); - try { - await onDeleteAnnouncement(id); - } catch (err: any) { - console.error("UI Delete Trigger Error:", err); - alert("Database failed to delete. Check console for error."); - } finally { - setDeletingId(null); - } - }; - - const executeSubmit = async (overrideId?: string) => { - setIsSubmitting(true); - try { - const selectedEvent = events.find(ev => ev.id === selectedEventId); - const expiresAt = calculateExpiry(); - const targetId = overrideId || editingId; - - const announcementPayload: Announcement = { - id: targetId || '', - title: type === 'DELAY' ? 'DELAY ALERT' : type === 'URGENT' ? 'URGENT' : type === 'ENDED' ? 'CONCLUDED' : type === 'ONGOING' ? 'LIVE NOW' : 'NOTICE', - message: selectedEvent ? `[${selectedEvent.title}] ${message}` : message, - type, - timestamp: new Date().toISOString(), - expiresAt: expiresAt, - eventId: selectedEventId || undefined - }; - - if (targetId && onUpdateAnnouncement) { - await onUpdateAnnouncement(announcementPayload); - } else { - await onAddAnnouncement(announcementPayload); - } - - if (type === 'ENDED' && selectedEvent) { - onUpdateEvent({ ...selectedEvent, status: 'Completed' }); - } else if (type === 'ONGOING' && selectedEvent) { - onUpdateEvent({ ...selectedEvent, status: 'Event Ongoing' }); - } - - cancelEdit(); - setShowOverwriteConfirm(null); - } catch (err) { - console.error("Broadcast operation failed:", err); - } finally { - setIsSubmitting(false); - } - }; - - const handleDownloadExcel = () => { - if (!selectedEventId || selectedEventId === 'GENERAL') return; - const event = events.find(e => e.id === selectedEventId); - if (!event) return; - - const filteredRegs = localRegistrations.filter(reg => String(reg.event_id) === String(selectedEventId)); - if (filteredRegs.length === 0) { - alert("No participants found for this event."); - return; - } - - const headers = ['Name', 'Registration No', 'Department', 'Year', 'Email', 'Phone', 'College']; - const rows = filteredRegs.map(r => [ - r.user_name || 'N/A', - `\t${r.reg_no || 'N/A'}`, - r.dept || 'N/A', - r.year || 'N/A', - r.email || 'N/A', - r.phone || 'N/A', - r.college || 'RAJALAKSHMI INSTITUTE OF TECHNOLOGY' - ]); - - const csvContent = [ - [`EVENT: ${event.title}`].join(','), - [`EXPORTED AT: ${new Date().toLocaleString()}`].join(','), - [''], - headers.join(','), - ...rows.map(row => row.map(cell => `"${cell}"`).join(',')) - ].join('\n'); - - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.setAttribute('download', `${event.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_participants.csv`); - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }; - - const handlePublish = async () => { - if (!message.trim() || !selectedEventId) return; - - if (selectedEventId !== 'GENERAL') { - const event = events.find(e => e.id === selectedEventId); - if (!event || event.created_by !== currentUserId) { - alert("Unauthorized: Only the creator can broadcast for this event."); - return; - } - } - - executeSubmit(); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!message) return; - - if ((type === 'ENDED' || type === 'ONGOING') && selectedEventId && !editingId) { - const existingStatusNotice = announcements.find(a => a.eventId === selectedEventId && a.type === type); - if (existingStatusNotice) { - setShowOverwriteConfirm(existingStatusNotice); - return; - } - } - - handlePublish(); - }; - - const getStatusColor = (t: Announcement['type']) => { - switch (t) { - case 'URGENT': return 'border-rose-500/30 text-rose-500 bg-rose-500/5'; - case 'DELAY': return 'border-amber-500/30 text-amber-500 bg-amber-500/5'; - case 'ENDED': return 'border-emerald-500/30 text-emerald-500 bg-emerald-500/5'; - case 'ONGOING': return 'border-purple-500/30 text-purple-500 bg-purple-500/5'; - default: return 'border-blue-500/30 text-blue-500 bg-blue-500/5'; - } - }; - - return ( -
- {/* Overwrite Confirmation Modal */} - {showOverwriteConfirm && createPortal( -
-
-
- -
-

Replace Previous Log?

-

- A conclusion log already exists for this scope.
- Replacing it will overwrite the history entry. -

-
- - -
-
-
, - document.body - )} - - {/* Delete Confirmation Modal */} - {confirmDeleteId && createPortal( -
-
-
- -
-

Remove Broadcast?

-

- This action is permanent and cannot be reversed. -

-
- - -
-
-
, - document.body - )} - -
-
-

ADMIN PULSE

-

Live Management of System Notifications

-
- -
- -
- {/* Left Side: Creation Form */} -
-
-
-

{editingId ? 'Refining Broadcast' : 'New Broadcast'}

- {editingId && ( - - )} -
- -
- -
- - {selectedEventId && selectedEventId !== 'GENERAL' && ( - - )} -
-
- -
- -
- {(['DELAY', 'INFO', 'URGENT', 'ENDED', 'ONGOING'] as Announcement['type'][]).map(t => ( - - ))} -
-
- -
- -
- {(['1H', 'NEVER', 'CUSTOM'] as ExpiryOption[]).map(opt => ( - - ))} -
- {expiryOption === 'CUSTOM' && ( - setCustomExpiry(e.target.value)} - required - /> - )} -
- - -
-
- -
- -
- - {imagePreview ? ( - Preview - ) : ( -
-
- -
- Upload Cover - Max. 2MB (16:9 Recommended) -
- )} -
-
-
- - - - - ); -}; - -export default CreateDomainView; diff --git a/RIT-EVENT-MANAGEMENT--main/components/CreateEventForm.tsx b/RIT-EVENT-MANAGEMENT--main/components/CreateEventForm.tsx deleted file mode 100644 index 48b668f..0000000 --- a/RIT-EVENT-MANAGEMENT--main/components/CreateEventForm.tsx +++ /dev/null @@ -1,1135 +0,0 @@ -import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react'; -import { Event, EventDay, Batch } from '../types'; -import { CLUBS } from '../constants'; -import { uploadToSupabase, supabase } from '../supabase'; - -const DEPARTMENTS = ['AIDS', 'CSBS', 'CSE', 'CCE', 'MECH', 'VLSI', 'BIO-TECH', 'AIML', 'ECE', 'H&S']; - -interface CreateEventFormProps { - onCancel: () => void; - onSuccess: (event: Event) => void; - eventToEdit?: Event; - onSupabaseError?: () => void; -} - -const CreateEventForm: React.FC = ({ onCancel, onSuccess, eventToEdit }) => { - const parseDateForInput = (dateStr: string) => { - if (!dateStr) return ''; - try { - const date = new Date(dateStr); - if (isNaN(date.getTime())) return ''; - const yyyy = date.getFullYear(); - const mm = String(date.getMonth() + 1).padStart(2, '0'); - const dd = String(date.getDate()).padStart(2, '0'); - return `${yyyy}-${mm}-${dd}`; - } catch { - return ''; - } - }; - - const parseDateTimeForInput = (dateStr?: string) => { - if (!dateStr) return ''; - try { - const date = new Date(dateStr); - if (isNaN(date.getTime())) return ''; - const yyyy = date.getFullYear(); - const mm = String(date.getMonth() + 1).padStart(2, '0'); - const dd = String(date.getDate()).padStart(2, '0'); - const hh = String(date.getHours()).padStart(2, '0'); - const min = String(date.getMinutes()).padStart(2, '0'); - return `${yyyy}-${mm}-${dd}T${hh}:${min}`; - } catch { - return ''; - } - }; - - const mapDBRowToEvent = (row: any): Event => ({ - ...row, - registrationDeadline: row.registration_deadline, - maxParticipants: row.max_participants, - durationDays: row.duration_days, - isTeamEvent: row.is_team_event, - teamSizeLimit: row.team_size_limit, - teamComposition: row.team_composition, - participantType: row.participant_type, - verificationStatus: row.verification_status, - pricingType: row.pricing_type, - deptLimits: deptLimits, - deptSectionLimits: deptSectionLimits, - }); - - // Build dayConfigs from normalized schedule data when editing - const buildDayConfigsFromSchedule = (): EventDay[] => { - if (!eventToEdit?.schedule || eventToEdit.schedule.length === 0) { - return [{ - date: eventToEdit ? parseDateForInput(eventToEdit.date) : '', - batches: [{ id: 1, startTime: '', endTime: '' }], - startTime: '', - endTime: '', - }]; - } - // Group schedule entries by day_idx - const dayMap = new Map(); - eventToEdit.schedule.forEach(s => { - if (!dayMap.has(s.day_idx)) dayMap.set(s.day_idx, []); - dayMap.get(s.day_idx)!.push(s); - }); - const days: EventDay[] = []; - const sortedDayIdxs = Array.from(dayMap.keys()).sort((a, b) => a - b); - sortedDayIdxs.forEach(dayIdx => { - const slots = dayMap.get(dayIdx)!.sort((a, b) => a.batch_idx - b.batch_idx); - const batches: Batch[] = slots.map((s, i) => ({ - id: i + 1, - startTime: s.start_time || '', - endTime: s.end_time || '', - resourcePerson: s.resource_person || undefined, - })); - days.push({ - date: slots[0]?.date ? parseDateForInput(slots[0].date) : '', - batches, - startTime: '', - endTime: '', - }); - }); - return days; - }; - - const [formData, setFormData] = useState({ - title: eventToEdit?.title || '', - location: eventToEdit?.location || '', - date: eventToEdit ? parseDateForInput(eventToEdit.date) : '', - category: eventToEdit?.category || 'TECHNICAL' as Event['category'], - domain: eventToEdit?.domain || '', - pricingType: eventToEdit?.pricingType || 'FREE' as Event['pricingType'], - coordinator: eventToEdit?.coordinator || '', - club: eventToEdit?.club || '', - image: eventToEdit?.image || '', - registrationDeadline: eventToEdit?.registrationDeadline ? parseDateTimeForInput(eventToEdit.registrationDeadline) : '', - maxParticipants: eventToEdit?.maxParticipants?.toString() || '', - durationDays: eventToEdit?.durationDays?.toString() || '1', - event_summary: eventToEdit?.event_summary || '', - isTeamEvent: eventToEdit?.isTeamEvent || false, - teamSizeLimit: eventToEdit?.teamSizeLimit?.toString() || '', - teamComposition: eventToEdit?.teamComposition || 'MIXED' as Event['teamComposition'], - participantType: eventToEdit?.participantType || 'INTERNAL' as Event['participantType'], - conducting_dept: eventToEdit?.conducting_dept || '', - refreshment_expense: eventToEdit?.refreshment_expense?.toString() || '', - transportation_expense: eventToEdit?.transportation_expense?.toString() || '', - session_coverage_fee: eventToEdit?.session_coverage_fee?.toString() || '', - total_expense: eventToEdit?.total_expense?.toString() || '0', - dayConfigs: buildDayConfigsFromSchedule(), - }); - - const [allLocations, setAllLocations] = useState([]); - const [bookedEvents, setBookedEvents] = useState([]); - const [locationDropdownOpen, setLocationDropdownOpen] = useState(false); - const locationDropdownRef = useRef(null); - - // Close dropdown on click outside - useEffect(() => { - const handleClickOutside = (e: MouseEvent) => { - if (locationDropdownRef.current && !locationDropdownRef.current.contains(e.target as Node)) { - setLocationDropdownOpen(false); - } - }; - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - useEffect(() => { - const total = (parseFloat(formData.refreshment_expense) || 0) + (parseFloat(formData.transportation_expense) || 0) + (parseFloat(formData.session_coverage_fee) || 0); - setFormData(prev => ({ ...prev, total_expense: total.toFixed(2) })); - }, [formData.refreshment_expense, formData.transportation_expense, formData.session_coverage_fee]); - - // Sync Launch Date to Day 1 configuration - useEffect(() => { - if (formData.date && formData.dayConfigs.length > 0) { - setFormData(prev => { - const newConfigs = [...prev.dayConfigs]; - newConfigs[0] = { ...newConfigs[0], date: formData.date }; - return { ...prev, dayConfigs: newConfigs }; - }); - } - }, [formData.date]); - - useEffect(() => { - const fetchLogistics = async () => { - const { data: locs } = await supabase.from('locations').select('*'); - if (locs) setAllLocations(locs); - - // Fetch all events that might overlap (Not rejected) - const { data: evts } = await supabase.from('events') - .select('id, location, date, duration_days, verification_status') - .neq('verification_status', 'REJECTED'); - if (evts) setBookedEvents(evts); - }; - fetchLogistics(); - }, []); - - useEffect(() => { - const days = parseInt(formData.durationDays) || 1; - setFormData(prev => { - const newConfigs = [...prev.dayConfigs]; - if (days > newConfigs.length) { - for (let i = newConfigs.length; i < days; i++) { - // Default to next day - let nextDate = ''; - if (newConfigs[i - 1]?.date) { - const d = new Date(newConfigs[i - 1].date); - d.setDate(d.getDate() + 1); - nextDate = d.toISOString().split('T')[0]; - } - newConfigs.push({ date: nextDate, batches: [{ id: 1, startTime: '', endTime: '' }], startTime: '', endTime: '' }); - } - } else if (days < newConfigs.length) { - newConfigs.splice(days); - } - return { ...prev, dayConfigs: newConfigs }; - }); - }, [formData.durationDays]); - - const [deptLimits, setDeptLimits] = useState>(eventToEdit?.deptLimits || {}); - const [deptSectionLimits, setDeptSectionLimits] = useState>>(eventToEdit?.deptSectionLimits || {}); - const [activeDeptForSections, setActiveDeptForSections] = useState(null); - - const [isSubmitting, setIsSubmitting] = useState(false); - const fileInputRef = useRef(null); - - const [availableDomains, setAvailableDomains] = useState([]); - - useEffect(() => { - const fetchDomains = async () => { - const { data } = await supabase.from('domains').select('*').eq('status', 'APPROVED').eq('category', formData.category); - if (data) { - setAvailableDomains(data.sort((a, b) => a.name.localeCompare(b.name))); - } - }; - fetchDomains(); - }, [formData.category]); - - const handleImageChange = (e: React.ChangeEvent) => { - const file = e.target.files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onloadend = () => { - const img = new Image(); - img.onload = () => { - const canvas = document.createElement('canvas'); - const max_width = 800; - let width = img.width; - let height = img.height; - - if (width > max_width) { - height = Math.round((height * max_width) / width); - width = max_width; - } - - canvas.width = width; - canvas.height = height; - const ctx = canvas.getContext('2d'); - if (ctx) { - ctx.drawImage(img, 0, 0, width, height); - const compressedBase64 = canvas.toDataURL('image/jpeg', 0.7); - setFormData(prev => ({ ...prev, image: compressedBase64 })); - } else { - setFormData(prev => ({ ...prev, image: reader.result as string })); - } - }; - img.src = reader.result as string; - }; - reader.readAsDataURL(file); - } - }; - - const handleDeptLimitChange = (dept: string, value: string) => { - const numValue = parseInt(value); - setDeptLimits(prev => { - const newLimits = { ...prev }; - if (isNaN(numValue) || numValue <= 0) { - delete newLimits[dept]; - } else { - newLimits[dept] = numValue; - } - return newLimits; - }); - }; - - const handleSectionLimitChange = (dept: string, sec: string, value: string) => { - const numValue = parseInt(value); - - setDeptSectionLimits(prev => { - const newSectionLimits = { ...prev }; - if (!newSectionLimits[dept]) { - newSectionLimits[dept] = {}; - } - - if (isNaN(numValue) || numValue <= 0) { - delete newSectionLimits[dept][sec]; - } else { - newSectionLimits[dept][sec] = numValue; - } - - if (Object.keys(newSectionLimits[dept]).length === 0) { - delete newSectionLimits[dept]; - } - - // Calculate overall department limit as the sum of section limits - const sectionSum: number = newSectionLimits[dept] - ? (Object.values(newSectionLimits[dept]) as any[]).reduce((sum: number, val: any) => sum + (Number(val) || 0), 0) - : 0; - - setDeptLimits(prevDept => { - const newDeptLimits = { ...prevDept }; - if (sectionSum > 0) { - newDeptLimits[dept] = sectionSum; - } else { - delete newDeptLimits[dept]; - } - return newDeptLimits; - }); - - return newSectionLimits; - }); - }; - - const handleNumBatchesChange = (dayIndex: number, value: string) => { - const count = parseInt(value); - setFormData(prev => { - const newConfigs = [...prev.dayConfigs]; - const dayBatches = [...newConfigs[dayIndex].batches]; - - if (count >= 1 && count > dayBatches.length) { - for (let i = dayBatches.length; i < count; i++) { - dayBatches.push({ id: i + 1, startTime: '', endTime: '' }); - } - } else { - // Always keep at least 1 batch - dayBatches.splice(Math.max(count, 1)); - } - - newConfigs[dayIndex] = { ...newConfigs[dayIndex], batches: dayBatches }; - return { ...prev, dayConfigs: newConfigs }; - }); - }; - - const handleBatchTimeChange = (dayIndex: number, batchIndex: number, field: 'startTime' | 'endTime', value: string) => { - setFormData(prev => { - const newConfigs = [...prev.dayConfigs]; - const dayBatches = [...newConfigs[dayIndex].batches]; - dayBatches[batchIndex] = { ...dayBatches[batchIndex], [field]: value }; - newConfigs[dayIndex] = { ...newConfigs[dayIndex], batches: dayBatches }; - return { ...prev, dayConfigs: newConfigs }; - }); - }; - - const handleDayDateChange = (dayIndex: number, value: string) => { - setFormData(prev => { - const newConfigs = [...prev.dayConfigs]; - newConfigs[dayIndex] = { ...newConfigs[dayIndex], date: value }; - return { ...prev, dayConfigs: newConfigs }; - }); - }; - - const handleDayTimeChange = (dayIndex: number, field: 'startTime' | 'endTime', value: string) => { - setFormData(prev => { - const newConfigs = [...prev.dayConfigs]; - newConfigs[dayIndex] = { ...newConfigs[dayIndex], [field]: value }; - return { ...prev, dayConfigs: newConfigs }; - }); - }; - - const handleResourcePersonChange = (dayIndex: number, batchIndex: number | null, field: keyof any, value: any) => { - setFormData(prev => { - const newConfigs = [...prev.dayConfigs]; - if (batchIndex === null) { - // Day level (Unified) - const currentRP = newConfigs[dayIndex].resourcePerson || { type: 'INTERNAL', name: '', phone: '', email: '' }; - newConfigs[dayIndex] = { ...newConfigs[dayIndex], resourcePerson: { ...currentRP, [field]: value } }; - } else { - // Batch level - const dayBatches = [...newConfigs[dayIndex].batches]; - const currentRP = dayBatches[batchIndex].resourcePerson || { type: 'INTERNAL', name: '', phone: '', email: '' }; - dayBatches[batchIndex] = { ...dayBatches[batchIndex], resourcePerson: { ...currentRP, [field]: value } }; - newConfigs[dayIndex] = { ...newConfigs[dayIndex], batches: dayBatches }; - } - return { ...prev, dayConfigs: newConfigs }; - }); - }; - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!formData.domain || !formData.image) { - alert("Please complete all required fields including image."); - return; - } - - if (formData.registrationDeadline) { - const deadline = new Date(formData.registrationDeadline); - if (isNaN(deadline.getTime())) { - alert("Please enter a valid registration deadline."); - return; - } - if (!eventToEdit && deadline < new Date()) { - alert("Validation Error: Registration deadline cannot be in the past."); - return; - } - const eventStart = new Date(formData.dayConfigs[0]?.date || formData.date); - if (!isNaN(eventStart.getTime()) && deadline > eventStart) { - alert("Validation Error: Registration deadline cannot be after the event start date."); - return; - } - } - - setIsSubmitting(true); - try { - // Keep Base64 string directly in the database, ignoring Firebase/Supabase storage - const finalImageUrl = formData.image; - - const totalLimit = parseInt(formData.maxParticipants); - - // Build the DB row — no longer includes time, batches, event_days, dept_limits - const dbRow: Record = { - title: formData.title, - location: formData.location, - date: new Date(formData.dayConfigs[0].date || formData.date).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }), - category: formData.category, - domain: formData.domain, - pricing_type: formData.pricingType, - coordinator: formData.coordinator, - club: formData.club, - image: finalImageUrl, - registration_deadline: formData.registrationDeadline || null, - max_participants: isNaN(totalLimit) ? null : totalLimit, - duration_days: parseInt(formData.durationDays) || 1, - event_summary: formData.event_summary, - is_team_event: formData.isTeamEvent, - team_size_limit: parseInt(formData.teamSizeLimit) || null, - team_composition: formData.teamComposition, - participant_type: formData.participantType, - verification_status: eventToEdit?.verificationStatus || (formData.category === 'CENTRE-ACTIVITY' ? 'PENDING_ADMIN' : 'PENDING_HOD'), - status: eventToEdit?.status || 'Scheduled', - refreshment_expense: parseFloat(formData.refreshment_expense) || 0, - transportation_expense: parseFloat(formData.transportation_expense) || 0, - session_coverage_fee: parseFloat(formData.session_coverage_fee) || 0, - total_expense: parseFloat(formData.total_expense) || 0, - conducting_dept: formData.conducting_dept || null, - created_by: (await supabase.auth.getUser()).data.user?.id || null, - request_by_faculty: eventToEdit?.request_by_faculty || new Date().toISOString(), - request_by_hod: eventToEdit?.request_by_HOD || null - }; - - // If editing, update; if creating, insert - let createdEvent: any; - if (eventToEdit?.id) { - const { data, error } = await supabase.from('events').update(dbRow).eq('id', eventToEdit.id).select().single(); - if (error) throw error; - createdEvent = data; - - // Clean up old schedule + resource persons + dept limits when updating (cascade handles RP/sched/limits cleanup) - await supabase.from('event_schedule').delete().eq('event_id', eventToEdit.id); - await supabase.from('resource_persons').delete().eq('event_id', eventToEdit.id); - await supabase.from('event_dept_limits').delete().eq('event_id', eventToEdit.id); - } else { - const { data, error } = await supabase.from('events').insert([dbRow]).select().single(); - if (error) throw error; - createdEvent = data; - } - - // Save schedule rows to event_schedule table - const scheduleRows: any[] = []; - formData.dayConfigs.forEach((day: any, dIdx: number) => { - const formattedDate = day.date - ? new Date(day.date).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }) - : ''; - if (day.batches && day.batches.length > 0) { - day.batches.forEach((batch: any, bIdx: number) => { - scheduleRows.push({ - event_id: createdEvent.id, - day_idx: dIdx + 1, - batch_idx: bIdx + 1, - date: formattedDate, - start_time: batch.startTime || '', - end_time: batch.endTime || '', - }); - }); - } else { - // Single unified slot for the day - scheduleRows.push({ - event_id: createdEvent.id, - day_idx: dIdx + 1, - batch_idx: 1, - date: formattedDate, - start_time: day.startTime || '', - end_time: day.endTime || '', - }); - } - }); - - let insertedSchedule: any[] = []; - if (scheduleRows.length > 0) { - const { data: schedData, error: schedError } = await supabase - .from('event_schedule') - .insert(scheduleRows) - .select(); - if (schedError) { - console.error("Schedule save error:", schedError); - } else { - insertedSchedule = schedData || []; - } - } - - // Save Resource Persons with schedule_id linking - const resourcePersonsToSave: any[] = []; - formData.dayConfigs.forEach((day: any, dIdx: number) => { - if (day.batches.length === 0 || (day.batches.length === 1 && !day.batches[0].resourcePerson?.name)) { - if (day.resourcePerson?.name) { - const schedRow = insertedSchedule.find((s: any) => s.day_idx === dIdx + 1 && s.batch_idx === 1); - resourcePersonsToSave.push({ - event_id: createdEvent.id, - day_idx: dIdx + 1, - batch_idx: 1, - schedule_id: schedRow?.id || null, - ...day.resourcePerson - }); - } - } else { - day.batches.forEach((batch: any, bIdx: number) => { - if (batch.resourcePerson?.name) { - const schedRow = insertedSchedule.find((s: any) => s.day_idx === dIdx + 1 && s.batch_idx === bIdx + 1); - resourcePersonsToSave.push({ - event_id: createdEvent.id, - day_idx: dIdx + 1, - batch_idx: bIdx + 1, - schedule_id: schedRow?.id || null, - ...batch.resourcePerson - }); - } - }); - } - }); - - if (resourcePersonsToSave.length > 0) { - const { error: rpError } = await supabase - .from('resource_persons') - .insert(resourcePersonsToSave); - - if (rpError) { - console.error("Resource Person save error:", rpError); - } - } - - // Save department limits to event_dept_limits table - const deptLimitRows = Object.entries(deptLimits).map(([dept, maxSeats]) => ({ - event_id: createdEvent.id, - department: dept, - max_seats: maxSeats, - section_limits: deptSectionLimits[dept] || {}, - })); - - if (deptLimitRows.length > 0) { - const { error: deptLimitsError } = await supabase - .from('event_dept_limits') - .insert(deptLimitRows); - if (deptLimitsError) { - console.error("Dept limits save error:", deptLimitsError); - } - } - - onSuccess(mapDBRowToEvent(createdEvent)); - } catch (err: any) { - console.error("Submission error:", err); - alert(`Failed to save event: ${err?.message || JSON.stringify(err)}`); - } finally { - setIsSubmitting(false); - } - }; - - const categories: Event['category'][] = ['TECHNICAL', 'NON-TECHNICAL', 'WORKSHOP', 'CENTRE-ACTIVITY']; - - return ( -
-
-
-

- {eventToEdit ? 'Refine Session' : 'Generate Event'} -

-

Coordinator Hub Portal

-
- -
- -
- {/* Section 1: Event Identity */} -
-

- - 01. Core Identity -

-
-
- - setFormData({ ...formData, title: e.target.value })} /> -
-
-
- - setFormData({ ...formData, coordinator: e.target.value })} /> -
-
- - -
-
- - -
-
-
-
- - {/* Section 1.5: Financial Governance (Admin Only View) */} -
-

- - 01B. Financial Projections -

-
-
-
- -
- - setFormData({ ...formData, refreshment_expense: e.target.value })} /> -
-
-
- -
- - setFormData({ ...formData, transportation_expense: e.target.value })} /> -
-
-
- -
- - setFormData({ ...formData, session_coverage_fee: e.target.value })} /> -
-
-
- -
- - -
-
-
-

- - Financial data is visible to you and administrative auditors only. -

-
-
- - {/* Section 2: Logistics */} -
-

- - 02. Logistics & Hosting -

-
-
-
- - { - const newDate = e.target.value; - setFormData(prev => { - const newConfigs = [...prev.dayConfigs]; - if (newConfigs.length > 0) newConfigs[0] = { ...newConfigs[0], date: newDate }; - return { ...prev, date: newDate, dayConfigs: newConfigs }; - }); - }} /> -
-
- - setFormData({ ...formData, durationDays: e.target.value })} /> -
-
-
- - {/* Custom dropdown for proper color support */} -
- - {locationDropdownOpen && ( -
- {Array.from(new Set(allLocations.map(l => l.block))).map(blockName => ( -
-
- {blockName} -
- {allLocations.filter(l => l.block === blockName).map(loc => { - const currentSelectedDate = formData.dayConfigs[0].date; - const isBooked = bookedEvents.find(ev => { - // Must match the location name - if (!ev.date || ev.location !== loc.name) return false; - // Skip the current event if we're editing it - if (eventToEdit?.id && ev.id === eventToEdit.id) return false; - - // If no date is selected yet, treat ALL existing bookings for this location as conflicts - if (!currentSelectedDate) return true; - - // Date overlap check: does the new event's date range overlap the existing one? - const existingStart = new Date(ev.date); - const existingEnd = new Date(ev.date); - existingEnd.setDate(existingEnd.getDate() + (ev.duration_days || 1)); - - const newStart = new Date(currentSelectedDate); - const newEnd = new Date(currentSelectedDate); - newEnd.setDate(newEnd.getDate() + (parseInt(formData.durationDays) || 1)); - - return newStart < existingEnd && existingStart < newEnd; - }); - - const isPending = isBooked?.verification_status === 'PENDING'; - const isApproved = isBooked?.verification_status === 'APPROVED'; - const isSelected = formData.location === loc.name; - - return ( - - ); - })} -
- ))} -
- )} -
- {/* Hidden required input for form validation */} - {}} /> -
-
-
- - {/* Section 3: Itinerary */} -
-

- - 03. Itinerary Scheduling -

-
- {formData.dayConfigs.map((day, dIdx) => ( -
-
- Day {dIdx + 1} Configuration -
- handleDayDateChange(dIdx, e.target.value)} /> - -
-
- - {day.batches.length === 0 ? ( - /* Unified Day View */ -
-
-
- Start Time - handleDayTimeChange(dIdx, 'startTime', e.target.value)} /> -
-
- End Time - handleDayTimeChange(dIdx, 'endTime', e.target.value)} /> -
-
- {/* Resource Person for Unified Day */} -
-

Resource Person

-
-
- -
- - -
-
-
- - handleResourcePersonChange(dIdx, null, 'name', e.target.value)} /> -
- {day.resourcePerson?.type === 'EXTERNAL' ? ( -
-
- - handleResourcePersonChange(dIdx, null, 'college_name', e.target.value)} /> -
-
- - handleResourcePersonChange(dIdx, null, 'dept', e.target.value)} /> -
-
- ) : ( -
- - handleResourcePersonChange(dIdx, null, 'dept', e.target.value)} /> -
- )} -
- - handleResourcePersonChange(dIdx, null, 'phone', e.target.value)} /> -
-
- - handleResourcePersonChange(dIdx, null, 'email', e.target.value)} /> -
-
-
-
- ) : ( -
- {day.batches.map((batch, bIdx) => ( -
-
- Phase 0{batch.id} Configuration -
- handleBatchTimeChange(dIdx, bIdx, 'startTime', e.target.value)} /> - / - handleBatchTimeChange(dIdx, bIdx, 'endTime', e.target.value)} /> -
-
- - {/* Resource Person for Batch */} -
-
- -
- - -
-
-
- - handleResourcePersonChange(dIdx, bIdx, 'name', e.target.value)} /> -
- {batch.resourcePerson?.type === 'EXTERNAL' ? ( -
-
- - handleResourcePersonChange(dIdx, bIdx, 'college_name', e.target.value)} /> -
-
- - handleResourcePersonChange(dIdx, bIdx, 'dept', e.target.value)} /> -
-
- ) : ( -
- - handleResourcePersonChange(dIdx, bIdx, 'dept', e.target.value)} /> -
- )} -
- - handleResourcePersonChange(dIdx, bIdx, 'phone', e.target.value)} /> -
-
- - handleResourcePersonChange(dIdx, bIdx, 'email', e.target.value)} /> -
-
-
- ))} -
- )} -
- ))} -
-
- - {/* Section 4: Governance */} -
-

- - 04. Access Control -

-
-
-
- - setFormData({ ...formData, registrationDeadline: e.target.value })} /> -
-
- - 0 - ? 'bg-slate-100 border-slate-100 text-slate-400 cursor-not-allowed' - : 'bg-white border-slate-200 text-slate-900 focus:border-[#004a99]' - }`} - value={formData.maxParticipants} - disabled={Object.keys(deptLimits).length > 0} - onChange={e => setFormData({ ...formData, maxParticipants: e.target.value })} - /> - {Object.keys(deptLimits).length > 0 && ( -

↓ Controlled by Dept Quotas

- )} -
-
- -
-
-

Collective Entry

-

Enable Team Registrations

-
-
- {formData.isTeamEvent && ( -
- ['-', '+', 'e', 'E'].includes(e.key) && e.preventDefault()} placeholder="Size" className="w-20 bg-slate-50 border border-slate-100 rounded-xl px-3 py-2 text-[10px] font-black" value={formData.teamSizeLimit} onChange={e => setFormData({ ...formData, teamSizeLimit: e.target.value })} /> - -
- )} - -
-
- -
- -
- {['INTERNAL', 'EXTERNAL', 'BOTH'].map(t => ( - - ))} -
-
-
-
- - {/* Section 5: Quotas */} -
-

- - 05. Department Quotas -

-
- {formData.maxParticipants !== '' && parseInt(formData.maxParticipants) > 0 && ( -
- -

Dept quotas locked — Total Capacity is set. Clear it first to use department-level limits.

-
- )} -
- {DEPARTMENTS.slice(0, 10).map(dept => { - const hasSectionLimits = deptSectionLimits[dept] && Object.keys(deptSectionLimits[dept]).length > 0; - return ( -
- {dept} - 0 - ? 'bg-slate-100 text-slate-300 cursor-not-allowed' - : hasSectionLimits - ? 'bg-blue-50 text-[#004a99] cursor-not-allowed' - : 'bg-slate-50 focus:text-[#004a99]' - }`} - value={deptLimits[dept] || ''} - disabled={(formData.maxParticipants !== '' && parseInt(formData.maxParticipants) > 0) || hasSectionLimits} - onChange={(e) => handleDeptLimitChange(dept, e.target.value)} - /> - -
- ); - })} -
- - {/* Per-Department Section Quota Inputs */} - {activeDeptForSections && ( -
-
-
-

- - Section limits for {activeDeptForSections} -

-

- Specify capacity for each section. Leaving a section blank will prevent students in that section from registering. -

-
- -
-
- {['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'].map(sec => { - const val = deptSectionLimits[activeDeptForSections]?.[sec] ?? ''; - return ( -
- Sec {sec} - handleSectionLimitChange(activeDeptForSections, sec, e.target.value)} - /> -
- ); - })} -
-
- )} -
-
- - {/* Section 6: Taxonomy */} -
-

- - 06. Taxonomy & Domain -

-
-
-
- - -
-
- - -
-
-
-

Pricing Model

-
- - -
-
-
-
- - {/* Section 7: Assets */} -
-

- - 07. Digital Assets & Summary -

-
- - - -
- -