893 lines
58 KiB
TypeScript
893 lines
58 KiB
TypeScript
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<string, string>;
|
|
onApproveCertificate: (regId: string) => Promise<void>;
|
|
localRegistrations?: any[];
|
|
onViewStudentHub?: () => void;
|
|
currentUserName?: string;
|
|
currentUserDept?: string;
|
|
currentUserEmail?: string;
|
|
onUpdateEvent: (event: Event) => void;
|
|
specialEvents?: any[];
|
|
onUpdateSpecialEvent?: (se: any) => Promise<void>;
|
|
}
|
|
|
|
// 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(
|
|
<div className="fixed bottom-10 left-1/2 -translate-x-1/2 z-[10000] bg-emerald-600 text-white px-8 py-4 rounded-2xl shadow-2xl font-black text-xs uppercase tracking-widest flex items-center gap-3 animate-in slide-in-from-bottom-10">
|
|
<i className="fas fa-check-circle"></i>
|
|
{message}
|
|
</div>,
|
|
document.body
|
|
);
|
|
};
|
|
|
|
const FacultyDashboard: React.FC<FacultyDashboardProps> = ({
|
|
onLogout,
|
|
events,
|
|
announcements,
|
|
studentDocuments,
|
|
onApproveCertificate,
|
|
localRegistrations = [],
|
|
onViewStudentHub,
|
|
currentUserName,
|
|
currentUserDept,
|
|
currentUserEmail,
|
|
onUpdateEvent,
|
|
specialEvents = [],
|
|
onUpdateSpecialEvent
|
|
}) => {
|
|
const [activeView, setActiveView] = useState<FacultyExtendedView>('OVERVIEW');
|
|
const [isLoaded, setIsLoaded] = useState(false);
|
|
const [toastMsg, setToastMsg] = useState<string | null>(null);
|
|
const [processingIds, setProcessingIds] = useState<Set<string>>(new Set());
|
|
const [removedIds, setRemovedIds] = useState<Set<string>>(new Set());
|
|
const [pendingDomains, setPendingDomains] = useState<any[]>([]);
|
|
|
|
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 (
|
|
<div className="animate-in fade-in zoom-in-95 duration-700 h-full flex flex-col items-center justify-center text-center py-20 relative z-10">
|
|
<div className="bg-[#004a99]/10 border border-[#004a99]/20 px-6 py-2 rounded-full mb-8 backdrop-blur-md">
|
|
<span className="text-[10px] font-black text-[#004a99] uppercase tracking-[0.3em]">Institutional Admin Portal</span>
|
|
</div>
|
|
<h1 className="text-7xl font-black text-slate-900 uppercase tracking-tighter mb-6 leading-none">
|
|
ADMIN <span className="text-transparent bg-clip-text bg-gradient-to-r from-[#004a99] to-orange-500">HUB</span>
|
|
</h1>
|
|
<p className="text-slate-600 text-xl font-medium max-w-2xl mb-12 uppercase tracking-widest leading-relaxed">
|
|
Orchestrate Academic Excellence <br />
|
|
<span className="text-sm font-bold text-slate-400">Review student progress, track events, and oversee departmental activities.</span>
|
|
</p>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 w-full max-w-4xl px-6">
|
|
<button
|
|
onClick={() => setActiveView('STATUS_TRACKER')}
|
|
className="px-10 py-6 bg-white border border-slate-200 text-[#004a99] rounded-3xl font-black uppercase text-xs tracking-[0.2em] hover:bg-[#004a99] hover:text-white transition-all shadow-xl shadow-slate-200 hover:-translate-y-1"
|
|
>
|
|
Track Student Progress <i className="fas fa-route ml-2"></i>
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveView('USER_MANAGEMENT')}
|
|
className="px-10 py-6 bg-slate-900 text-white rounded-3xl font-black uppercase text-xs tracking-[0.2em] hover:bg-black transition-all shadow-xl shadow-slate-900/20 hover:-translate-y-1"
|
|
>
|
|
Manage Users <i className="fas fa-users-cog ml-2"></i>
|
|
</button>
|
|
{/* Coordinator Hub navigation explicitly removed for Admin Hub pure view */}
|
|
{onViewStudentHub && (
|
|
<button
|
|
onClick={onViewStudentHub}
|
|
className="px-10 py-6 bg-emerald-600 text-white rounded-3xl font-black uppercase text-xs tracking-[0.2em] hover:bg-emerald-700 transition-all shadow-xl shadow-emerald-600/20 hover:-translate-y-1"
|
|
>
|
|
Student Hub <i className="fas fa-graduation-cap ml-2"></i>
|
|
</button>
|
|
)}
|
|
<button
|
|
onClick={() => setActiveView('VERIFY')}
|
|
className="px-10 py-6 bg-amber-500 text-white rounded-3xl font-black uppercase text-xs tracking-[0.2em] hover:bg-amber-600 transition-all shadow-xl shadow-amber-600/20 hover:-translate-y-1"
|
|
>
|
|
Event Verification <i className="fas fa-shield-check ml-2"></i>
|
|
</button>
|
|
<button
|
|
onClick={() => setActiveView('TRACK_VENUE')}
|
|
className="px-10 py-6 bg-indigo-600 text-white rounded-3xl font-black uppercase text-xs tracking-[0.2em] hover:bg-indigo-700 transition-all shadow-xl shadow-indigo-600/20 hover:-translate-y-1"
|
|
>
|
|
Track Venue <i className="fas fa-map-marker-alt ml-2"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
);
|
|
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 (
|
|
<div className="animate-in fade-in slide-in-from-bottom-10 duration-500 h-full flex flex-col">
|
|
<div className="mb-10 flex items-end justify-between">
|
|
<div>
|
|
<h2 className="text-4xl font-black text-slate-900 tracking-tighter uppercase mb-2">Verification Queue</h2>
|
|
<p className="text-amber-600 font-bold text-[10px] tracking-[0.3em] uppercase flex items-center gap-2">
|
|
<span className="w-2 h-2 bg-amber-500 rounded-full animate-pulse"></span>
|
|
{(pendingEvents.length + pendingSpecialEvents.length + pendingDomains.filter((d: any) => !removedIds.has(d.id)).length)} Submissions Awaiting Audit
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8 pb-20">
|
|
{/* Standard Events */}
|
|
{pendingEvents.map((event) => (
|
|
<div key={event.id} className="bg-white border border-slate-200 rounded-[2.5rem] overflow-hidden group shadow-sm flex flex-col">
|
|
<div className="h-44 relative overflow-hidden bg-slate-100">
|
|
<img src={event.image} alt={event.title} className="w-full h-full object-cover" />
|
|
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent"></div>
|
|
<div className="absolute top-4 right-4 bg-white/90 backdrop-blur-md px-4 py-1.5 rounded-full text-[9px] font-black uppercase tracking-widest text-[#004a99]">Normal Event</div>
|
|
<div className="absolute bottom-4 left-4">
|
|
<span className={`px-3 py-1 rounded-full text-[8px] font-black uppercase tracking-widest ${event.pricingType === 'PAID' ? 'bg-rose-500 text-white' : 'bg-emerald-500 text-white'}`}>{event.pricingType}</span>
|
|
</div>
|
|
</div>
|
|
<div className="p-6 flex-1 flex flex-col gap-0">
|
|
<h3 className="text-lg font-black text-slate-900 uppercase tracking-tight mb-1">{event.title}</h3>
|
|
<p className="text-[9px] text-[#004a99] font-black uppercase tracking-widest mb-4">Normal Event · {event.category}</p>
|
|
|
|
{/* Full Details Line-by-Line */}
|
|
<div className="space-y-2 mb-5 text-[10px] font-bold text-slate-600 uppercase tracking-wide">
|
|
<div className="flex items-start gap-2"><i className="fas fa-user-tie text-[#004a99] w-4 mt-0.5"></i><span><span className="text-slate-400">Coordinator:</span> {event.coordinator || '—'}</span></div>
|
|
<div className="flex items-start gap-2"><i className="fas fa-building text-[#004a99] w-4 mt-0.5"></i><span><span className="text-slate-400">Dept:</span> {event.conducting_dept || '—'}</span></div>
|
|
<div className="flex items-start gap-2"><i className="fas fa-users text-[#004a99] w-4 mt-0.5"></i><span><span className="text-slate-400">Club/Entity:</span> {event.club || '—'}</span></div>
|
|
<div className="flex items-start gap-2"><i className="far fa-calendar text-orange-500 w-4 mt-0.5"></i><span><span className="text-slate-400">Date:</span> {event.date || '—'}</span></div>
|
|
<div className="flex items-start gap-2"><i className="fas fa-clock text-orange-500 w-4 mt-0.5"></i><span><span className="text-slate-400">Time:</span> {event.schedule?.[0]?.start_time || '—'}</span></div>
|
|
<div className="flex items-start gap-2"><i className="fas fa-location-dot text-orange-500 w-4 mt-0.5"></i><span><span className="text-slate-400">Venue:</span> {event.location || '—'}</span></div>
|
|
<div className="flex items-start gap-2"><i className="fas fa-calendar-week text-slate-400 w-4 mt-0.5"></i><span><span className="text-slate-400">Duration:</span> {event.durationDays || 1} Day{(event.durationDays || 1) > 1 ? 's' : ''}</span></div>
|
|
<div className="flex items-start gap-2"><i className="fas fa-layer-group text-slate-400 w-4 mt-0.5"></i><span><span className="text-slate-400">Domain:</span> {event.domain || '—'}</span></div>
|
|
<div className="flex items-start gap-2"><i className="fas fa-users text-slate-400 w-4 mt-0.5"></i><span><span className="text-slate-400">Capacity:</span> {event.maxParticipants ? `${event.maxParticipants} seats` : 'Unlimited'}</span></div>
|
|
<div className="flex items-start gap-2"><i className="fas fa-globe text-slate-400 w-4 mt-0.5"></i><span><span className="text-slate-400">Access:</span> {event.participantType || 'INTERNAL'}</span></div>
|
|
{event.isTeamEvent && <div className="flex items-start gap-2"><i className="fas fa-people-arrows text-slate-400 w-4 mt-0.5"></i><span><span className="text-slate-400">Team Size:</span> {event.teamSizeLimit || '—'} ({event.teamComposition})</span></div>}
|
|
{event.registrationDeadline && <div className="flex items-start gap-2"><i className="fas fa-hourglass-end text-rose-400 w-4 mt-0.5"></i><span><span className="text-slate-400">Deadline:</span> {new Date(event.registrationDeadline).toLocaleString()}</span></div>}
|
|
{event.event_summary && (
|
|
<div className="flex items-start gap-2 pt-1">
|
|
<i className="fas fa-align-left text-slate-400 w-4 mt-0.5"></i>
|
|
<span className="italic text-slate-500 font-medium normal-case tracking-normal line-clamp-3">{event.event_summary}</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Financial Projection Section */}
|
|
<div className="bg-slate-50 border border-slate-100 rounded-2xl p-4 mb-5 space-y-2">
|
|
<p className="text-[8px] font-black text-slate-400 uppercase tracking-widest mb-2">Financial Projections</p>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-[9px] font-bold text-slate-500 uppercase">Refreshment</span>
|
|
<span className="text-[10px] font-black text-slate-800">₹{event.refreshment_expense || 0}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-[9px] font-bold text-slate-500 uppercase">Transportation</span>
|
|
<span className="text-[10px] font-black text-slate-800">₹{event.transportation_expense || 0}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-[9px] font-bold text-slate-500 uppercase">Session Coverage</span>
|
|
<span className="text-[10px] font-black text-slate-800">₹{(event as any).session_coverage_fee || 0}</span>
|
|
</div>
|
|
<div className="flex items-center justify-between pt-2 border-t border-slate-200">
|
|
<span className="text-[9px] font-black text-emerald-600 uppercase">Total Projected</span>
|
|
<span className="text-xs font-black text-emerald-700">₹{event.total_expense || 0}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex gap-3 mt-auto">
|
|
<button
|
|
onClick={() => generateEventDetailsPDF(event)}
|
|
className="w-12 py-4 bg-blue-50 text-blue-600 rounded-2xl flex items-center justify-center hover:bg-blue-100 transition-all active:scale-95 shadow-sm border border-blue-100 flex-shrink-0"
|
|
title="Download Event Report"
|
|
>
|
|
<i className="fas fa-file-pdf"></i>
|
|
</button>
|
|
<button
|
|
disabled={processingIds.has(event.id)}
|
|
onClick={() => handleVerifyAction(event, 'APPROVED', false)}
|
|
className="flex-1 py-4 bg-emerald-600 text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-emerald-700 transition-all active:scale-95 shadow-lg shadow-emerald-600/20 disabled:opacity-50"
|
|
>
|
|
{processingIds.has(event.id) ? <i className="fas fa-circle-notch fa-spin"></i> : <><i className="fas fa-check mr-1"></i> Approve</>}
|
|
</button>
|
|
<button
|
|
disabled={processingIds.has(event.id)}
|
|
onClick={() => handleVerifyAction(event, 'REJECTED', false)}
|
|
className="flex-1 py-4 bg-rose-50 text-rose-600 border border-rose-100 rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-rose-600 hover:text-white transition-all active:scale-95 disabled:opacity-50"
|
|
>
|
|
{processingIds.has(event.id) ? <i className="fas fa-circle-notch fa-spin"></i> : <><i className="fas fa-times mr-1"></i> Reject</>}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* Special Events */}
|
|
{pendingSpecialEvents.map((event) => (
|
|
<div key={event.id} className="bg-slate-900 text-white rounded-[2.5rem] overflow-hidden group shadow-xl flex flex-col border border-white/5">
|
|
<div className="h-48 relative overflow-hidden bg-slate-800 flex items-center justify-center">
|
|
<i className="fas fa-star text-4xl text-orange-500 opacity-20"></i>
|
|
<div className="absolute top-4 right-4 bg-orange-500 text-white px-4 py-1.5 rounded-full text-[9px] font-black uppercase tracking-widest">Special Event</div>
|
|
</div>
|
|
<div className="p-8 flex-1 flex flex-col">
|
|
<h3 className="text-xl font-black text-white uppercase tracking-tight mb-2 line-clamp-1">{event.title}</h3>
|
|
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mb-6">External Link Portal</p>
|
|
|
|
<div className="space-y-3 mb-8 flex-1">
|
|
<div className="flex items-center gap-3 text-slate-500 font-bold text-[9px] uppercase tracking-widest">
|
|
<i className="fas fa-link text-orange-500"></i>
|
|
<span className="truncate">{event.link}</span>
|
|
</div>
|
|
<p className="text-[10px] text-slate-300 italic line-clamp-2">{event.description}</p>
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<button
|
|
disabled={processingIds.has(event.id)}
|
|
onClick={() => handleVerifyAction(event, 'APPROVED', true)}
|
|
className="flex-1 py-4 bg-orange-500 text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-orange-600 transition-all active:scale-95 shadow-lg shadow-orange-500/20 disabled:opacity-50 group/btn"
|
|
>
|
|
{processingIds.has(event.id) ? <i className="fas fa-circle-notch fa-spin"></i> : "Approve"}
|
|
</button>
|
|
<button
|
|
disabled={processingIds.has(event.id)}
|
|
onClick={() => handleVerifyAction(event, 'REJECTED', true)}
|
|
className="flex-1 py-4 bg-white/5 text-white border border-white/10 rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-white/10 transition-all active:scale-95 disabled:opacity-50"
|
|
>
|
|
{processingIds.has(event.id) ? <i className="fas fa-circle-notch fa-spin"></i> : "Reject"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{/* Pending Domains */}
|
|
{pendingDomains.filter((d: any) => !removedIds.has(d.id)).map((domain) => (
|
|
<div key={domain.id} className="bg-white border border-emerald-200 rounded-[2.5rem] overflow-hidden group shadow-sm flex flex-col">
|
|
<div className="h-48 relative overflow-hidden bg-slate-100 flex items-center justify-center italic">
|
|
<img src={domain.image} alt={domain.name} className="w-full h-full object-cover" />
|
|
<div className="absolute top-4 right-4 bg-emerald-500 text-white px-4 py-1.5 rounded-full text-[9px] font-black uppercase tracking-widest">New Domain</div>
|
|
</div>
|
|
<div className="p-8 flex-1 flex flex-col">
|
|
<h3 className="text-xl font-black text-slate-900 uppercase tracking-tight mb-2 line-clamp-1">{domain.name}</h3>
|
|
<p className="text-[10px] text-emerald-600 font-bold uppercase tracking-widest mb-6">Category: {domain.category}</p>
|
|
|
|
<div className="space-y-3 mb-8 flex-1">
|
|
<p className="text-[10px] text-slate-500 font-bold line-clamp-3">{domain.description || 'No description provided'}</p>
|
|
</div>
|
|
|
|
<div className="flex gap-3">
|
|
<button
|
|
disabled={processingIds.has(domain.id)}
|
|
onClick={async () => {
|
|
if (processingIds.has(domain.id)) return;
|
|
setProcessingIds(prev => new Set(prev).add(domain.id));
|
|
try {
|
|
await supabase.from('domains').update({ status: 'APPROVED' }).eq('id', domain.id);
|
|
setRemovedIds(prev => new Set(prev).add(domain.id));
|
|
handleToast(`Domain "${domain.name}" Approved successfully!`);
|
|
} catch (err) {
|
|
handleToast(`Failed to update "${domain.name}". Please try again.`);
|
|
} finally {
|
|
setProcessingIds(prev => { const next = new Set(prev); next.delete(domain.id); return next; });
|
|
}
|
|
}}
|
|
className="flex-1 py-4 bg-emerald-600 text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-emerald-700 transition-all active:scale-95 shadow-lg shadow-emerald-600/20 disabled:opacity-50 group/btn"
|
|
>
|
|
{processingIds.has(domain.id) ? <i className="fas fa-circle-notch fa-spin"></i> : "Approve"}
|
|
</button>
|
|
<button
|
|
disabled={processingIds.has(domain.id)}
|
|
onClick={async () => {
|
|
if (processingIds.has(domain.id)) return;
|
|
setProcessingIds(prev => new Set(prev).add(domain.id));
|
|
try {
|
|
await supabase.from('domains').update({ status: 'REJECTED' }).eq('id', domain.id);
|
|
setRemovedIds(prev => new Set(prev).add(domain.id));
|
|
handleToast(`Domain "${domain.name}" Rejected!`);
|
|
} catch (err) {
|
|
handleToast(`Failed to update "${domain.name}". Please try again.`);
|
|
} finally {
|
|
setProcessingIds(prev => { const next = new Set(prev); next.delete(domain.id); return next; });
|
|
}
|
|
}}
|
|
className="flex-1 py-4 bg-rose-50 text-rose-600 border border-rose-100 rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-rose-600 hover:text-white transition-all active:scale-95 disabled:opacity-50"
|
|
>
|
|
{processingIds.has(domain.id) ? <i className="fas fa-circle-notch fa-spin"></i> : "Reject"}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{pendingEvents.length === 0 && pendingSpecialEvents.length === 0 && pendingDomains.filter((d: any) => !removedIds.has(d.id)).length === 0 && (
|
|
<div className="col-span-full py-40 flex flex-col items-center justify-center bg-white/50 border border-dashed border-slate-200 rounded-[3rem] text-center px-10">
|
|
<div className="w-20 h-20 bg-emerald-50 rounded-full flex items-center justify-center mb-6">
|
|
<i className="fas fa-check-double text-2xl text-emerald-500"></i>
|
|
</div>
|
|
<h4 className="text-slate-900 font-black uppercase tracking-[0.3em] text-xs mb-3">Audit Complete</h4>
|
|
<p className="text-slate-500 font-bold uppercase tracking-[0.2em] text-[9px] max-w-sm">No submissions remaining in the verification pipeline.</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* ── 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 ? (
|
|
<div className="mt-16">
|
|
<div className="flex items-center gap-4 mb-8">
|
|
<div className="w-8 h-8 bg-emerald-100 rounded-xl flex items-center justify-center">
|
|
<i className="fas fa-clipboard-check text-emerald-600 text-sm"></i>
|
|
</div>
|
|
<div>
|
|
<h3 className="text-xl font-black text-slate-900 tracking-tighter uppercase">Verified & Scheduled Records</h3>
|
|
<p className="text-[10px] font-bold text-emerald-600 uppercase tracking-widest">{totalVerified} approved event{totalVerified !== 1 ? 's' : ''} on record</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-white border border-slate-200 rounded-[2rem] overflow-hidden shadow-sm">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="bg-slate-50 border-b border-slate-100">
|
|
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">#</th>
|
|
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Event</th>
|
|
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Type</th>
|
|
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Coordinator</th>
|
|
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Date</th>
|
|
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Venue</th>
|
|
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Category</th>
|
|
<th className="text-left px-6 py-4 text-[9px] font-black text-slate-400 uppercase tracking-[0.25em]">Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-50">
|
|
{verifiedEvents.map((ev, idx) => (
|
|
<tr key={ev.id} className="hover:bg-slate-50/70 transition-colors group">
|
|
<td className="px-6 py-4 text-[10px] font-black text-slate-300">{idx + 1}</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-xl overflow-hidden bg-slate-100 flex-shrink-0">
|
|
{ev.image ? (
|
|
<img src={ev.image} alt={ev.title} className="w-full h-full object-cover" />
|
|
) : (
|
|
<div className="w-full h-full flex items-center justify-center"><i className="fas fa-calendar text-slate-300 text-xs"></i></div>
|
|
)}
|
|
</div>
|
|
<span className="text-[11px] font-black text-slate-900 uppercase tracking-tight line-clamp-1 max-w-[160px]">{ev.title}</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4"><span className="px-3 py-1 bg-blue-50 text-[#004a99] text-[8px] font-black uppercase tracking-widest rounded-full border border-blue-100">Normal</span></td>
|
|
<td className="px-6 py-4 text-[10px] font-bold text-slate-600 uppercase">{ev.coordinator || '—'}</td>
|
|
<td className="px-6 py-4 text-[10px] font-bold text-slate-500 uppercase whitespace-nowrap">{ev.date || '—'}</td>
|
|
<td className="px-6 py-4 text-[10px] font-bold text-slate-500 uppercase line-clamp-1 max-w-[120px]">{ev.location || '—'}</td>
|
|
<td className="px-6 py-4">
|
|
<span className="px-3 py-1 bg-slate-50 text-slate-600 text-[8px] font-black uppercase tracking-widest rounded-full border border-slate-100">{ev.category || 'General'}</span>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-2 h-2 rounded-full bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.6)]"></span>
|
|
<span className="text-[9px] font-black text-emerald-600 uppercase tracking-widest">{ev.status || 'Scheduled'}</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
{verifiedSpecial.map((ev, idx) => (
|
|
<tr key={ev.id} className="hover:bg-orange-50/30 transition-colors group">
|
|
<td className="px-6 py-4 text-[10px] font-black text-slate-300">{verifiedEvents.length + idx + 1}</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-8 h-8 rounded-xl bg-orange-100 flex items-center justify-center flex-shrink-0">
|
|
<i className="fas fa-star text-orange-500 text-xs"></i>
|
|
</div>
|
|
<span className="text-[11px] font-black text-slate-900 uppercase tracking-tight line-clamp-1 max-w-[160px]">{ev.title}</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4"><span className="px-3 py-1 bg-orange-50 text-orange-600 text-[8px] font-black uppercase tracking-widest rounded-full border border-orange-100">Special</span></td>
|
|
<td className="px-6 py-4 text-[10px] font-bold text-slate-600 uppercase">{ev.coordinator || '—'}</td>
|
|
<td className="px-6 py-4 text-[10px] font-bold text-slate-500 uppercase whitespace-nowrap">{ev.date || '—'}</td>
|
|
<td className="px-6 py-4">
|
|
<a href={ev.link} target="_blank" rel="noopener noreferrer" className="text-[10px] font-bold text-[#004a99] hover:underline truncate max-w-[100px] block">{ev.link || '—'}</a>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<span className="px-3 py-1 bg-slate-50 text-slate-600 text-[8px] font-black uppercase tracking-widest rounded-full border border-slate-100">Special</span>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center gap-2">
|
|
<span className="w-2 h-2 rounded-full bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.6)]"></span>
|
|
<span className="text-[9px] font-black text-emerald-600 uppercase tracking-widest">Approved</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : null;
|
|
})()}
|
|
</div>
|
|
);
|
|
}
|
|
case 'STATUS_TRACKER':
|
|
return (
|
|
<AdminStatusTrackerView
|
|
events={events}
|
|
onShowToast={handleToast}
|
|
studentDocuments={studentDocuments}
|
|
onApproveCertificate={onApproveCertificate}
|
|
localRegistrations={localRegistrations}
|
|
isAdmin={true}
|
|
/>
|
|
);
|
|
case 'USER_MANAGEMENT':
|
|
return <UserManagementView />;
|
|
case 'PARTICIPANTS':
|
|
return <FacultyParticipantsView events={events} localRegistrations={localRegistrations} />;
|
|
case 'NOTIFICATIONS':
|
|
return <FacultyNotifications />;
|
|
case 'PROFILE':
|
|
return <FacultyProfileView onLogout={onLogout} name={currentUserName} dept={currentUserDept} email={currentUserEmail} />;
|
|
case 'TRACK_VENUE': {
|
|
const TrackVenueView = () => {
|
|
const [selectedVenue, setSelectedVenue] = React.useState<string | null>(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<string, typeof events> = {};
|
|
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<number, typeof ev.schedule>();
|
|
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 (
|
|
<div className="animate-in fade-in slide-in-from-bottom-10 duration-500 pb-20">
|
|
{/* Header */}
|
|
<div className="mb-8 flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-4xl font-black text-slate-900 tracking-tighter uppercase mb-1">Track Venue</h2>
|
|
<p className="text-indigo-600 font-bold text-[10px] tracking-[0.3em] uppercase flex items-center gap-2">
|
|
<span className="w-2 h-2 bg-indigo-500 rounded-full"></span>
|
|
{selectedVenue ? `Monthly Schedule — ${selectedVenue}` : 'Select a venue to view its full calendar'}
|
|
</p>
|
|
</div>
|
|
{selectedVenue && (
|
|
<button onClick={() => setSelectedVenue(null)}
|
|
className="flex items-center gap-2 text-[10px] font-black text-slate-500 hover:text-indigo-600 uppercase tracking-widest border border-slate-200 bg-white px-5 py-2.5 rounded-xl hover:border-indigo-300 hover:shadow-md transition-all">
|
|
<i className="fas fa-arrow-left text-[8px]"></i> All Venues
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{!selectedVenue ? (
|
|
venues.length === 0 ? (
|
|
<div className="py-40 flex flex-col items-center justify-center bg-white/50 border border-dashed border-slate-200 rounded-[3rem] text-center px-10">
|
|
<div className="w-20 h-20 bg-indigo-50 rounded-full flex items-center justify-center mb-6">
|
|
<i className="fas fa-map-marker-alt text-2xl text-indigo-400"></i>
|
|
</div>
|
|
<h4 className="text-slate-900 font-black uppercase tracking-[0.3em] text-xs mb-3">No Venues Found</h4>
|
|
<p className="text-slate-500 font-bold uppercase tracking-[0.2em] text-[9px] max-w-sm">No events with venue data added yet.</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
|
{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 (
|
|
<button key={venue} onClick={() => setSelectedVenue(venue)}
|
|
className="group bg-white border border-slate-200 rounded-[2rem] p-8 text-left hover:border-indigo-400 hover:shadow-xl hover:shadow-indigo-500/10 transition-all duration-300 hover:-translate-y-1 active:scale-95">
|
|
<div className="w-12 h-12 bg-indigo-50 rounded-2xl flex items-center justify-center mb-5 group-hover:bg-indigo-100 transition-colors">
|
|
<i className="fas fa-map-marker-alt text-indigo-500"></i>
|
|
</div>
|
|
<h3 className="text-lg font-black text-slate-900 uppercase tracking-tight mb-1 group-hover:text-indigo-600">{venue}</h3>
|
|
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mb-4">{va.length} Event{va.length !== 1 ? 's' : ''} Total</p>
|
|
<div className="flex gap-2 flex-wrap">
|
|
{ap > 0 && <span className="px-2 py-0.5 bg-emerald-50 border border-emerald-200 text-emerald-700 text-[8px] font-black rounded-full uppercase">{ap} Approved</span>}
|
|
{pe > 0 && <span className="px-2 py-0.5 bg-amber-50 border border-amber-200 text-amber-700 text-[8px] font-black rounded-full uppercase">{pe} Pending</span>}
|
|
</div>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
)
|
|
) : (
|
|
<>
|
|
{/* Venue info + legend */}
|
|
<div className="bg-white border border-slate-200 rounded-[2rem] p-5 mb-6 flex flex-wrap items-center gap-6">
|
|
<div className="flex items-center gap-4">
|
|
<div className="w-11 h-11 bg-indigo-100 rounded-2xl flex items-center justify-center">
|
|
<i className="fas fa-map-marker-alt text-indigo-600 text-sm"></i>
|
|
</div>
|
|
<div>
|
|
<h3 className="text-lg font-black text-slate-900 uppercase tracking-tight leading-none">{selectedVenue}</h3>
|
|
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mt-0.5">{venueEvents.length} event{venueEvents.length !== 1 ? 's' : ''} at this venue</p>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-5 ml-auto flex-wrap">
|
|
<div className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-emerald-500"></span><span className="text-[9px] font-black uppercase tracking-widest text-slate-500">Approved</span></div>
|
|
<div className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-amber-400 animate-pulse"></span><span className="text-[9px] font-black uppercase tracking-widest text-slate-500">Pending</span></div>
|
|
<div className="flex items-center gap-1.5"><span className="w-2.5 h-2.5 rounded-full bg-rose-400"></span><span className="text-[9px] font-black uppercase tracking-widest text-slate-500">Rejected</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Calendar Card */}
|
|
<div className="bg-white border border-slate-200 rounded-[2rem] overflow-hidden shadow-sm mb-8">
|
|
{/* Month nav header */}
|
|
<div className="flex items-center justify-between px-8 py-5 bg-gradient-to-r from-indigo-600 to-indigo-500">
|
|
<button onClick={prevMonthFn}
|
|
className="w-9 h-9 rounded-xl bg-white/15 border border-white/20 flex items-center justify-center hover:bg-white/25 transition-all">
|
|
<i className="fas fa-chevron-left text-[10px] text-white"></i>
|
|
</button>
|
|
<div className="text-center select-none">
|
|
<h3 className="text-3xl font-black text-white uppercase tracking-tight leading-none">{MONTH_NAMES[calMonth]}</h3>
|
|
<p className="text-[14px] font-bold text-white/70 uppercase tracking-[0.5em] mt-1">{calYear}</p>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button onClick={goToday}
|
|
className="px-4 py-2 rounded-xl bg-white text-indigo-600 text-[9px] font-black uppercase tracking-widest hover:bg-indigo-50 transition-all shadow-sm">
|
|
Today
|
|
</button>
|
|
<button onClick={nextMonthFn}
|
|
className="w-9 h-9 rounded-xl bg-white/15 border border-white/20 flex items-center justify-center hover:bg-white/25 transition-all">
|
|
<i className="fas fa-chevron-right text-[10px] text-white"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Day-of-week header row */}
|
|
<div className="grid grid-cols-7 border-b border-slate-100 bg-slate-50">
|
|
{['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'].map(d => (
|
|
<div key={d} className="py-3 text-center border-r border-slate-100 last:border-r-0">
|
|
<span className="hidden lg:block text-[9px] font-black uppercase tracking-[0.15em] text-slate-400">{d}</span>
|
|
<span className="lg:hidden text-[9px] font-black uppercase tracking-[0.15em] text-slate-400">{d.slice(0, 3)}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
|
|
{/* 42-cell grid */}
|
|
<div className="grid grid-cols-7" style={{ gridAutoRows: 'minmax(115px, auto)' }}>
|
|
{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 (
|
|
<div key={idx} className={[
|
|
'border-b border-r border-slate-100 flex flex-col',
|
|
!isCur ? 'bg-slate-50/70' : hasEvs ? 'bg-indigo-50/25' : 'bg-white hover:bg-slate-50/40'
|
|
].join(' ')}>
|
|
{/* Date number */}
|
|
<div className="flex items-center justify-between px-2 pt-2 pb-1">
|
|
<span className={[
|
|
'w-7 h-7 flex items-center justify-center rounded-full text-[11px] font-black select-none transition-colors',
|
|
isTod ? 'bg-indigo-600 text-white shadow-md shadow-indigo-400/40' :
|
|
!isCur ? 'text-slate-300' :
|
|
hasEvs ? 'text-indigo-700' :
|
|
'text-slate-500'
|
|
].join(' ')}>
|
|
{cell.day}
|
|
</span>
|
|
{hasEvs && <span className="text-[7px] font-black text-indigo-400 uppercase">{cellEvs.length} ev</span>}
|
|
</div>
|
|
{/* Event chips */}
|
|
<div className="px-1.5 pb-1.5 space-y-1 overflow-hidden flex-1 min-w-0">
|
|
{cellEvs.map((ev, ei) => {
|
|
const batches = getBatches(ev);
|
|
return (
|
|
<div key={ev.id + String(ei)} className={`rounded-lg border px-2 pt-1 pb-1.5 min-w-0 ${chipBg(ev.verificationStatus)}`}>
|
|
<p className="text-[8px] font-black uppercase tracking-wide leading-tight truncate">{ev.title}</p>
|
|
{batches.map((b, bi) => (
|
|
<div key={bi} className="flex items-center gap-1 mt-0.5 flex-wrap">
|
|
<span className="text-[7px] font-black opacity-80 uppercase bg-white/40 px-1.5 py-0.5 rounded-full leading-none whitespace-nowrap">{b.label}</span>
|
|
<span className="text-[7px] font-bold opacity-70 leading-none whitespace-nowrap">{b.time}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Detail table */}
|
|
{venueEvents.length > 0 && (
|
|
<div className="bg-white border border-slate-200 rounded-[2rem] overflow-hidden shadow-sm">
|
|
<div className="px-8 py-5 border-b border-slate-100 flex items-center gap-3 bg-slate-50">
|
|
<i className="fas fa-list-ul text-indigo-500"></i>
|
|
<h4 className="text-[11px] font-black text-slate-900 uppercase tracking-widest">All Events at {selectedVenue}</h4>
|
|
<span className="ml-auto px-3 py-1 bg-indigo-100 text-indigo-700 text-[8px] font-black rounded-full uppercase">{venueEvents.length} Total</span>
|
|
</div>
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full">
|
|
<thead>
|
|
<tr className="bg-slate-50/60 border-b border-slate-100">
|
|
{['#', 'Event', 'Day', 'Date', 'Batches & Timings', 'Duration', 'Coordinator', 'Status'].map(h => (
|
|
<th key={h} className="text-left px-5 py-3 text-[8px] font-black text-slate-400 uppercase tracking-widest whitespace-nowrap">{h}</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y divide-slate-50">
|
|
{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 (
|
|
<tr key={ev.id} className="hover:bg-indigo-50/20 transition-colors">
|
|
<td className="px-5 py-4 text-[10px] font-black text-slate-300">{idx + 1}</td>
|
|
<td className="px-5 py-4 text-[11px] font-black text-slate-900 uppercase max-w-[140px]">
|
|
<p className="truncate">{ev.title}</p>
|
|
<p className="text-[8px] font-bold text-slate-400 normal-case">{ev.category}</p>
|
|
</td>
|
|
<td className="px-5 py-4 text-[10px] font-bold text-indigo-600 uppercase whitespace-nowrap">{dayName}</td>
|
|
<td className="px-5 py-4 text-[10px] font-bold text-slate-500 whitespace-nowrap">{ev.date || '—'}</td>
|
|
<td className="px-5 py-4">
|
|
{batches.length > 0 ? (
|
|
<div className="space-y-1.5">
|
|
{batches.map((b, bi) => (
|
|
<div key={bi} className="flex items-center gap-2 flex-wrap">
|
|
<span className="px-2 py-0.5 bg-indigo-50 border border-indigo-200 text-indigo-700 text-[7px] font-black rounded-full uppercase whitespace-nowrap">{b.label}</span>
|
|
<span className="text-[9px] font-bold text-slate-600 whitespace-nowrap">{b.time}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<span className="text-[9px] text-slate-300 font-bold">—</span>
|
|
)}
|
|
</td>
|
|
<td className="px-5 py-4 text-[10px] font-bold text-slate-500 whitespace-nowrap">{ev.durationDays || 1} Day{(ev.durationDays || 1) > 1 ? 's' : ''}</td>
|
|
<td className="px-5 py-4 text-[10px] font-bold text-slate-600 uppercase">{ev.coordinator || '—'}</td>
|
|
<td className="px-5 py-4">
|
|
<div className="flex items-center gap-1.5">
|
|
<span className={`w-2 h-2 rounded-full ${dotCls(ev.verificationStatus)}`}></span>
|
|
<span className={`text-[8px] font-black uppercase tracking-widest ${txtCls(ev.verificationStatus)}`}>{ev.verificationStatus}</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
);
|
|
})}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
return <TrackVenueView />;
|
|
}
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen bg-[#F3F4F6] text-slate-900 font-inter relative overflow-hidden">
|
|
{/* Background Layer */}
|
|
<div className="fixed inset-0 z-0 overflow-hidden">
|
|
<div
|
|
className="absolute inset-0 bg-cover bg-center"
|
|
style={{ backgroundImage: 'url("https://cache.careers360.mobi/media/presets/720X480/colleges/social-media/media-gallery/3425/2021/6/16/DSC08602.JPG")' }}
|
|
></div>
|
|
<div className="absolute inset-0 bg-white/90 backdrop-blur-md"></div>
|
|
</div>
|
|
|
|
<div className="relative z-10 flex min-h-screen">
|
|
<aside className={`w-24 md:w-64 border-r border-slate-100 bg-white/60 backdrop-blur-2xl flex flex-col transition-all duration-1000 ${isLoaded ? 'translate-x-0' : '-translate-x-full'}`}>
|
|
<div className="p-8 border-b border-slate-100 flex flex-col items-center gap-4">
|
|
<img
|
|
src="https://www.facultyplus.com/wp-content/uploads/2020/11/Rit-logo.png"
|
|
alt="RIT Logo"
|
|
className="h-12 w-auto object-contain transition-all duration-300"
|
|
/>
|
|
<div className="hidden md:flex flex-col items-center leading-none">
|
|
<span className="font-black text-xs tracking-tight text-[#004a99]">ADMIN PORTAL</span>
|
|
<span className="text-[7px] text-slate-400 font-bold uppercase tracking-[0.2em] mt-1 text-center">Management Console</span>
|
|
</div>
|
|
</div>
|
|
|
|
<nav className="flex-1 py-10 px-4 space-y-2">
|
|
{menuItems.map((item) => (
|
|
<button
|
|
key={item.id}
|
|
onClick={() => setActiveView(item.id)}
|
|
className={`w-full flex items-center gap-4 px-6 py-4 rounded-[1.5rem] transition-all group ${activeView === item.id
|
|
? 'bg-[#004a99] text-white shadow-xl shadow-[#004a99]/30'
|
|
: 'text-slate-400 hover:text-[#004a99] hover:bg-white/50'
|
|
}`}
|
|
>
|
|
<i className={`fas ${item.icon} text-lg`}></i>
|
|
<span className="hidden md:block font-black text-[10px] uppercase tracking-widest">{item.label}</span>
|
|
</button>
|
|
))}
|
|
</nav>
|
|
|
|
<div className="p-6 border-t border-slate-100">
|
|
<button
|
|
onClick={onLogout}
|
|
className="w-full flex items-center justify-center gap-4 px-4 py-4 rounded-2xl text-rose-500 hover:bg-rose-50 transition-all font-black text-[10px] uppercase tracking-widest"
|
|
>
|
|
<i className="fas fa-right-from-bracket"></i>
|
|
<span className="hidden md:block">Logout</span>
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
<main className="flex-1 overflow-y-auto custom-scrollbar relative">
|
|
<div className="p-10 min-h-screen">
|
|
{renderView()}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
{toastMsg && <Toast message={toastMsg} onClose={handleCloseToast} />}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default FacultyDashboard; |