670 lines
36 KiB
TypeScript
670 lines
36 KiB
TypeScript
import React, { useState, useMemo, useEffect } from 'react';
|
|
import { createPortal } from 'react-dom';
|
|
import { Event } from '../types';
|
|
import { supabase } from '../supabase';
|
|
|
|
interface AdminStatusTrackerViewProps {
|
|
events: Event[];
|
|
onShowToast: (msg: string) => void;
|
|
studentDocuments: Record<string, string>;
|
|
onApproveCertificate?: (regId: string) => Promise<void>;
|
|
localRegistrations?: any[];
|
|
isAdmin?: boolean;
|
|
currentUserId?: string;
|
|
}
|
|
|
|
interface FilterState {
|
|
depts: string[];
|
|
years: string[];
|
|
}
|
|
|
|
const AdminStatusTrackerView: React.FC<AdminStatusTrackerViewProps> = ({
|
|
events,
|
|
onShowToast,
|
|
onApproveCertificate,
|
|
localRegistrations = [],
|
|
isAdmin = false,
|
|
currentUserId
|
|
}) => {
|
|
const [selectedEventId, setSelectedEventId] = useState<string | null>(null);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [viewingDoc, setViewingDoc] = useState<string | null>(null);
|
|
const [isApproving, setIsApproving] = useState<string | null>(null);
|
|
const [liveRegistrations, setLiveRegistrations] = useState<any[]>(localRegistrations);
|
|
const [isLoadingRegs, setIsLoadingRegs] = useState(true);
|
|
|
|
const [activeFilters, setActiveFilters] = useState<FilterState>({ depts: [], years: [] });
|
|
const [showFilters, setShowFilters] = useState(false);
|
|
const [viewMode, setViewMode] = useState<'ALL' | 'OD_PENDING' | 'CERT_PENDING'>('ALL');
|
|
const [isUploadingOd, setIsUploadingOd] = useState<string | null>(null);
|
|
const [externalUserIds, setExternalUserIds] = useState<Set<string>>(new Set());
|
|
const [studentTypeFilter, setStudentTypeFilter] = useState<'INTERNAL' | 'EXTERNAL'>('INTERNAL');
|
|
const [isLandingMode, setIsLandingMode] = useState(true);
|
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
|
const [isBulkUploading, setIsBulkUploading] = useState(false);
|
|
|
|
|
|
const handleOdUpload = async (e: React.ChangeEvent<HTMLInputElement>, regId: string) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
setIsUploadingOd(regId);
|
|
try {
|
|
const reader = new FileReader();
|
|
reader.onloadend = async () => {
|
|
try {
|
|
const base64 = reader.result as string;
|
|
const isPdf = file.type === 'application/pdf';
|
|
const extension = isPdf ? 'pdf' : 'jpg';
|
|
const fileName = `OD_${regId}_${Date.now()}.${extension}`;
|
|
|
|
const { uploadToSupabase } = await import('../supabase');
|
|
const publicUrl = await uploadToSupabase(base64, fileName, 'OD_PROVIDERS');
|
|
|
|
const { error } = await supabase.from('registrations').update({ od_url: publicUrl }).eq('id', regId);
|
|
if (error) throw error;
|
|
|
|
onShowToast("OD Document uploaded successfully.");
|
|
await refreshRegs();
|
|
} catch (uploadErr: any) {
|
|
console.error("OD Upload processing failed", uploadErr);
|
|
alert(`OD Upload failed: ${uploadErr.message || 'Unknown error'}`);
|
|
} finally {
|
|
setIsUploadingOd(null);
|
|
}
|
|
};
|
|
reader.readAsDataURL(file);
|
|
} catch (err: any) {
|
|
console.error("OD Upload failed", err);
|
|
alert(`OD Upload failed: ${err.message || 'Unknown error'}`);
|
|
setIsUploadingOd(null);
|
|
}
|
|
};
|
|
|
|
// Fetch all registrations directly from Supabase (bypasses prop staleness)
|
|
useEffect(() => {
|
|
const fetchRegs = async () => {
|
|
setIsLoadingRegs(true);
|
|
const { data, error } = await supabase
|
|
.from('registrations')
|
|
.select('*')
|
|
.order('registered_at', { ascending: false });
|
|
if (data && !error) {
|
|
setLiveRegistrations(data);
|
|
} else if (localRegistrations.length > 0) {
|
|
setLiveRegistrations(localRegistrations);
|
|
}
|
|
|
|
const { data: extUsers } = await supabase.from('externalusers').select('id');
|
|
if (extUsers) {
|
|
setExternalUserIds(new Set(extUsers.map(u => u.id)));
|
|
}
|
|
|
|
setIsLoadingRegs(false);
|
|
};
|
|
fetchRegs();
|
|
}, []);
|
|
|
|
const refreshRegs = async () => {
|
|
const { data, error } = await supabase
|
|
.from('registrations')
|
|
.select('*')
|
|
.order('registered_at', { ascending: false });
|
|
if (data && !error) setLiveRegistrations(data);
|
|
|
|
const { data: extUsers } = await supabase.from('externalusers').select('id');
|
|
if (extUsers) {
|
|
setExternalUserIds(new Set(extUsers.map(u => u.id)));
|
|
}
|
|
};
|
|
|
|
const handleApprove = async (regId: string) => {
|
|
if (onApproveCertificate) {
|
|
setIsApproving(regId);
|
|
try {
|
|
await onApproveCertificate(regId);
|
|
onShowToast("Certificate verified and student hub updated.");
|
|
await refreshRegs(); // Refresh so card instantly shows Verified
|
|
} catch (err) {
|
|
console.error("Approval failed:", err);
|
|
} finally {
|
|
setIsApproving(null);
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleDownload = async (url: string, studentName: string, eventTitle: string) => {
|
|
try {
|
|
const response = await fetch(url);
|
|
const blob = await response.blob();
|
|
const blobUrl = window.URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = blobUrl;
|
|
link.download = `Cert_${studentName.replace(/\s+/g, '_')}_${eventTitle.replace(/\s+/g, '_')}.jpg`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
window.URL.revokeObjectURL(blobUrl);
|
|
} catch (err) {
|
|
console.error("Download failed:", err);
|
|
}
|
|
};
|
|
|
|
const filterOptions = {
|
|
depts: ['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'],
|
|
years: ['1st Year', '2nd Year', '3rd Year', '4th Year', '5th Year']
|
|
};
|
|
|
|
const mappedParticipants = useMemo(() => {
|
|
return liveRegistrations.map(reg => {
|
|
const event = events.find(e => e.id === reg.event_id);
|
|
const isEnded = event?.status === 'Completed';
|
|
const isExternal = externalUserIds.has(reg.user_id);
|
|
const isVerified = reg.certification_status === 'APPROVED' || isExternal;
|
|
const hasUploaded = !!reg.certification_url;
|
|
|
|
const hasUploadedOd = !!reg.od_url;
|
|
const isFree = event?.pricingType === 'FREE';
|
|
const isPaidVerified = reg.payment_status === 'COMPLETED';
|
|
|
|
const stages = [
|
|
{ label: 'Registered', status: 'completed' },
|
|
{ label: 'TEAM', status: !event?.isTeamEvent || reg.team_code ? 'completed' : 'active' },
|
|
{ label: 'Payment', status: isFree || isPaidVerified ? 'completed' : (reg.team_code || !event?.isTeamEvent ? 'active' : 'pending') },
|
|
{ label: 'Ticket', status: isFree || isPaidVerified ? 'completed' : 'pending' },
|
|
{ label: 'Ongoing', status: isEnded ? 'completed' : (event?.status === 'Event Ongoing' ? 'active' : 'pending') },
|
|
{ label: 'Ended', status: isEnded ? 'completed' : 'pending' },
|
|
{ label: 'Certification', status: isVerified ? 'completed' : (isEnded ? 'active' : 'pending') },
|
|
{ label: 'OD', status: hasUploadedOd ? 'completed' : (isVerified ? 'active' : 'pending') }
|
|
];
|
|
|
|
return {
|
|
...reg,
|
|
name: reg.user_name || 'Student',
|
|
regNo: reg.reg_no || 'N/A',
|
|
phone: reg.phone || 'No Phone',
|
|
dept: reg.dept || 'N/A',
|
|
year: reg.year || 'N/A',
|
|
college: reg.college || null,
|
|
eventTitle: event?.title || 'Unknown Event',
|
|
stages,
|
|
hasUploaded,
|
|
isVerified,
|
|
isExternal,
|
|
hasUploadedOd,
|
|
od_url: reg.od_url
|
|
};
|
|
});
|
|
}, [liveRegistrations, events]);
|
|
|
|
const filteredParticipants = useMemo(() => {
|
|
return mappedParticipants.filter(p => {
|
|
const eventMatch = !selectedEventId || p.event_id === selectedEventId;
|
|
const searchMatch = p.name.toLowerCase().includes(searchTerm.toLowerCase()) || p.regNo.toLowerCase().includes(searchTerm.toLowerCase());
|
|
const deptMatch = activeFilters.depts.length === 0 || activeFilters.depts.includes(p.dept);
|
|
const yearMatch = activeFilters.years.length === 0 || activeFilters.years.includes(p.year);
|
|
|
|
let viewMatch = true;
|
|
if (viewMode === 'OD_PENDING') viewMatch = p.isVerified;
|
|
if (viewMode === 'CERT_PENDING') viewMatch = p.hasUploaded || (p.stages[5].status === 'completed'); // Ended or Uploaded
|
|
|
|
const isExternal = externalUserIds.has(p.user_id);
|
|
const typeMatch = studentTypeFilter === 'EXTERNAL' ? isExternal : !isExternal;
|
|
|
|
return eventMatch && searchMatch && deptMatch && yearMatch && viewMatch && typeMatch;
|
|
}).sort((a, b) => {
|
|
// Sort pending certificates to the top
|
|
if (a.hasUploaded && !a.isVerified && (!b.hasUploaded || b.isVerified)) return -1;
|
|
if (b.hasUploaded && !b.isVerified && (!a.hasUploaded || a.isVerified)) return 1;
|
|
return 0;
|
|
});
|
|
}, [mappedParticipants, selectedEventId, searchTerm, activeFilters, externalUserIds, studentTypeFilter, viewMode]);
|
|
|
|
const handleBulkOdUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file || selectedIds.size === 0) return;
|
|
|
|
setIsBulkUploading(true);
|
|
try {
|
|
const reader = new FileReader();
|
|
reader.onloadend = async () => {
|
|
try {
|
|
const base64 = reader.result as string;
|
|
const isPdf = file.type === 'application/pdf';
|
|
const extension = isPdf ? 'pdf' : 'jpg';
|
|
const fileName = `BULK_OD_${Date.now()}.${extension}`;
|
|
|
|
const { uploadToSupabase } = await import('../supabase');
|
|
const publicUrl = await uploadToSupabase(base64, fileName, 'OD_PROVIDERS');
|
|
|
|
const idsArray = Array.from(selectedIds);
|
|
const { error } = await supabase.from('registrations').update({ od_url: publicUrl }).in('id', idsArray);
|
|
if (error) throw error;
|
|
|
|
onShowToast(`OD Document assigned to ${selectedIds.size} students successfully.`);
|
|
setSelectedIds(new Set());
|
|
await refreshRegs();
|
|
} catch (uploadErr: any) {
|
|
console.error("Bulk OD Upload failed", uploadErr);
|
|
alert(`Bulk OD Upload failed: ${uploadErr.message || 'Unknown error'}`);
|
|
} finally {
|
|
setIsBulkUploading(false);
|
|
}
|
|
};
|
|
reader.readAsDataURL(file);
|
|
} catch (err: any) {
|
|
console.error("Bulk OD Upload failed", err);
|
|
alert(`Bulk OD Upload failed: ${err.message || 'Unknown error'}`);
|
|
setIsBulkUploading(false);
|
|
}
|
|
};
|
|
|
|
const toggleSelectAll = () => {
|
|
if (selectedIds.size === filteredParticipants.length) {
|
|
setSelectedIds(new Set());
|
|
} else {
|
|
setSelectedIds(new Set(filteredParticipants.map(p => p.id)));
|
|
}
|
|
};
|
|
|
|
const toggleFilterValue = (category: 'depts' | 'years', value: string) => {
|
|
|
|
setActiveFilters(prev => {
|
|
const current = prev[category];
|
|
const next = current.includes(value) ? current.filter(v => v !== value) : [...current, value];
|
|
return { ...prev, [category]: next };
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="animate-in fade-in slide-in-from-bottom-10 duration-500 max-w-7xl mx-auto py-10 px-4">
|
|
{isLandingMode ? (
|
|
<div className="flex flex-col items-center py-20 px-4 text-center">
|
|
<div className="mb-12">
|
|
<h3 className="text-5xl font-black text-slate-900 tracking-tighter uppercase mb-3">
|
|
Status <span className="text-[#004a99]">Tracker</span>
|
|
</h3>
|
|
<p className="text-slate-400 font-bold uppercase tracking-[0.4em] text-xs">Choose Verification Category</p>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 w-full max-w-5xl">
|
|
<button
|
|
onClick={() => { setViewMode('OD_PENDING'); setIsLandingMode(false); }}
|
|
className="group relative h-[350px] bg-white border border-slate-200 rounded-[3rem] p-12 overflow-hidden transition-all duration-500 hover:shadow-2xl hover:border-[#004a99] hover:-translate-y-2 flex flex-col items-center justify-center text-center shadow-sm"
|
|
>
|
|
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:scale-110 transition-transform duration-700">
|
|
<i className="fas fa-file-invoice-dollar text-[120px] text-[#004a99]"></i>
|
|
</div>
|
|
<div className="w-24 h-24 rounded-3xl bg-blue-50 flex items-center justify-center text-[#004a99] mb-8 group-hover:bg-[#004a99] group-hover:text-white transition-all duration-500 shadow-inner">
|
|
<i className="fas fa-file-signature text-4xl"></i>
|
|
</div>
|
|
<h4 className="text-3xl font-black text-slate-900 uppercase tracking-tighter mb-4">OD Pending <span className="text-[#004a99]">List</span></h4>
|
|
<p className="text-slate-500 font-medium leading-relaxed max-w-[240px] mb-8">Process on-duty requests and verify institutional approvals for students.</p>
|
|
<div className="flex items-center gap-2 text-[#004a99] font-black text-xs uppercase tracking-[0.2em]">
|
|
Explore Registry <i className="fas fa-arrow-right animate-pulse"></i>
|
|
</div>
|
|
</button>
|
|
|
|
<button
|
|
onClick={() => { setViewMode('CERT_PENDING'); setIsLandingMode(false); }}
|
|
className="group relative h-[350px] bg-white border border-slate-200 rounded-[3rem] p-12 overflow-hidden transition-all duration-500 hover:shadow-2xl hover:border-orange-500 hover:-translate-y-2 flex flex-col items-center justify-center text-center shadow-sm"
|
|
>
|
|
<div className="absolute top-0 right-0 p-8 opacity-10 group-hover:scale-110 transition-transform duration-700">
|
|
<i className="fas fa-certificate text-[120px] text-orange-500"></i>
|
|
</div>
|
|
<div className="w-24 h-24 rounded-3xl bg-orange-50 flex items-center justify-center text-orange-500 mb-8 group-hover:bg-orange-500 group-hover:text-white transition-all duration-500 shadow-inner">
|
|
<i className="fas fa-award text-4xl"></i>
|
|
</div>
|
|
<h4 className="text-3xl font-black text-slate-900 uppercase tracking-tighter mb-4">Certifications <span className="text-orange-500">Pending</span></h4>
|
|
<p className="text-slate-500 font-medium leading-relaxed max-w-[240px] mb-8">Verify event completion certificates and academic proof of participation.</p>
|
|
<div className="flex items-center gap-2 text-orange-600 font-black text-xs uppercase tracking-[0.2em]">
|
|
Explore Registry <i className="fas fa-arrow-right animate-pulse"></i>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => { setViewMode('ALL'); setIsLandingMode(false); }}
|
|
className="mt-16 text-slate-400 hover:text-[#004a99] font-black text-[10px] uppercase tracking-[0.4em] transition-colors border-b-2 border-transparent hover:border-[#004a99] pb-1"
|
|
>
|
|
Or Manage All Participants <i className="fas fa-external-link-alt ml-2"></i>
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div className="flex flex-col md:flex-row md:items-end justify-between mb-12 gap-8 border-b border-slate-200 pb-8 relative">
|
|
<button
|
|
onClick={() => setIsLandingMode(true)}
|
|
className="absolute -top-12 left-0 text-[10px] font-black text-slate-400 uppercase tracking-widest hover:text-[#004a99] transition-all flex items-center gap-2"
|
|
>
|
|
<i className="fas fa-chevron-left text-[8px]"></i> Return to Choices
|
|
</button>
|
|
<div>
|
|
<h3 className="text-4xl font-black text-slate-900 tracking-tighter uppercase mb-2">
|
|
{viewMode === 'OD_PENDING' ? 'OD' : (viewMode === 'CERT_PENDING' ? 'Certification' : 'System')} <span className="text-[#004a99] font-black">{viewMode === 'ALL' ? 'Monitor' : 'Queue'}</span>
|
|
</h3>
|
|
<p className="text-[10px] font-black text-slate-500 uppercase tracking-[0.4em]">
|
|
Real-time Tracking & Verification Hub
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row gap-4 flex-1 max-w-3xl items-center">
|
|
<div className="relative flex-1 group w-full">
|
|
<select
|
|
className="w-full bg-white border border-slate-200 rounded-2xl pl-12 pr-6 py-4 text-slate-900 focus:ring-2 focus:ring-[#004a99] outline-none font-bold appearance-none cursor-pointer shadow-sm"
|
|
value={selectedEventId || ''}
|
|
onChange={(e) => setSelectedEventId(e.target.value || null)}
|
|
>
|
|
<option value="">All Contexts</option>
|
|
{events.map(ev => <option key={ev.id} value={ev.id}>{ev.title}</option>)}
|
|
</select>
|
|
<i className="fas fa-layer-group absolute left-5 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
|
</div>
|
|
|
|
<div className="relative flex-[1.5] group flex gap-3 w-full">
|
|
<input
|
|
type="text"
|
|
placeholder="Search registry..."
|
|
className="flex-1 bg-white border border-slate-200 rounded-2xl px-6 py-4 text-slate-900 focus:ring-2 focus:ring-[#004a99] outline-none font-bold placeholder:text-slate-400 shadow-sm"
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
/>
|
|
<button onClick={() => setShowFilters(true)} className="bg-white border border-slate-200 px-6 py-4 rounded-2xl hover:border-[#004a99] hover:text-[#004a99] text-slate-500 transition-all shadow-sm">
|
|
<i className="fas fa-sliders-h"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{viewMode !== 'CERT_PENDING' && (
|
|
<div className="flex gap-4 mb-8">
|
|
<button
|
|
onClick={() => setStudentTypeFilter('INTERNAL')}
|
|
className={`flex-1 py-6 rounded-3xl border-2 transition-all flex flex-col items-center justify-center gap-2 ${studentTypeFilter === 'INTERNAL' ? 'bg-blue-50 border-[#004a99] text-[#004a99] shadow-md' : 'bg-white border-slate-200 text-slate-500 hover:border-blue-300'}`}
|
|
>
|
|
<i className="fas fa-university text-2xl"></i>
|
|
<span className="font-black uppercase tracking-widest text-xs">Internal Students</span>
|
|
</button>
|
|
<button
|
|
onClick={() => setStudentTypeFilter('EXTERNAL')}
|
|
className={`flex-1 py-6 rounded-3xl border-2 transition-all flex flex-col items-center justify-center gap-2 ${studentTypeFilter === 'EXTERNAL' ? 'bg-orange-50 border-orange-500 text-orange-600 shadow-md' : 'bg-white border-slate-200 text-slate-500 hover:border-orange-300'}`}
|
|
>
|
|
<i className="fas fa-globe text-2xl"></i>
|
|
<span className="font-black uppercase tracking-widest text-xs">External Students</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 gap-8">
|
|
{isLoadingRegs ? (
|
|
<div className="py-40 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-500 font-black uppercase tracking-[0.3em] text-sm">Syncing Registrations...</p>
|
|
</div>
|
|
) : filteredParticipants.length > 0 ? filteredParticipants.map((p) => (
|
|
<div key={p.id} className={`bg-white border ${selectedIds.has(p.id) ? 'border-[#004a99] ring-2 ring-blue-50' : 'border-slate-200'} rounded-[3rem] p-8 flex flex-col xl:flex-row items-center gap-10 hover:border-blue-300 transition-all duration-500 group/card shadow-sm hover:shadow-xl relative`}>
|
|
{/* Selection Checkbox */}
|
|
<div className="absolute top-8 left-8 z-20">
|
|
<input
|
|
type="checkbox"
|
|
className="w-5 h-5 rounded-lg border-2 border-slate-200 text-[#004a99] focus:ring-[#004a99] cursor-pointer"
|
|
checked={selectedIds.has(p.id)}
|
|
onChange={(e) => {
|
|
const newSet = new Set(selectedIds);
|
|
if (e.target.checked) newSet.add(p.id);
|
|
else newSet.delete(p.id);
|
|
setSelectedIds(newSet);
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
{/* Student Info */}
|
|
<div className="flex items-center gap-6 w-full xl:w-[24rem] text-left pl-10">
|
|
|
|
<div className="w-20 h-20 rounded-3xl bg-blue-50 border border-blue-100 flex items-center justify-center text-[#004a99] text-2xl font-black">
|
|
{p.name.charAt(0)}
|
|
</div>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-3 mb-1">
|
|
<h4 className="text-xl font-black text-slate-900 uppercase tracking-tight truncate">{p.name}</h4>
|
|
{p.hasUploaded && !p.isVerified && (
|
|
<span className="bg-amber-100 text-amber-700 text-[7px] font-black px-2 py-0.5 rounded-full animate-pulse border border-amber-200">PENDING</span>
|
|
)}
|
|
</div>
|
|
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest leading-none mb-1">{p.regNo}</p>
|
|
{p.team_name && (
|
|
<p className="text-[9px] font-black text-blue-600 uppercase tracking-widest mb-1 flex items-center gap-1">
|
|
<i className="fas fa-users"></i> {p.team_name}
|
|
</p>
|
|
)}
|
|
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mb-1">{p.dept} • {p.year}</p>
|
|
{p.college && (
|
|
<p className="text-[9px] font-black text-orange-500 uppercase tracking-widest mb-2 flex items-center gap-1">
|
|
<i className="fas fa-university"></i> {p.college}
|
|
</p>
|
|
)}
|
|
<div className="flex items-center gap-2">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-emerald-500"></div>
|
|
<p className="text-[9px] font-black text-slate-500 uppercase tracking-widest truncate">{p.eventTitle}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Timeline View */}
|
|
<div className="flex-1 w-full overflow-x-auto no-scrollbar py-2">
|
|
<div className="flex items-start justify-between min-w-[500px] relative px-6">
|
|
<div className="absolute top-6 left-12 right-12 h-[2px] bg-slate-100 z-0"></div>
|
|
{p.stages.map((stage: any, idx: number) => (
|
|
<div key={idx} className="relative z-10 flex flex-col items-center w-16">
|
|
<div className={`w-12 h-12 rounded-2xl flex items-center justify-center border transition-all duration-500 ${stage.status === 'completed' ? 'bg-[#004a99] border-blue-800 text-white shadow-lg shadow-blue-900/20' :
|
|
stage.status === 'active' ? 'bg-white border-blue-200 text-[#004a99] shadow-sm' :
|
|
'bg-slate-50 border-slate-200 text-slate-400'
|
|
}`}>
|
|
{stage.status === 'completed' ? <i className="fas fa-check text-sm"></i> : <span className="text-[10px] font-black">{idx + 1}</span>}
|
|
</div>
|
|
<span className="mt-4 text-[8px] font-black uppercase tracking-widest text-slate-500 text-center">{stage.label}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions Area */}
|
|
<div className="w-full xl:w-64 flex flex-col gap-3">
|
|
{p.certification_url && (
|
|
<div className="flex gap-2">
|
|
<button onClick={() => setViewingDoc(p.certification_url)} className="flex-1 py-4 bg-white border border-slate-200 text-slate-700 rounded-2xl text-[9px] font-black uppercase tracking-widest hover:border-[#004a99] hover:text-[#004a99] transition-all shadow-sm">
|
|
<i className="fas fa-eye mr-2"></i> Inspect
|
|
</button>
|
|
<button onClick={() => handleDownload(p.certification_url, p.name, p.eventTitle)} className="w-14 py-4 bg-white border border-slate-200 text-slate-500 rounded-2xl hover:text-[#004a99] hover:border-blue-200 transition-all shadow-sm">
|
|
<i className="fas fa-download"></i>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Certification Status / Approval */}
|
|
{(() => {
|
|
const eventObj = events.find(e => e.id === p.event_id);
|
|
const isOwner = eventObj && eventObj.created_by === currentUserId;
|
|
|
|
if (p.isExternal) {
|
|
return (
|
|
<div className="w-full py-3 bg-blue-50 border border-blue-100 text-blue-600 rounded-2xl text-[10px] font-black uppercase tracking-widest text-center flex items-center justify-center gap-2 shadow-sm">
|
|
<i className="fas fa-check-circle"></i> Auto-Verified (External)
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (p.hasUploaded && !p.isVerified) {
|
|
if (isAdmin || !isOwner) {
|
|
return (
|
|
<div className="w-full py-3 bg-emerald-50 border border-emerald-100 text-emerald-600 rounded-2xl text-[10px] font-black uppercase tracking-widest text-center flex items-center justify-center gap-2 shadow-sm opacity-60">
|
|
<i className="fas fa-shield-check"></i> {isOwner ? "Pending Approval" : "Read Only (Other Coord)"}
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<button
|
|
onClick={() => handleApprove(p.id)}
|
|
disabled={isApproving === p.id}
|
|
className="w-full py-3 bg-emerald-600 text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-emerald-700 transition-all shadow-lg shadow-emerald-600/20 active:scale-95 disabled:opacity-50"
|
|
>
|
|
{isApproving === p.id ? <><i className="fas fa-spinner fa-spin mr-2"></i> Syncing</> : <><i className="fas fa-check-double mr-2"></i> Approve Proof</>}
|
|
</button>
|
|
);
|
|
}
|
|
|
|
if (p.isVerified) {
|
|
return (
|
|
<div className="w-full py-3 bg-emerald-50 border border-emerald-100 text-emerald-600 rounded-2xl text-[10px] font-black uppercase tracking-widest text-center flex items-center justify-center gap-2 shadow-sm">
|
|
<i className="fas fa-shield-check"></i> Verified Credentials
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="w-full py-3 bg-slate-50 border border-slate-200 text-slate-500 rounded-2xl text-[9px] font-black uppercase tracking-widest text-center flex items-center justify-center gap-2">
|
|
<i className="fas fa-clock"></i> Awaiting Cert Upload
|
|
</div>
|
|
);
|
|
})()}
|
|
|
|
{/* OD Upload Actions */}
|
|
{(() => {
|
|
const eventObj = events.find(e => e.id === p.event_id);
|
|
const isOwner = eventObj && eventObj.created_by === currentUserId;
|
|
|
|
if (!p.hasUploadedOd) {
|
|
if (isAdmin || !isOwner) {
|
|
return (
|
|
<div className="w-full py-3 bg-blue-50 border border-blue-100 text-[#004a99] rounded-2xl text-[10px] font-black uppercase tracking-widest text-center flex items-center justify-center gap-2 shadow-sm opacity-60">
|
|
<i className="fas fa-file"></i> {isOwner ? "No OD Uploaded" : "Read Only"}
|
|
</div>
|
|
);
|
|
}
|
|
return (
|
|
<label className={`w-full py-3 ${isUploadingOd === p.id || p.hasUploadedOd ? 'bg-blue-200 text-blue-500 cursor-not-allowed' : 'bg-blue-50 border border-blue-200 text-[#004a99] cursor-pointer hover:bg-blue-100'} rounded-2xl text-[10px] font-black uppercase tracking-widest text-center transition-all flex justify-center items-center gap-2`}>
|
|
{isUploadingOd === p.id ? <><i className="fas fa-spinner fa-spin"></i> Uploading OD...</> : <><i className="fas fa-upload"></i> Upload OD</>}
|
|
<input type="file" className="hidden" accept="image/*,.pdf" onChange={(e) => handleOdUpload(e, p.id)} disabled={isUploadingOd === p.id || p.hasUploadedOd} />
|
|
</label>
|
|
);
|
|
}
|
|
return (
|
|
<div className="w-full py-3 bg-teal-50 border border-teal-100 text-teal-600 rounded-2xl text-[10px] font-black uppercase tracking-widest text-center flex items-center justify-center gap-2 shadow-sm">
|
|
<i className="fas fa-check-circle"></i> OD Uploaded
|
|
<button onClick={() => setViewingDoc(p.od_url)} className="ml-2 hover:text-teal-800 focus:outline-none"><i className="fas fa-eye"></i></button>
|
|
</div>
|
|
);
|
|
})()}
|
|
</div>
|
|
</div>
|
|
)) : (
|
|
<div className="py-40 flex flex-col items-center justify-center text-center bg-white border-2 border-dashed border-slate-200 rounded-[4rem]">
|
|
<div className="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mb-8">
|
|
<i className="fas fa-database text-slate-300 text-3xl"></i>
|
|
</div>
|
|
<h4 className="text-2xl font-black uppercase tracking-tighter text-slate-400">No matching student data</h4>
|
|
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest mt-3">Try adjusting your filters or search term</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Floating Bulk Action Bar */}
|
|
{selectedIds.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-10 py-5 flex items-center gap-8 pointer-events-auto ring-1 ring-[#004a99]/10">
|
|
<div className="flex items-center gap-4 border-r border-slate-100 pr-8">
|
|
<button
|
|
onClick={toggleSelectAll}
|
|
className="w-10 h-10 rounded-xl bg-slate-50 flex items-center justify-center text-[#004a99] hover:bg-blue-50 transition-all border border-slate-100"
|
|
>
|
|
<i className={`fas ${selectedIds.size === filteredParticipants.length ? 'fa-check-double' : 'fa-square'}`}></i>
|
|
</button>
|
|
<span className="text-xs font-black text-slate-900 uppercase tracking-widest whitespace-nowrap">
|
|
{selectedIds.size} Students Active
|
|
</span>
|
|
</div>
|
|
|
|
<label className={`bg-[#004a99] text-white px-10 py-3.5 rounded-full text-[10px] font-black uppercase tracking-widest hover:bg-blue-800 transition-all shadow-xl shadow-blue-900/20 active:scale-95 flex items-center gap-3 cursor-pointer ${isBulkUploading ? 'opacity-50 pointer-events-none' : ''}`}>
|
|
{isBulkUploading ? <i className="fas fa-spinner fa-spin"></i> : <i className="fas fa-file-export"></i>}
|
|
{isBulkUploading ? "Processing..." : "Provide Bulk OD"}
|
|
<input type="file" className="hidden" accept="image/*,.pdf" onChange={handleBulkOdUpload} disabled={isBulkUploading} />
|
|
</label>
|
|
|
|
<button
|
|
onClick={() => setSelectedIds(new Set())}
|
|
className="text-[10px] font-black text-slate-400 hover:text-rose-500 uppercase tracking-widest transition-colors px-4"
|
|
>
|
|
Clear
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{showFilters && createPortal(
|
|
|
|
<div className="fixed inset-0 z-[10005] flex justify-end">
|
|
<div className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm animate-in fade-in duration-300" onClick={() => setShowFilters(false)} />
|
|
<div className="relative w-full max-w-md bg-white h-full shadow-2xl flex flex-col animate-in slide-in-from-right duration-500 border-l border-slate-200">
|
|
<div className="p-10 border-b border-slate-100 flex justify-between items-center bg-slate-50/50">
|
|
<div>
|
|
<h4 className="text-2xl font-black text-slate-900 uppercase tracking-tight">System Filters</h4>
|
|
<p className="text-[10px] font-black text-[#004a99] uppercase tracking-widest mt-1">Refine Registry View</p>
|
|
</div>
|
|
<button onClick={() => setShowFilters(false)} className="w-12 h-12 rounded-full bg-white border border-slate-200 flex items-center justify-center text-slate-400 hover:text-slate-900 hover:border-slate-300 transition-all shadow-sm"><i className="fas fa-times"></i></button>
|
|
</div>
|
|
<div className="flex-1 p-10 space-y-4 overflow-y-auto no-scrollbar">
|
|
<h5 className="text-[10px] font-black text-slate-500 uppercase tracking-[0.3em] mb-6">Departments</h5>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{filterOptions.depts.map(dept => (
|
|
<button
|
|
key={dept}
|
|
onClick={() => toggleFilterValue('depts', dept)}
|
|
className={`py-3 rounded-xl text-[9px] font-black uppercase transition-all border ${activeFilters.depts.includes(dept) ? 'bg-[#004a99] border-[#004a99] text-white shadow-md shadow-blue-900/20' : 'bg-white border-slate-200 text-slate-500 hover:border-slate-300'}`}
|
|
>
|
|
{dept}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<h5 className="text-[10px] font-black text-slate-500 uppercase tracking-[0.3em] mb-6 mt-8">Years</h5>
|
|
<div className="grid grid-cols-2 gap-3">
|
|
{filterOptions.years.map(year => (
|
|
<button
|
|
key={year}
|
|
onClick={() => toggleFilterValue('years', year)}
|
|
className={`py-3 rounded-xl text-[9px] font-black uppercase transition-all border ${activeFilters.years.includes(year) ? 'bg-[#004a99] border-[#004a99] text-white shadow-md shadow-blue-900/20' : 'bg-white border-slate-200 text-slate-500 hover:border-slate-300'}`}
|
|
>
|
|
{year}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="p-10 bg-slate-50 border-t border-slate-200 flex gap-4">
|
|
<button onClick={() => setActiveFilters({ depts: [], years: [] })} className="flex-1 py-4 bg-white border border-slate-200 rounded-2xl text-[10px] font-black text-slate-600 uppercase tracking-widest hover:bg-slate-50 transition-all shadow-sm">Reset View</button>
|
|
<button onClick={() => setShowFilters(false)} className="flex-1 py-4 bg-[#004a99] text-white rounded-2xl text-[10px] font-black uppercase tracking-widest hover:bg-blue-900 shadow-xl shadow-blue-900/20 active:scale-95 transition-all">Apply Filters</button>
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
)}
|
|
|
|
{viewingDoc && createPortal(
|
|
<div className="fixed inset-0 z-[10005] flex items-center justify-center bg-slate-900/60 backdrop-blur-md p-4 animate-in fade-in duration-300" onClick={() => setViewingDoc(null)}>
|
|
<div className="relative w-full max-w-4xl max-h-[90vh] bg-white rounded-[3rem] overflow-hidden shadow-2xl animate-in zoom-in-95 duration-500" onClick={e => e.stopPropagation()}>
|
|
<div className="p-6 border-b border-slate-100 flex justify-between items-center bg-slate-50/50">
|
|
<span className="text-[10px] font-black text-slate-500 uppercase tracking-[0.4em]">Proof Inspection Mode</span>
|
|
<button onClick={() => setViewingDoc(null)} className="w-10 h-10 rounded-full bg-white border border-slate-200 flex items-center justify-center text-slate-500 hover:text-rose-600 hover:border-rose-200 transition-all shadow-sm"><i className="fas fa-times"></i></button>
|
|
</div>
|
|
<div className="p-10 flex items-center justify-center min-h-[50vh] bg-slate-100/50">
|
|
<img src={viewingDoc} className="max-w-full max-h-[70vh] object-contain rounded-2xl shadow-lg ring-1 ring-slate-200" alt="Document" />
|
|
</div>
|
|
</div>
|
|
</div>,
|
|
document.body
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default AdminStatusTrackerView; |