Convert backends to Firebase and combine projects
This commit is contained in:
@@ -0,0 +1,639 @@
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { Event } from '../types';
|
||||
import { CATEGORIES, DOMAIN_MAP } from '../constants';
|
||||
import { supabase } from '../supabase';
|
||||
import * as XLSX from 'xlsx-js-style';
|
||||
|
||||
type ParticipantsSubView = 'CATEGORIES' | 'DOMAINS' | 'EVENTS' | 'STUDENTS';
|
||||
interface FacultyParticipantsViewProps {
|
||||
events: Event[];
|
||||
localRegistrations?: any[];
|
||||
currentUserId?: string;
|
||||
}
|
||||
|
||||
const FacultyParticipantsView: React.FC<FacultyParticipantsViewProps> = ({ events, localRegistrations = [], currentUserId }) => {
|
||||
const [currentView, setCurrentView] = useState<ParticipantsSubView>('STUDENTS');
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||
const [selectedDomain, setSelectedDomain] = useState<string | null>(null);
|
||||
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [showEventFilter, setShowEventFilter] = useState(false);
|
||||
const [showSideFilter, setShowSideFilter] = useState(false);
|
||||
const [selectedDepts, setSelectedDepts] = useState<string[]>([]);
|
||||
const [selectedYears, setSelectedYears] = useState<string[]>([]);
|
||||
const [activeFilterTab, setActiveFilterTab] = useState<'DEPT' | 'YEAR'>('DEPT');
|
||||
const [registrations, setRegistrations] = useState<any[]>(localRegistrations);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [selectedStudentIds, setSelectedStudentIds] = useState<Set<string>>(new Set());
|
||||
const [studentTypeFilter, setStudentTypeFilter] = useState<'INTERNAL' | 'EXTERNAL'>('INTERNAL');
|
||||
const [externalUserIds, setExternalUserIds] = useState<Set<string>>(new Set());
|
||||
|
||||
// Fetch registrations directly from Supabase for reliability
|
||||
useEffect(() => {
|
||||
const fetchRegistrations = async () => {
|
||||
setIsLoading(true);
|
||||
let { data, error } = await supabase
|
||||
.from('participants')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) {
|
||||
// Fallback to registrations if participants table doesn't exist yet
|
||||
const fallback = await supabase
|
||||
.from('registrations')
|
||||
.select('*')
|
||||
.order('registered_at', { ascending: false });
|
||||
data = fallback.data;
|
||||
error = fallback.error;
|
||||
}
|
||||
|
||||
if (data && !error) {
|
||||
setRegistrations(data);
|
||||
} else if (localRegistrations.length > 0) {
|
||||
setRegistrations(localRegistrations);
|
||||
}
|
||||
|
||||
const { data: extUsers } = await supabase.from('externalusers').select('id');
|
||||
if (extUsers) {
|
||||
setExternalUserIds(new Set(extUsers.map(u => u.id)));
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
fetchRegistrations();
|
||||
}, []);
|
||||
|
||||
const departments = ['CSBS', 'AIDS', 'AIML', 'ECE', 'VLSI', 'H&S', 'CCE', 'CSE', 'MECH', 'BIO-TECH', 'Information Technology (IT)', 'Electrical & Electronics Engineering (EEE)', 'Civil Engineering', 'Biomedical Engineering', 'Chemical Engineering', 'Aeronautical / Aerospace Engineering', 'Mechatronics Engineering', 'Others'];
|
||||
const years = ['1st Year', '2nd Year', '3rd Year', '4th Year', '5th Year'];
|
||||
|
||||
const handleCategorySelect = (id: string) => {
|
||||
setSelectedCategory(id);
|
||||
setCurrentView('DOMAINS');
|
||||
};
|
||||
|
||||
const handleDomainSelect = (id: string) => {
|
||||
setSelectedDomain(id);
|
||||
setCurrentView('EVENTS');
|
||||
};
|
||||
|
||||
const handleEventSelect = (id: string) => {
|
||||
setSelectedEventId(id);
|
||||
setCurrentView('STUDENTS');
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
if (currentView === 'STUDENTS') {
|
||||
setSelectedEventId(null);
|
||||
setCurrentView('EVENTS');
|
||||
} else if (currentView === 'EVENTS') {
|
||||
setSelectedDomain(null);
|
||||
setCurrentView('DOMAINS');
|
||||
} else if (currentView === 'DOMAINS') {
|
||||
setSelectedCategory(null);
|
||||
setCurrentView('CATEGORIES');
|
||||
}
|
||||
};
|
||||
|
||||
const filteredParticipants = useMemo(() => {
|
||||
return registrations.filter(r => {
|
||||
const userName = r.user_name || '';
|
||||
const regNo = r.reg_no || '';
|
||||
const searchLower = searchTerm.toLowerCase();
|
||||
|
||||
const matchesSearch = !searchTerm ||
|
||||
userName.toLowerCase().includes(searchLower) ||
|
||||
regNo.toLowerCase().includes(searchLower);
|
||||
|
||||
const matchesEvent = !selectedEventId || String(r.event_id) === String(selectedEventId);
|
||||
const matchesDept = selectedDepts.length === 0 || (r.dept && selectedDepts.includes(r.dept));
|
||||
const matchesYear = selectedYears.length === 0 || (r.year && selectedYears.includes(r.year));
|
||||
|
||||
const isExternal = r.college && r.college !== 'RAJALAKSHMI INSTITUTE OF TECHNOLOGY' && r.college !== 'Rajalakshmi Institute of Technology';
|
||||
const matchesType = studentTypeFilter === 'EXTERNAL' ? isExternal : !isExternal;
|
||||
|
||||
return matchesSearch && matchesEvent && matchesDept && matchesYear && matchesType;
|
||||
});
|
||||
}, [registrations, selectedEventId, searchTerm, selectedDepts, selectedYears, studentTypeFilter, externalUserIds]);
|
||||
|
||||
const getEventTitle = (eventId: string) => {
|
||||
return events.find(e => e.id === eventId)?.title || 'Unknown Event';
|
||||
};
|
||||
|
||||
const handleExportSelected = () => {
|
||||
if (selectedStudentIds.size === 0) return;
|
||||
|
||||
const selectedData = filteredParticipants.filter(p => selectedStudentIds.has(p.id));
|
||||
|
||||
// Formatting data for Excel
|
||||
const formattedData = selectedData.map(p => ({
|
||||
'Student Name': p.user_name || 'N/A',
|
||||
'Reg No': String(p.reg_no || 'N/A'),
|
||||
'Gender': p.gender || 'N/A',
|
||||
'College': p.college || 'RIT',
|
||||
'Department': p.dept || 'N/A',
|
||||
'Section': p.section || 'N/A',
|
||||
'Year': p.year || 'N/A',
|
||||
'Email': p.user_id?.includes('@') ? p.user_id : (p.user_email || 'N/A'),
|
||||
'Phone': String(p.phone || 'N/A'),
|
||||
'Event': p.event_name || getEventTitle(String(p.event_id)),
|
||||
'Team': p.team_name || 'N/A',
|
||||
'Payment Status': p.payment_status || 'PENDING',
|
||||
'Cert Status': p.certification_status === 'APPROVED' ? 'VERIFIED' : (p.certification_url ? 'PENDING' : 'NO CERT'),
|
||||
'OD Status': p.od_url ? 'OD READY' : 'NO OD'
|
||||
}));
|
||||
|
||||
// Create worksheet
|
||||
const worksheet = XLSX.utils.json_to_sheet(formattedData);
|
||||
|
||||
// Auto-fit columns
|
||||
if (formattedData.length > 0) {
|
||||
const keys = Object.keys(formattedData[0]);
|
||||
const wscols = keys.map(key => ({
|
||||
wch: Math.max(
|
||||
key.length,
|
||||
...formattedData.map(row => String(row[key as keyof typeof row] || '').length)
|
||||
) + 2
|
||||
}));
|
||||
worksheet['!cols'] = wscols;
|
||||
}
|
||||
|
||||
// Create workbook and export
|
||||
const workbook = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, 'Participants');
|
||||
|
||||
XLSX.writeFile(workbook, `Participants_Export_${new Date().getTime()}.xlsx`);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="animate-in slide-in-from-bottom-10 duration-500 pt-12">
|
||||
<div className="flex flex-col md:flex-row md:items-end justify-between mb-8 gap-8 border-b border-slate-200 pb-8">
|
||||
<div>
|
||||
<h3 className="text-4xl font-black tracking-tighter uppercase mb-2 text-slate-900">
|
||||
SYSTEM <span className="text-[#004a99]">REGISTRY</span>
|
||||
</h3>
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-[0.4em]">Live Database Dossier Sync</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
{currentView !== 'CATEGORIES' && (
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="text-slate-600 font-black uppercase tracking-widest text-[10px] bg-slate-50 px-6 py-3 rounded-full border border-slate-200 hover:bg-slate-100 hover:text-slate-900 shadow-sm transition-all"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-10 flex flex-col md:flex-row gap-4">
|
||||
<div className="relative flex-1">
|
||||
<i className="fas fa-search absolute left-6 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search registry..."
|
||||
className="w-full bg-white border border-slate-200 rounded-2xl pl-14 pr-6 py-4 text-slate-900 focus:ring-2 focus:ring-[#004a99] outline-none font-bold placeholder:text-slate-400 transition-all shadow-sm"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setShowEventFilter(!showEventFilter)}
|
||||
className="h-full px-6 py-4 bg-white border border-slate-200 rounded-2xl flex items-center gap-3 text-slate-700 font-bold hover:bg-slate-50 hover:border-slate-300 transition-all min-w-[240px] justify-between shadow-sm"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<i className="fas fa-filter text-[#004a99] text-xs"></i>
|
||||
<span className="text-xs uppercase tracking-widest truncate max-w-[150px]">
|
||||
{selectedEventId ? events.find(e => e.id === selectedEventId)?.title : 'All Events'}
|
||||
</span>
|
||||
</div>
|
||||
<i className={`fas fa-chevron-down text-[10px] transition-transform ${showEventFilter ? 'rotate-180' : ''}`}></i>
|
||||
</button>
|
||||
|
||||
{showEventFilter && (
|
||||
<div className="absolute top-full right-0 mt-2 w-80 bg-white border border-slate-200 rounded-2xl shadow-xl z-50 py-2 max-h-[400px] overflow-y-auto custom-scrollbar animate-in fade-in slide-in-from-top-2">
|
||||
<button
|
||||
onClick={() => { setSelectedEventId(null); setShowEventFilter(false); setCurrentView('STUDENTS'); }}
|
||||
className={`w-full text-left px-6 py-3 text-[10px] font-black uppercase tracking-widest hover:bg-slate-50 transition-all ${!selectedEventId ? 'text-[#004a99] bg-blue-50/50' : 'text-slate-600'}`}
|
||||
>
|
||||
All Events
|
||||
</button>
|
||||
<div className="h-px bg-slate-100 my-2" />
|
||||
{events.map(event => (
|
||||
<button
|
||||
key={event.id}
|
||||
onClick={() => { setSelectedEventId(event.id); setShowEventFilter(false); setCurrentView('STUDENTS'); }}
|
||||
className={`w-full text-left px-6 py-3 text-[10px] font-black uppercase tracking-widest hover:bg-slate-50 transition-all ${selectedEventId === event.id ? 'text-[#004a99] bg-blue-50/50' : 'text-slate-600'}`}
|
||||
>
|
||||
{event.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowSideFilter(true)}
|
||||
className="px-6 py-4 bg-white border border-slate-200 rounded-2xl text-slate-700 font-black uppercase text-[10px] tracking-widest hover:bg-slate-50 transition-all flex items-center gap-2 relative shadow-sm"
|
||||
>
|
||||
<i className="fas fa-sliders-h text-[#004a99]"></i>
|
||||
Filters
|
||||
{(selectedDepts.length > 0 || selectedYears.length > 0) && (
|
||||
<span className="absolute -top-1 -right-1 w-4 h-4 bg-[#004a99] rounded-full text-[8px] flex items-center justify-center text-white shadow-md">
|
||||
{selectedDepts.length + selectedYears.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setCurrentView(currentView === 'CATEGORIES' ? 'STUDENTS' : 'CATEGORIES');
|
||||
setSelectedEventId(null);
|
||||
setSearchTerm('');
|
||||
}}
|
||||
className="px-6 py-4 bg-[#004a99] text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-blue-800 transition-all flex items-center gap-2 shadow-md hover:shadow-lg active:scale-95"
|
||||
>
|
||||
<i className={`fas ${currentView === 'CATEGORIES' ? 'fa-list' : 'fa-grid-2'}`}></i>
|
||||
{currentView === 'CATEGORIES' ? 'List View' : 'Browse System'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4 mb-8">
|
||||
<button
|
||||
onClick={() => setStudentTypeFilter('INTERNAL')}
|
||||
className={`flex-1 py-4 rounded-2xl border transition-all flex items-center justify-center gap-3 ${studentTypeFilter === 'INTERNAL' ? 'bg-blue-50 border-[#004a99] text-[#004a99] shadow-sm' : 'bg-white border-slate-200 text-slate-500 hover:border-blue-300'}`}
|
||||
>
|
||||
<i className="fas fa-university text-sm"></i>
|
||||
<span className="font-black uppercase tracking-widest text-[10px]">Internal Students</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setStudentTypeFilter('EXTERNAL')}
|
||||
className={`flex-1 py-4 rounded-2xl border transition-all flex items-center justify-center gap-3 ${studentTypeFilter === 'EXTERNAL' ? 'bg-orange-50 border-orange-500 text-orange-600 shadow-sm' : 'bg-white border-slate-200 text-slate-500 hover:border-orange-300'}`}
|
||||
>
|
||||
<i className="fas fa-globe text-sm"></i>
|
||||
<span className="font-black uppercase tracking-widest text-[10px]">External Students</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{currentView === 'CATEGORIES' && !searchTerm && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
|
||||
{CATEGORIES.map((cat) => (
|
||||
<div key={cat.id} onClick={() => handleCategorySelect(cat.id)} className="group relative h-[400px] rounded-[3rem] overflow-hidden cursor-pointer bg-slate-100 border border-slate-200 hover:border-blue-300 hover:shadow-xl transition-all shadow-sm">
|
||||
<img src={cat.image} className="w-full h-full object-cover opacity-60 group-hover:scale-105 transition-transform duration-700" alt={cat.name} />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
|
||||
<div className="absolute bottom-10 left-10">
|
||||
<h4 className="text-3xl font-black text-white uppercase tracking-tighter">{cat.name}</h4>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentView === 'DOMAINS' && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{(DOMAIN_MAP[selectedCategory!] || []).map((domain) => (
|
||||
<div key={domain.id} onClick={() => handleDomainSelect(domain.id)} className="group relative h-[350px] rounded-[2.5rem] overflow-hidden cursor-pointer bg-slate-100 border border-slate-200 hover:border-blue-300 hover:shadow-xl transition-all shadow-sm">
|
||||
<img src={domain.image} className="w-full h-full object-cover opacity-60 group-hover:scale-105 transition-transform duration-700" alt={domain.name} />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
|
||||
<div className="absolute bottom-10 left-10"><h4 className="text-2xl font-black text-white uppercase tracking-tight">{domain.name}</h4></div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentView === 'EVENTS' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{events.filter(e => (!selectedCategory || selectedCategory === 'ALL' || e.category === selectedCategory) && (!selectedDomain || selectedDomain === 'ALL' || e.domain === selectedDomain)).map((event) => (
|
||||
<div key={event.id} onClick={() => handleEventSelect(event.id)} className="bg-white border border-slate-200 rounded-[2.5rem] p-8 cursor-pointer hover:border-blue-300 hover:shadow-md transition-all flex flex-col h-full shadow-sm">
|
||||
<h4 className="text-xl font-black uppercase mb-4 text-slate-900 line-clamp-2">{event.title}</h4>
|
||||
<div className="mt-auto flex items-center justify-between">
|
||||
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest">
|
||||
{registrations.filter(r => String(r.event_id) === String(event.id)).length} Enrolled
|
||||
</p>
|
||||
<div className="w-10 h-10 rounded-full bg-slate-50 flex items-center justify-center text-[#004a99] group-hover:bg-blue-50 transition-colors">
|
||||
<i className="fas fa-arrow-right"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<FilterDrawer
|
||||
isOpen={showSideFilter}
|
||||
onClose={() => setShowSideFilter(false)}
|
||||
activeTab={activeFilterTab}
|
||||
setActiveTab={setActiveFilterTab}
|
||||
departments={departments}
|
||||
years={years}
|
||||
selectedDepts={selectedDepts}
|
||||
setSelectedDepts={setSelectedDepts}
|
||||
selectedYears={selectedYears}
|
||||
setSelectedYears={setSelectedYears}
|
||||
/>
|
||||
|
||||
{(currentView === 'STUDENTS' || searchTerm) && (
|
||||
<div className="space-y-8">
|
||||
{isLoading ? (
|
||||
<div className="col-span-full py-32 flex flex-col items-center justify-center text-center bg-white border border-dashed border-slate-200 rounded-[3rem]">
|
||||
<div className="w-16 h-16 border-4 border-blue-100 border-t-[#004a99] rounded-full animate-spin mb-6"></div>
|
||||
<p className="text-slate-400 font-black uppercase tracking-[0.3em] text-sm">Loading Registrations...</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white border border-slate-200 rounded-[2.5rem] overflow-hidden shadow-sm">
|
||||
<div className="overflow-x-auto custom-scrollbar">
|
||||
<table className="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-slate-50 border-b border-slate-200">
|
||||
<th className="px-8 py-6 w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-4 h-4 rounded border-slate-300 text-[#004a99] focus:ring-[#004a99] cursor-pointer"
|
||||
checked={filteredParticipants.length > 0 && filteredParticipants.every(p => selectedStudentIds.has(p.id))}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setSelectedStudentIds(new Set(filteredParticipants.map(p => p.id)));
|
||||
} else {
|
||||
setSelectedStudentIds(new Set());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</th>
|
||||
<th className="px-8 py-6 text-[10px] font-black tracking-widest text-slate-400 uppercase whitespace-nowrap">Student</th>
|
||||
<th className="px-8 py-6 text-[10px] font-black tracking-widest text-slate-400 uppercase whitespace-nowrap">Event Info</th>
|
||||
<th className="px-8 py-6 text-[10px] font-black tracking-widest text-slate-400 uppercase whitespace-nowrap">Academic</th>
|
||||
<th className="px-8 py-6 text-[10px] font-black tracking-widest text-slate-400 uppercase whitespace-nowrap">Contact</th>
|
||||
<th className="px-8 py-6 text-[10px] font-black tracking-widest text-slate-400 uppercase whitespace-nowrap text-center">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{filteredParticipants.length > 0 ? filteredParticipants.map((p) => (
|
||||
<tr key={p.id} className="hover:bg-slate-50/50 transition-colors group">
|
||||
<td className="px-8 py-6 w-10">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="w-4 h-4 rounded border-slate-300 text-[#004a99] focus:ring-[#004a99] cursor-pointer"
|
||||
checked={selectedStudentIds.has(p.id)}
|
||||
onChange={(e) => {
|
||||
const newSet = new Set(selectedStudentIds);
|
||||
if (e.target.checked) newSet.add(p.id);
|
||||
else newSet.delete(p.id);
|
||||
setSelectedStudentIds(newSet);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="px-8 py-6 whitespace-nowrap">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-2xl bg-blue-50 text-[#004a99] flex items-center justify-center font-black text-lg border border-blue-100 shadow-sm">
|
||||
{(p.user_name || 'N').charAt(0).toUpperCase()}
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<p className="text-sm font-black text-slate-900 uppercase tracking-tight group-hover:text-[#004a99] transition-colors">{p.user_name || 'N/A'}</p>
|
||||
{(externalUserIds.has(p.user_id) || !!p.college) && (
|
||||
<span className="bg-orange-100 text-orange-600 text-[7px] font-black px-1.5 py-0.5 rounded-md border border-orange-200 uppercase tracking-tighter">External</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">{p.reg_no || 'N/A'}</p>
|
||||
{p.college && (
|
||||
<p className="text-[9px] font-black text-orange-500 uppercase tracking-widest mt-0.5 flex items-center gap-1">
|
||||
<i className="fas fa-school text-[8px]"></i> {p.college}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-8 py-6 whitespace-nowrap">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-8 h-8 rounded-full bg-orange-50 text-orange-500 flex items-center justify-center border border-orange-100 shrink-0">
|
||||
<i className="fas fa-calendar-alt text-[10px]"></i>
|
||||
</div>
|
||||
<p className="text-xs font-bold text-slate-700 truncate max-w-[200px] uppercase">{p.event_name || getEventTitle(String(p.event_id))}</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-8 py-6 whitespace-nowrap">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<i className="fas fa-university text-slate-400 text-[10px]"></i>
|
||||
<p className="text-[10px] font-black text-slate-700 uppercase tracking-wider">{p.dept || 'N/A'} {p.section ? `• SEC ${p.section}` : ''}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 pl-[18px]">
|
||||
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest">{p.year || 'N/A'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-8 py-6 whitespace-nowrap">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-7 h-7 rounded-full bg-slate-100 text-slate-400 flex items-center justify-center shrink-0">
|
||||
<i className="fas fa-envelope text-[9px]"></i>
|
||||
</div>
|
||||
<p className="text-[11px] font-bold text-slate-600 lowercase tracking-wide">
|
||||
{p.user_id?.includes('@') ? p.user_id : (p.user_email || 'N/A')}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-8 py-6 whitespace-nowrap">
|
||||
<div className="flex flex-col gap-1.5 items-center justify-center">
|
||||
<span className={`text-[7px] font-black px-2 py-1 rounded-md uppercase tracking-widest border w-24 text-center shadow-sm ${p.payment_status === 'COMPLETED' ? 'bg-emerald-50 text-emerald-600 border-emerald-200' : 'bg-rose-50 text-rose-600 border-rose-200'}`}>
|
||||
<i className={`fas mr-1 ${p.payment_status === 'COMPLETED' ? 'fa-check' : 'fa-clock'}`}></i> {p.payment_status || 'PENDING'}
|
||||
</span>
|
||||
<span className={`text-[7px] font-black px-2 py-1 rounded-md uppercase tracking-widest border w-24 text-center shadow-sm ${p.certification_status === 'APPROVED' ? 'bg-blue-50 text-[#004a99] border-blue-200' : (p.certification_url ? 'bg-amber-50 text-amber-600 border-amber-200' : 'bg-slate-50 text-slate-400 border-slate-200')}`}>
|
||||
<i className={`fas mr-1 ${p.certification_status === 'APPROVED' ? 'fa-check-double' : (p.certification_url ? 'fa-spinner' : 'fa-certificate')}`}></i> {p.certification_status === 'APPROVED' ? 'VERIFIED' : (p.certification_url ? 'PENDING' : 'NO CERT')}
|
||||
</span>
|
||||
<span className={`text-[7px] font-black px-2 py-1 rounded-md uppercase tracking-widest border w-24 text-center shadow-sm ${p.od_url ? 'bg-teal-50 text-teal-600 border-teal-200' : 'bg-slate-50 text-slate-400 border-slate-200'}`}>
|
||||
<i className={`fas mr-1 ${p.od_url ? 'fa-file-signature' : 'fa-file-excel'}`}></i> {p.od_url ? 'OD READY' : 'NO OD'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)) : (
|
||||
<tr>
|
||||
<td colSpan={6} className="px-8 py-32 text-center">
|
||||
<div className="flex flex-col items-center justify-center">
|
||||
<div className="w-20 h-20 bg-slate-50 border border-slate-200 rounded-full flex items-center justify-center mb-6 shadow-sm">
|
||||
<i className="fas fa-search text-slate-300 text-2xl"></i>
|
||||
</div>
|
||||
<p className="text-slate-400 font-black uppercase tracking-[0.3em] text-sm">No active registrations for this scope</p>
|
||||
<p className="text-slate-500 text-[10px] font-bold uppercase tracking-widest mt-2 mb-8">Try adjusting your search or filters</p>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSearchTerm('');
|
||||
setSelectedEventId(null);
|
||||
setSelectedDepts([]);
|
||||
setSelectedYears([]);
|
||||
}}
|
||||
className="px-8 py-3 bg-white border border-slate-200 rounded-xl text-[#004a99] font-black uppercase text-[10px] tracking-widest hover:bg-slate-50 hover:border-slate-300 transition-all shadow-sm"
|
||||
>
|
||||
Clear All Filters
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedStudentIds.size > 0 && (
|
||||
<div className="fixed bottom-10 left-1/2 -translate-x-1/2 z-[1000] flex justify-center animate-in slide-in-from-bottom-10 pointer-events-none">
|
||||
<div className="bg-white/95 backdrop-blur-xl border border-slate-200 shadow-2xl rounded-full px-8 py-4 flex items-center gap-6 pointer-events-auto ring-1 ring-[#004a99]/10">
|
||||
<span className="text-xs font-black text-slate-600 uppercase tracking-widest">
|
||||
{selectedStudentIds.size} Selected
|
||||
</span>
|
||||
<button
|
||||
onClick={handleExportSelected}
|
||||
className="bg-emerald-600 text-white px-8 py-3 rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-emerald-700 transition-all shadow-lg shadow-emerald-600/20 active:scale-95 flex items-center gap-2"
|
||||
>
|
||||
<i className="fas fa-file-excel"></i> Export as Excel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FilterDrawer: React.FC<{
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
activeTab: 'DEPT' | 'YEAR';
|
||||
setActiveTab: (tab: 'DEPT' | 'YEAR') => void;
|
||||
departments: string[];
|
||||
years: string[];
|
||||
selectedDepts: string[];
|
||||
setSelectedDepts: (depts: string[]) => void;
|
||||
selectedYears: string[];
|
||||
setSelectedYears: (years: string[]) => void;
|
||||
}> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
activeTab,
|
||||
setActiveTab,
|
||||
departments,
|
||||
years,
|
||||
selectedDepts,
|
||||
setSelectedDepts,
|
||||
selectedYears,
|
||||
setSelectedYears
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const toggleDept = (dept: string) => {
|
||||
if (selectedDepts.includes(dept)) {
|
||||
setSelectedDepts(selectedDepts.filter(d => d !== dept));
|
||||
} else {
|
||||
setSelectedDepts([...selectedDepts, dept]);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleYear = (year: string) => {
|
||||
if (selectedYears.includes(year)) {
|
||||
setSelectedYears(selectedYears.filter(y => y !== year));
|
||||
} else {
|
||||
setSelectedYears([...selectedYears, year]);
|
||||
}
|
||||
};
|
||||
|
||||
const clearAll = () => {
|
||||
setSelectedDepts([]);
|
||||
setSelectedYears([]);
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-[9999] flex justify-end">
|
||||
<div className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm" onClick={onClose}></div>
|
||||
<div className="relative w-full max-w-md bg-white h-full shadow-2xl flex flex-col animate-in slide-in-from-right duration-300">
|
||||
<div className="p-8 border-b border-slate-100 flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-xl font-black text-slate-900 uppercase tracking-tight">Refine Results</h4>
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mt-1">Select multiple filters</p>
|
||||
</div>
|
||||
<button onClick={onClose} className="w-10 h-10 rounded-full bg-slate-50 border border-slate-200 flex items-center justify-center text-slate-400 hover:text-slate-900 hover:bg-slate-100 transition-all">
|
||||
<i className="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex overflow-hidden">
|
||||
{/* Sidebar Tabs */}
|
||||
<div className="w-32 border-r border-slate-100 bg-slate-50/50">
|
||||
<button
|
||||
onClick={() => setActiveTab('DEPT')}
|
||||
className={`w-full py-6 px-4 text-left relative transition-all ${activeTab === 'DEPT' ? 'bg-blue-50 text-blue-800' : 'text-slate-500 hover:text-slate-700 hover:bg-slate-100/50'}`}
|
||||
>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest">Department</span>
|
||||
{activeTab === 'DEPT' && <div className="absolute right-0 top-0 bottom-0 w-1 bg-[#004a99]"></div>}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('YEAR')}
|
||||
className={`w-full py-6 px-4 text-left relative transition-all ${activeTab === 'YEAR' ? 'bg-blue-50 text-blue-800' : 'text-slate-500 hover:text-slate-700 hover:bg-slate-100/50'}`}
|
||||
>
|
||||
<span className="text-[10px] font-black uppercase tracking-widest">Year</span>
|
||||
{activeTab === 'YEAR' && <div className="absolute right-0 top-0 bottom-0 w-1 bg-[#004a99]"></div>}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Options Content */}
|
||||
<div className="flex-1 p-8 overflow-y-auto custom-scrollbar">
|
||||
{activeTab === 'DEPT' ? (
|
||||
<div className="space-y-4">
|
||||
{departments.map(dept => (
|
||||
<label key={dept} className="flex items-center gap-4 group cursor-pointer">
|
||||
<div
|
||||
onClick={() => toggleDept(dept)}
|
||||
className={`w-6 h-6 rounded-lg border-2 flex items-center justify-center transition-all ${selectedDepts.includes(dept) ? 'bg-[#004a99] border-[#004a99] text-white' : 'border-slate-200 bg-white group-hover:border-blue-400'}`}
|
||||
>
|
||||
{selectedDepts.includes(dept) && <i className="fas fa-check text-[10px]"></i>}
|
||||
</div>
|
||||
<span className={`text-xs font-black uppercase tracking-widest transition-colors ${selectedDepts.includes(dept) ? 'text-slate-900' : 'text-slate-500'}`}>
|
||||
{dept}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{years.map(year => (
|
||||
<label key={year} className="flex items-center gap-4 group cursor-pointer">
|
||||
<div
|
||||
onClick={() => toggleYear(year)}
|
||||
className={`w-6 h-6 rounded-lg border-2 flex items-center justify-center transition-all ${selectedYears.includes(year) ? 'bg-[#004a99] border-[#004a99] text-white' : 'border-slate-200 bg-white group-hover:border-blue-400'}`}
|
||||
>
|
||||
{selectedYears.includes(year) && <i className="fas fa-check text-[10px]"></i>}
|
||||
</div>
|
||||
<span className={`text-xs font-black uppercase tracking-widest transition-colors ${selectedYears.includes(year) ? 'text-slate-900' : 'text-slate-500'}`}>
|
||||
{year}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-8 border-t border-slate-100 bg-slate-50 flex gap-4">
|
||||
<button
|
||||
onClick={clearAll}
|
||||
className="flex-1 py-4 bg-white border border-slate-200 rounded-2xl text-[10px] font-black text-slate-500 uppercase tracking-widest hover:bg-slate-100 transition-all shadow-sm"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 py-4 bg-[#004a99] text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-blue-800 transition-all shadow-md active:scale-95"
|
||||
>
|
||||
Apply Filters
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body
|
||||
);
|
||||
};
|
||||
|
||||
export default FacultyParticipantsView;
|
||||
Reference in New Issue
Block a user