Merged two websites
This commit is contained in:
@@ -0,0 +1,622 @@
|
||||
import { API_BASE_URL } from '../../lib/config';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Users,
|
||||
CheckSquare,
|
||||
Upload,
|
||||
FileCheck,
|
||||
XCircle,
|
||||
CheckCircle,
|
||||
FileText,
|
||||
Search,
|
||||
ChevronRight,
|
||||
Eye,
|
||||
Award
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { cn } from '../../lib/utils';
|
||||
|
||||
interface Event {
|
||||
id: number;
|
||||
title: string;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
type: string;
|
||||
location: string;
|
||||
category: string;
|
||||
department: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface Registration {
|
||||
id: string;
|
||||
userId: string;
|
||||
eventId: number;
|
||||
userName: string;
|
||||
userEmail: string;
|
||||
regNo: string;
|
||||
phone: string;
|
||||
gender: string;
|
||||
dept: string;
|
||||
section: string;
|
||||
year: string;
|
||||
college: string;
|
||||
paymentStatus: 'PENDING' | 'COMPLETED';
|
||||
teamCode: string | null;
|
||||
teamName: string | null;
|
||||
isTeamLeader: boolean;
|
||||
certificationUrl: string | null;
|
||||
certificationStatus: 'NOT_SUBMITTED' | 'PENDING_APPROVAL' | 'APPROVED';
|
||||
odUrl: string | null;
|
||||
}
|
||||
|
||||
export const StudentRegistrationsView: React.FC = () => {
|
||||
const { user } = useAuth();
|
||||
const [events, setEvents] = useState<Event[]>([]);
|
||||
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
|
||||
const [registrations, setRegistrations] = useState<Registration[]>([]);
|
||||
const [attendanceLogs, setAttendanceLogs] = useState<any[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
// Search and view states
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [activeSubTab, setActiveSubTab] = useState<'roster' | 'attendance' | 'od' | 'certificates'>('roster');
|
||||
|
||||
// Audit modals
|
||||
const [inspectCertReg, setInspectCertReg] = useState<Registration | null>(null);
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
|
||||
// OD Upload states
|
||||
const [odFileBase64, setOdFileBase64] = useState<string | null>(null);
|
||||
|
||||
// Attendance checking state (maps registrationId -> dayLabel -> isPresent)
|
||||
const [attendanceGrid, setAttendanceGrid] = useState<Record<string, Record<string, boolean>>>({});
|
||||
|
||||
const daysList = ["Day 1", "Day 2", "Day 3"]; // Standard slots
|
||||
|
||||
useEffect(() => {
|
||||
fetchEvents();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedEvent) {
|
||||
fetchRegistrations(selectedEvent.id);
|
||||
}
|
||||
}, [selectedEvent]);
|
||||
|
||||
const fetchEvents = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const res = await fetch(API_BASE_URL + '/api/events');
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
// Faculty can manage their proposed events, Admins/HODs can manage all
|
||||
let filtered = data.filter((e: Event) => e.status === 'APPROVED' || e.status === 'COMPLETED' || e.status === 'Event Ongoing');
|
||||
if (user?.role === 'FACULTY') {
|
||||
filtered = filtered.filter((e: Event) => e.department === user.department);
|
||||
}
|
||||
setEvents(filtered);
|
||||
if (filtered.length > 0) {
|
||||
setSelectedEvent(filtered[0]);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchRegistrations = async (eventId: number) => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const [regsRes, attRes] = await Promise.all([
|
||||
fetch(API_BASE_URL + `/api/registrations?eventId=${eventId}`),
|
||||
fetch(API_BASE_URL + `/api/attendance?eventId=${eventId}`)
|
||||
]);
|
||||
|
||||
if (regsRes.ok && attRes.ok) {
|
||||
const regsData = await regsRes.json();
|
||||
const attData = await attRes.json();
|
||||
|
||||
setRegistrations(regsData);
|
||||
setAttendanceLogs(attData);
|
||||
|
||||
// Pre-populate attendance grid from attendanceLogs
|
||||
const grid: Record<string, Record<string, boolean>> = {};
|
||||
regsData.forEach((r: Registration) => {
|
||||
grid[r.id] = {};
|
||||
daysList.forEach(day => {
|
||||
const match = attData.find((a: any) => String(a.registrationId || a.registration_id) === String(r.id) && a.dayLabel === day);
|
||||
grid[r.id][day] = match ? !!match.isPresent : false;
|
||||
});
|
||||
});
|
||||
setAttendanceGrid(grid);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle Payment Status Manually
|
||||
const handleTogglePayment = async (reg: Registration) => {
|
||||
setIsProcessing(true);
|
||||
const newStatus = reg.paymentStatus === 'COMPLETED' ? 'PENDING' : 'COMPLETED';
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/registrations/${reg.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ paymentStatus: newStatus })
|
||||
});
|
||||
if (res.ok) {
|
||||
await fetchRegistrations(selectedEvent!.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Toggle Attendance Cell local state
|
||||
const handleToggleAttendance = (regId: string, day: string) => {
|
||||
setAttendanceGrid(prev => ({
|
||||
...prev,
|
||||
[regId]: {
|
||||
...prev[regId],
|
||||
[day]: !prev[regId][day]
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
// Save Attendance Grid to Firestore
|
||||
const handleSaveAttendance = async () => {
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
const payload: any[] = [];
|
||||
Object.keys(attendanceGrid).forEach(regId => {
|
||||
daysList.forEach(day => {
|
||||
payload.push({
|
||||
registrationId: regId,
|
||||
eventId: selectedEvent!.id,
|
||||
dayLabel: day,
|
||||
batchLabel: "Slot 1",
|
||||
isPresent: attendanceGrid[regId][day],
|
||||
date: new Date().toLocaleDateString()
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const res = await fetch(API_BASE_URL + '/api/attendance', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
alert("Attendance records synchronized successfully!");
|
||||
await fetchRegistrations(selectedEvent!.id);
|
||||
} else {
|
||||
alert("Failed to save attendance.");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle OD PDF File selection
|
||||
const handleODFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
setOdFileBase64(reader.result as string);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
// Upload OD File to all registered students
|
||||
const handleUploadODFile = async () => {
|
||||
if (!odFileBase64) return alert("Please select an On-Duty file first.");
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
// Loop over and update all registrations for this event
|
||||
for (const reg of registrations) {
|
||||
await fetch(`${API_BASE_URL}/api/registrations/${reg.id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ odUrl: odFileBase64 })
|
||||
});
|
||||
}
|
||||
alert("OD sheet broadcasted to all registered students successfully!");
|
||||
setOdFileBase64(null);
|
||||
await fetchRegistrations(selectedEvent!.id);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Approve or Reject screenshot proof
|
||||
const handleAuditCertificate = async (regId: string, status: 'APPROVED' | 'NOT_SUBMITTED') => {
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE_URL}/api/registrations/${regId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ certificationStatus: status })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
setInspectCertReg(null);
|
||||
alert(status === 'APPROVED' ? "Certificate approved!" : "Certificate rejected.");
|
||||
await fetchRegistrations(selectedEvent!.id);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
} finally {
|
||||
setIsProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredRoster = registrations.filter(r =>
|
||||
r.userName.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||
r.regNo.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in duration-300">
|
||||
{/* Header Info */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-6">
|
||||
<div>
|
||||
<h2 className="text-3xl font-black text-text-dark tracking-tight">Student Engagement Hub</h2>
|
||||
<p className="text-text-muted font-medium text-sm">Review registration rosters, log session attendance checklists, release OD files, and audit certificate proofs.</p>
|
||||
</div>
|
||||
|
||||
{/* Event Selector Dropdown */}
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="text-xs font-black text-slate-400 uppercase tracking-widest">Selected Event:</label>
|
||||
<select
|
||||
value={selectedEvent?.id || ''}
|
||||
onChange={(e) => {
|
||||
const ev = events.find(ev => ev.id === Number(e.target.value));
|
||||
if (ev) setSelectedEvent(ev);
|
||||
}}
|
||||
className="bg-white border border-slate-200 rounded-2xl py-3.5 px-5 text-xs font-black focus:outline-none focus:ring-2 focus:ring-brand-indigo/10 appearance-none cursor-pointer pr-10 shadow-sm"
|
||||
>
|
||||
{events.map(e => <option key={e.id} value={e.id}>{e.title}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedEvent ? (
|
||||
<div className="space-y-8">
|
||||
{/* Quick Metrics Panels */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
|
||||
<span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Passes Secured</span>
|
||||
<p className="text-2xl font-black text-slate-900">{registrations.length}</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
|
||||
<span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Teamed Up</span>
|
||||
<p className="text-2xl font-black text-slate-900">{registrations.filter(r => r.teamCode).length}</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
|
||||
<span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">In Review Proofs</span>
|
||||
<p className="text-2xl font-black text-amber-500">{registrations.filter(r => r.certificationStatus === 'PENDING_APPROVAL').length}</p>
|
||||
</div>
|
||||
<div className="bg-white p-6 rounded-[2rem] border border-slate-100 shadow-sm">
|
||||
<span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Verified Certificates</span>
|
||||
<p className="text-2xl font-black text-emerald-500">{registrations.filter(r => r.certificationStatus === 'APPROVED').length}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sub Navigation Tabs */}
|
||||
<div className="flex bg-slate-100 p-1 rounded-2xl gap-1 max-w-md">
|
||||
<button
|
||||
onClick={() => setActiveSubTab('roster')}
|
||||
className={cn("flex-1 py-3 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all", activeSubTab === 'roster' ? "bg-white text-brand-indigo shadow-sm" : "text-text-muted hover:text-text-dark")}
|
||||
>
|
||||
Roster & Fee
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveSubTab('attendance')}
|
||||
className={cn("flex-1 py-3 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all", activeSubTab === 'attendance' ? "bg-white text-brand-indigo shadow-sm" : "text-text-muted hover:text-text-dark")}
|
||||
>
|
||||
Attendance
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveSubTab('od')}
|
||||
className={cn("flex-1 py-3 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all", activeSubTab === 'od' ? "bg-white text-brand-indigo shadow-sm" : "text-text-muted hover:text-text-dark")}
|
||||
>
|
||||
On-Duty Release
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveSubTab('certificates')}
|
||||
className={cn("flex-1 py-3 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all", activeSubTab === 'certificates' ? "bg-white text-brand-indigo shadow-sm" : "text-text-muted hover:text-text-dark")}
|
||||
>
|
||||
Audits
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sub Tab Contents */}
|
||||
<div className="bg-white rounded-[2.5rem] border border-slate-100 shadow-sm p-8">
|
||||
|
||||
{/* SUB TAB 1: ROSTER & MANUAL FEE */}
|
||||
{activeSubTab === 'roster' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<h3 className="text-lg font-black text-slate-800 uppercase tracking-tight">Registration Roster</h3>
|
||||
|
||||
{/* Search Roster */}
|
||||
<div className="relative max-w-xs w-full">
|
||||
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search name or regNo..."
|
||||
className="w-full bg-slate-50 border border-slate-100 rounded-xl py-2.5 pl-10 pr-4 text-xs font-semibold focus:outline-none focus:bg-white focus:border-brand-indigo/20 transition-all"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs font-bold text-slate-500 uppercase tracking-widest">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-100 text-slate-400">
|
||||
<th className="py-4">Student Details</th>
|
||||
<th className="py-4">Class</th>
|
||||
<th className="py-4">College</th>
|
||||
<th className="py-4">Alliance/Team</th>
|
||||
<th className="py-4 text-center">Payment Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{filteredRoster.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-8 text-center text-slate-300 italic font-bold">No registered students matched the search.</td>
|
||||
</tr>
|
||||
) : (
|
||||
filteredRoster.map((reg) => (
|
||||
<tr key={reg.id} className="hover:bg-slate-50/50">
|
||||
<td className="py-4">
|
||||
<p className="text-sm font-black text-brand-navy">{reg.userName}</p>
|
||||
<p className="text-[10px] text-slate-400 mt-0.5">{reg.regNo} | {reg.phone}</p>
|
||||
</td>
|
||||
<td className="py-4 text-slate-600">
|
||||
{reg.year} Year / SEC {reg.section}
|
||||
<p className="text-[9px] text-brand-indigo mt-0.5">{reg.dept}</p>
|
||||
</td>
|
||||
<td className="py-4 text-slate-600 truncate max-w-[150px]">{reg.college}</td>
|
||||
<td className="py-4">
|
||||
{reg.teamCode ? (
|
||||
<div>
|
||||
<p className="text-slate-800 font-black text-[11px]">{reg.teamName}</p>
|
||||
<p className="text-[9px] text-slate-400">Code: {reg.teamCode}</p>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-300 font-normal">Individual</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="py-4 text-center">
|
||||
<button
|
||||
onClick={() => handleTogglePayment(reg)}
|
||||
disabled={isProcessing}
|
||||
className={cn(
|
||||
"px-3 py-1.5 rounded-lg text-[9px] font-black tracking-widest border transition-all",
|
||||
reg.paymentStatus === 'COMPLETED'
|
||||
? "bg-emerald-50 border-emerald-100 text-emerald-600 hover:bg-emerald-100"
|
||||
: "bg-red-50 border-red-100 text-red-600 hover:bg-red-100 animate-pulse"
|
||||
)}
|
||||
>
|
||||
{reg.paymentStatus}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SUB TAB 2: SESSION ATTENDANCE GRID */}
|
||||
{activeSubTab === 'attendance' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h3 className="text-lg font-black text-slate-800 uppercase tracking-tight">Mark Slot Presence</h3>
|
||||
<button
|
||||
onClick={handleSaveAttendance}
|
||||
disabled={isProcessing}
|
||||
className="px-6 py-2.5 bg-brand-navy text-white rounded-xl font-black text-[10px] uppercase tracking-widest hover:scale-[1.02] transition-all shadow-md"
|
||||
>
|
||||
{isProcessing ? 'Syncing...' : 'Save Attendance'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-xs font-bold text-slate-500 uppercase tracking-widest">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-100 text-slate-400">
|
||||
<th className="py-4">Student</th>
|
||||
<th className="py-4">Register No</th>
|
||||
{daysList.map(day => <th key={day} className="py-4 text-center">{day}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-50">
|
||||
{registrations.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="py-8 text-center text-slate-300 italic font-bold">No students registered yet.</td>
|
||||
</tr>
|
||||
) : (
|
||||
registrations.map((reg) => (
|
||||
<tr key={reg.id} className="hover:bg-slate-50/50">
|
||||
<td className="py-4 font-black text-brand-navy">{reg.userName}</td>
|
||||
<td className="py-4 text-slate-400">{reg.regNo}</td>
|
||||
{daysList.map(day => (
|
||||
<td key={day} className="py-4 text-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={attendanceGrid[reg.id]?.[day] || false}
|
||||
onChange={() => handleToggleAttendance(reg.id, day)}
|
||||
className="w-4.5 h-4.5 rounded border-slate-300 text-brand-indigo focus:ring-brand-indigo cursor-pointer"
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SUB TAB 3: ON-DUTY UPLOADER */}
|
||||
{activeSubTab === 'od' && (
|
||||
<div className="space-y-6 max-w-xl">
|
||||
<div>
|
||||
<h3 className="text-lg font-black text-slate-800 uppercase tracking-tight">On-Duty Official Release</h3>
|
||||
<p className="text-xs text-slate-400 font-semibold mt-1">Upload the unified signed On-Duty letter for this event. It will be released to all registered students automatically.</p>
|
||||
</div>
|
||||
|
||||
<div className="p-8 border-2 border-dashed border-slate-200 rounded-[2rem] text-center space-y-4">
|
||||
<Upload className="w-10 h-10 text-slate-300 mx-auto mb-2" />
|
||||
<div className="relative">
|
||||
<input
|
||||
type="file"
|
||||
accept="application/pdf,image/*"
|
||||
onChange={handleODFileChange}
|
||||
className="hidden"
|
||||
id="od-sheet-uploader"
|
||||
/>
|
||||
<label
|
||||
htmlFor="od-sheet-uploader"
|
||||
className="px-6 py-3 bg-slate-50 border border-slate-200 text-slate-600 rounded-xl font-black text-[10px] uppercase tracking-widest cursor-pointer hover:bg-slate-100 transition-all inline-block"
|
||||
>
|
||||
{odFileBase64 ? 'OD Document Loaded' : 'Choose OD Letter'}
|
||||
</label>
|
||||
</div>
|
||||
{odFileBase64 && (
|
||||
<p className="text-[10px] font-black text-emerald-500 uppercase tracking-wider">File captured ready to broadcast</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleUploadODFile}
|
||||
disabled={isProcessing || !odFileBase64}
|
||||
className="w-full py-4 bg-brand-indigo text-white rounded-xl font-black text-[10px] uppercase tracking-widest shadow-md shadow-brand-indigo/10 disabled:opacity-50"
|
||||
>
|
||||
{isProcessing ? 'Releasing ODs...' : 'Broadcast OD to All Students'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* SUB TAB 4: CERTIFICATE AUDITOR */}
|
||||
{activeSubTab === 'certificates' && (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-black text-slate-800 uppercase tracking-tight">Audit Student Proofs</h3>
|
||||
<p className="text-xs text-slate-400 font-semibold mt-1">Inspect uploaded completion screenshot proofs and approve them to unlock certificate downloads.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{registrations.filter(r => r.certificationStatus === 'PENDING_APPROVAL').length === 0 ? (
|
||||
<div className="col-span-2 py-16 bg-slate-50 rounded-3xl border border-dashed border-slate-200 text-center text-slate-400 font-bold uppercase tracking-wider text-xs">
|
||||
<FileCheck className="w-12 h-12 text-slate-300 mx-auto mb-4 opacity-50" />
|
||||
All certificate proofs have been successfully audited!
|
||||
</div>
|
||||
) : (
|
||||
registrations.filter(r => r.certificationStatus === 'PENDING_APPROVAL').map((reg) => (
|
||||
<div
|
||||
key={reg.id}
|
||||
className="bg-slate-50 p-6 rounded-3xl border border-slate-100 flex items-center justify-between hover:border-slate-200 transition-all"
|
||||
>
|
||||
<div>
|
||||
<p className="text-sm font-black text-slate-800 uppercase">{reg.userName}</p>
|
||||
<p className="text-[9px] text-slate-400 font-bold uppercase tracking-wider mt-0.5">{reg.regNo} | {reg.dept}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setInspectCertReg(reg)}
|
||||
className="px-4 py-2.5 bg-brand-glow text-brand-indigo rounded-xl font-black text-[10px] uppercase tracking-widest flex items-center gap-1.5 shadow-sm border border-brand-indigo/5"
|
||||
>
|
||||
<Eye className="w-4 h-4" /> Inspect Proof
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-[2.5rem] p-20 text-center border border-slate-100 shadow-sm">
|
||||
<Users className="w-16 h-16 text-slate-300 mx-auto mb-6 opacity-20" />
|
||||
<h3 className="text-xl font-black text-slate-800 mb-2">No Live Events Available</h3>
|
||||
<p className="text-slate-400 font-medium text-sm">Create/approve events in your main dashboard first to access student hub audits.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* INSPECT SCREENSHOT MODAL */}
|
||||
{inspectCertReg && (
|
||||
<div className="fixed inset-0 z-[1000] flex items-center justify-center p-6 bg-slate-900/60 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-[3rem] w-full max-w-md overflow-hidden shadow-2xl relative border border-slate-100 flex flex-col justify-between max-h-[85vh]">
|
||||
<div className="p-6 bg-slate-900 text-white flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-lg font-black uppercase leading-none">Inspect Screenshot</h3>
|
||||
<p className="text-[9px] font-bold text-slate-400 uppercase tracking-widest mt-1">{inspectCertReg.userName} | {inspectCertReg.regNo}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setInspectCertReg(null)}
|
||||
className="p-1.5 hover:bg-white/10 rounded-lg text-white"
|
||||
>
|
||||
<XCircle className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Proof Body */}
|
||||
<div className="p-8 overflow-y-auto max-h-[50vh] flex flex-col items-center">
|
||||
{inspectCertReg.certificationUrl ? (
|
||||
<img
|
||||
src={inspectCertReg.certificationUrl}
|
||||
alt="Completion proof"
|
||||
className="w-full object-contain rounded-2xl border border-slate-200 shadow-sm"
|
||||
/>
|
||||
) : (
|
||||
<p className="text-xs text-slate-400 font-bold uppercase tracking-widest">No proof uploaded</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions Bar */}
|
||||
<div className="p-6 bg-slate-50 border-t border-slate-100 flex gap-4">
|
||||
<button
|
||||
onClick={() => handleAuditCertificate(inspectCertReg.id, 'NOT_SUBMITTED')}
|
||||
disabled={isProcessing}
|
||||
className="flex-1 py-3.5 bg-red-50 text-red-600 rounded-xl font-black text-[10px] uppercase tracking-widest border border-red-100 hover:bg-red-100"
|
||||
>
|
||||
Reject Proof
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleAuditCertificate(inspectCertReg.id, 'APPROVED')}
|
||||
disabled={isProcessing}
|
||||
className="flex-1 py-3.5 bg-emerald-500 text-white rounded-xl font-black text-[10px] uppercase tracking-widest shadow-md shadow-emerald-100 hover:bg-emerald-600"
|
||||
>
|
||||
Approve & Unlock Cert
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user