import React, { useState, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { FacultyView, Event, Announcement } from '../types'; import FacultyProfileView from './FacultyProfileView'; import FacultyNotifications from './FacultyNotifications'; import AdminStatusTrackerView from './AdminStatusTrackerView'; import UserManagementView from './UserManagementView'; import FacultyParticipantsView from './FacultyParticipantsView'; import { supabase } from '../supabase'; import { generateEventDetailsPDF } from '../utils/pdfGenerator'; interface FacultyDashboardProps { onLogout: () => void; events: Event[]; announcements: Announcement[]; studentDocuments: Record; onApproveCertificate: (regId: string) => Promise; localRegistrations?: any[]; onViewStudentHub?: () => void; currentUserName?: string; currentUserDept?: string; currentUserEmail?: string; onUpdateEvent: (event: Event) => void; specialEvents?: any[]; onUpdateSpecialEvent?: (se: any) => Promise; } // Extend FacultyView to include STATUS_TRACKER, USER_MANAGEMENT, PARTICIPANTS, PROFILE for faculty type FacultyExtendedView = FacultyView | 'STATUS_TRACKER' | 'USER_MANAGEMENT' | 'PARTICIPANTS' | 'PROFILE' | 'VERIFY' | 'TRACK_VENUE'; const Toast: React.FC<{ message: string; onClose: () => void }> = ({ message, onClose }) => { useEffect(() => { const timer = setTimeout(onClose, 3000); return () => clearTimeout(timer); }, [onClose]); return createPortal(
{message}
, document.body ); }; const FacultyDashboard: React.FC = ({ onLogout, events, announcements, studentDocuments, onApproveCertificate, localRegistrations = [], onViewStudentHub, currentUserName, currentUserDept, currentUserEmail, onUpdateEvent, specialEvents = [], onUpdateSpecialEvent }) => { const [activeView, setActiveView] = useState('OVERVIEW'); const [isLoaded, setIsLoaded] = useState(false); const [toastMsg, setToastMsg] = useState(null); const [processingIds, setProcessingIds] = useState>(new Set()); const [removedIds, setRemovedIds] = useState>(new Set()); const [pendingDomains, setPendingDomains] = useState([]); useEffect(() => { setIsLoaded(true); }, []); useEffect(() => { if (activeView === 'VERIFY') { fetchPendingDomains(); } }, [activeView]); const fetchPendingDomains = async () => { const { data, error } = await supabase.from('domains').select('*').eq('status', 'PENDING'); if (!error && data) { setPendingDomains(data); } }; const handleCloseToast = useCallback(() => setToastMsg(null), []); const menuItems: { id: FacultyExtendedView; label: string; icon: string }[] = [ { id: 'OVERVIEW', label: 'OVERVIEW', icon: 'fa-chart-pie' }, { id: 'STATUS_TRACKER', label: 'TRACKER', icon: 'fa-route' }, { id: 'VERIFY', label: 'VERIFY', icon: 'fa-shield-check' }, { id: 'TRACK_VENUE', label: 'VENUES', icon: 'fa-map-marker-alt' }, { id: 'USER_MANAGEMENT', label: 'USERS', icon: 'fa-users-cog' }, { id: 'PARTICIPANTS', label: 'PARTICIPANTS', icon: 'fa-users' }, { id: 'NOTIFICATIONS', label: 'NOTICES', icon: 'fa-bell' }, { id: 'PROFILE', label: 'PROFILE', icon: 'fa-user-tie' }, ]; const handleToast = (msg: string) => setToastMsg(msg); const renderView = () => { switch (activeView) { case 'OVERVIEW': return (
Institutional Admin Portal

ADMIN HUB

Orchestrate Academic Excellence
Review student progress, track events, and oversee departmental activities.

{/* Coordinator Hub navigation explicitly removed for Admin Hub pure view */} {onViewStudentHub && ( )}
); case 'VERIFY': { const pendingEvents = events.filter(e => e.verificationStatus === 'PENDING_ADMIN' && !removedIds.has(e.id)); const pendingSpecialEvents = specialEvents.filter(se => (se.verificationStatus === 'PENDING' || se.verificationStatus === 'PENDING_ADMIN') && !removedIds.has(se.id)); const handleVerifyAction = async (item: any, status: 'APPROVED' | 'REJECTED', isSpecial: boolean) => { if (processingIds.has(item.id)) return; setProcessingIds(prev => new Set(prev).add(item.id)); try { if (isSpecial && onUpdateSpecialEvent) { await onUpdateSpecialEvent({ ...item, verificationStatus: status }); } else { await onUpdateEvent({ ...item, verificationStatus: status }); } // Optimistic removal setRemovedIds(prev => new Set(prev).add(item.id)); handleToast(`"${item.title}" ${status === 'APPROVED' ? 'Approved' : 'Rejected'} successfully!`); } catch (err) { handleToast(`Failed to update "${item.title}". Please try again.`); } finally { setProcessingIds(prev => { const next = new Set(prev); next.delete(item.id); return next; }); } }; return (

Verification Queue

{(pendingEvents.length + pendingSpecialEvents.length + pendingDomains.filter((d: any) => !removedIds.has(d.id)).length)} Submissions Awaiting Audit

{/* Standard Events */} {pendingEvents.map((event) => (
{event.title}
Normal Event
{event.pricingType}

{event.title}

Normal Event · {event.category}

{/* Full Details Line-by-Line */}
Coordinator: {event.coordinator || '—'}
Dept: {event.conducting_dept || '—'}
Club/Entity: {event.club || '—'}
Date: {event.date || '—'}
Time: {event.schedule?.[0]?.start_time || '—'}
Venue: {event.location || '—'}
Duration: {event.durationDays || 1} Day{(event.durationDays || 1) > 1 ? 's' : ''}
Domain: {event.domain || '—'}
Capacity: {event.maxParticipants ? `${event.maxParticipants} seats` : 'Unlimited'}
Access: {event.participantType || 'INTERNAL'}
{event.isTeamEvent &&
Team Size: {event.teamSizeLimit || '—'} ({event.teamComposition})
} {event.registrationDeadline &&
Deadline: {new Date(event.registrationDeadline).toLocaleString()}
} {event.event_summary && (
{event.event_summary}
)}
{/* Financial Projection Section */}

Financial Projections

Refreshment ₹{event.refreshment_expense || 0}
Transportation ₹{event.transportation_expense || 0}
Session Coverage ₹{(event as any).session_coverage_fee || 0}
Total Projected ₹{event.total_expense || 0}
))} {/* Special Events */} {pendingSpecialEvents.map((event) => (
Special Event

{event.title}

External Link Portal

{event.link}

{event.description}

))} {/* Pending Domains */} {pendingDomains.filter((d: any) => !removedIds.has(d.id)).map((domain) => (
{domain.name}
New Domain

{domain.name}

Category: {domain.category}

{domain.description || 'No description provided'}

))} {pendingEvents.length === 0 && pendingSpecialEvents.length === 0 && pendingDomains.filter((d: any) => !removedIds.has(d.id)).length === 0 && (

Audit Complete

No submissions remaining in the verification pipeline.

)}
{/* ── Verified & Scheduled Records ── */} {(() => { const verifiedEvents = events.filter(e => e.verificationStatus === 'APPROVED'); const verifiedSpecial = specialEvents.filter(se => se.verificationStatus === 'APPROVED'); const totalVerified = verifiedEvents.length + verifiedSpecial.length; return totalVerified > 0 ? (

Verified & Scheduled Records

{totalVerified} approved event{totalVerified !== 1 ? 's' : ''} on record

{verifiedEvents.map((ev, idx) => ( ))} {verifiedSpecial.map((ev, idx) => ( ))}
# Event Type Coordinator Date Venue Category Status
{idx + 1}
{ev.image ? ( {ev.title} ) : (
)}
{ev.title}
Normal {ev.coordinator || '—'} {ev.date || '—'} {ev.location || '—'} {ev.category || 'General'}
{ev.status || 'Scheduled'}
{verifiedEvents.length + idx + 1}
{ev.title}
Special {ev.coordinator || '—'} {ev.date || '—'} {ev.link || '—'} Special
Approved
) : null; })()}
); } case 'STATUS_TRACKER': return ( ); case 'USER_MANAGEMENT': return ; case 'PARTICIPANTS': return ; case 'NOTIFICATIONS': return ; case 'PROFILE': return ; case 'TRACK_VENUE': { const TrackVenueView = () => { const [selectedVenue, setSelectedVenue] = React.useState(null); const DAY_NAMES = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; const today = new Date(); const venues = Array.from(new Set(events.filter(e => e.location).map(e => e.location))).sort(); const venueEvents = selectedVenue ? events.filter(e => e.location === selectedVenue) : []; const [calYear, setCalYear] = React.useState(() => { let first: Date | null = null; venueEvents.forEach(ev => { try { const d = new Date(ev.date); if (!isNaN(d.getTime()) && (first === null || d < first)) first = d; } catch { /* skip */ } }); return (first ?? today).getFullYear(); }); const [calMonth, setCalMonth] = React.useState(() => { let first: Date | null = null; venueEvents.forEach(ev => { try { const d = new Date(ev.date); if (!isNaN(d.getTime()) && (first === null || d < first)) first = d; } catch { /* skip */ } }); return (first ?? today).getMonth(); }); const prevMonthFn = () => { if (calMonth === 0) { setCalMonth(11); setCalYear(y => y - 1); } else setCalMonth(m => m - 1); }; const nextMonthFn = () => { if (calMonth === 11) { setCalMonth(0); setCalYear(y => y + 1); } else setCalMonth(m => m + 1); }; const goToday = () => { setCalYear(today.getFullYear()); setCalMonth(today.getMonth()); }; // dateKey → events[] const eventsByDate: Record = {}; venueEvents.forEach(ev => { try { const base = new Date(ev.date); if (isNaN(base.getTime())) return; const numDays = Math.max(ev.durationDays || 1, 1); for (let i = 0; i < numDays; i++) { const dd = new Date(base); dd.setDate(base.getDate() + i); const key = dd.getFullYear() + '-' + String(dd.getMonth() + 1).padStart(2, '0') + '-' + String(dd.getDate()).padStart(2, '0'); eventsByDate[key] = [...(eventsByDate[key] || []), ev]; } } catch { /* skip */ } }); // 42-cell calendar grid const firstDOW = new Date(calYear, calMonth, 1).getDay(); const daysInMonth = new Date(calYear, calMonth + 1, 0).getDate(); const prevMonthDays = new Date(calYear, calMonth, 0).getDate(); type Cell = { day: number; kind: 'prev' | 'cur' | 'next'; key: string }; const cells: Cell[] = []; for (let i = 0; i < firstDOW; i++) { const d = prevMonthDays - firstDOW + 1 + i; const m = calMonth === 0 ? 11 : calMonth - 1; const y = calMonth === 0 ? calYear - 1 : calYear; cells.push({ day: d, kind: 'prev', key: y + '-' + String(m + 1).padStart(2, '0') + '-' + String(d).padStart(2, '0') }); } for (let d = 1; d <= daysInMonth; d++) { cells.push({ day: d, kind: 'cur', key: calYear + '-' + String(calMonth + 1).padStart(2, '0') + '-' + String(d).padStart(2, '0') }); } const rest = 42 - cells.length; for (let d = 1; d <= rest; d++) { const m = calMonth === 11 ? 0 : calMonth + 1; const y = calMonth === 11 ? calYear + 1 : calYear; cells.push({ day: d, kind: 'next', key: y + '-' + String(m + 1).padStart(2, '0') + '-' + String(d).padStart(2, '0') }); } // Extract batch info from an event const getBatches = (ev: Event): { label: string; time: string }[] => { const r: { label: string; time: string }[] = []; if (ev.schedule && ev.schedule.length > 0) { // Group by day_idx const dayMap = new Map(); ev.schedule.forEach(s => { if (!dayMap.has(s.day_idx)) dayMap.set(s.day_idx, []); dayMap.get(s.day_idx)!.push(s); }); const numDays = dayMap.size; dayMap.forEach((slots, dayIdx) => { slots!.forEach(slot => { const label = numDays > 1 ? `Day ${dayIdx} · Batch ${slot.batch_idx}` : `Batch ${slot.batch_idx}`; r.push({ label, time: `${slot.start_time} → ${slot.end_time}` }); }); }); } if (r.length === 0) r.push({ label: 'Session', time: '—' }); return r; }; const chipBg = (s?: string) => s === 'APPROVED' ? 'bg-emerald-100 border-emerald-300 text-emerald-900' : (s === 'PENDING' || s === 'PENDING_HOD' || s === 'PENDING_ADMIN') ? 'bg-amber-100 border-amber-300 text-amber-900' : 'bg-rose-100 border-rose-300 text-rose-900'; const dotCls = (s?: string) => s === 'APPROVED' ? 'bg-emerald-500' : (s === 'PENDING' || s === 'PENDING_HOD' || s === 'PENDING_ADMIN') ? 'bg-amber-400 animate-pulse' : 'bg-rose-500'; const txtCls = (s?: string) => s === 'APPROVED' ? 'text-emerald-600' : (s === 'PENDING' || s === 'PENDING_HOD' || s === 'PENDING_ADMIN') ? 'text-amber-600' : 'text-rose-600'; return (
{/* Header */}

Track Venue

{selectedVenue ? `Monthly Schedule — ${selectedVenue}` : 'Select a venue to view its full calendar'}

{selectedVenue && ( )}
{!selectedVenue ? ( venues.length === 0 ? (

No Venues Found

No events with venue data added yet.

) : (
{venues.map(venue => { const va = events.filter(e => e.location === venue); const ap = va.filter(e => e.verificationStatus === 'APPROVED').length; const pe = va.filter(e => e.verificationStatus === 'PENDING').length; return ( ); })}
) ) : ( <> {/* Venue info + legend */}

{selectedVenue}

{venueEvents.length} event{venueEvents.length !== 1 ? 's' : ''} at this venue

Approved
Pending
Rejected
{/* Calendar Card */}
{/* Month nav header */}

{MONTH_NAMES[calMonth]}

{calYear}

{/* Day-of-week header row */}
{['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].map(d => (
{d} {d.slice(0, 3)}
))}
{/* 42-cell grid */}
{cells.map((cell, idx) => { const isCur = cell.kind === 'cur'; const cellEvs = isCur ? (eventsByDate[cell.key] || []) : []; const hasEvs = cellEvs.length > 0; const isTod = isCur && cell.day === today.getDate() && calMonth === today.getMonth() && calYear === today.getFullYear(); return (
{/* Date number */}
{cell.day} {hasEvs && {cellEvs.length} ev}
{/* Event chips */}
{cellEvs.map((ev, ei) => { const batches = getBatches(ev); return (

{ev.title}

{batches.map((b, bi) => (
{b.label} {b.time}
))}
); })}
); })}
{/* Detail table */} {venueEvents.length > 0 && (

All Events at {selectedVenue}

{venueEvents.length} Total
{['#', 'Event', 'Day', 'Date', 'Batches & Timings', 'Duration', 'Coordinator', 'Status'].map(h => ( ))} {venueEvents.map((ev, idx) => { let dayName = '—'; try { const d = new Date(ev.date); if (!isNaN(d.getTime())) dayName = DAY_NAMES[d.getDay()]; } catch { /* skip */ } const batches = getBatches(ev); return ( ); })}
{h}
{idx + 1}

{ev.title}

{ev.category}

{dayName} {ev.date || '—'} {batches.length > 0 ? (
{batches.map((b, bi) => (
{b.label} {b.time}
))}
) : ( )}
{ev.durationDays || 1} Day{(ev.durationDays || 1) > 1 ? 's' : ''} {ev.coordinator || '—'}
{ev.verificationStatus}
)} )}
); }; return ; } default: return null; } }; return (
{/* Background Layer */}
{renderView()}
{toastMsg && }
); }; export default FacultyDashboard;