Files
Event-Management-System/RIT-EVENT-MANAGEMENT--main/components/AdminEventStatusView.tsx

429 lines
21 KiB
TypeScript

import React, { useState } from 'react';
import { createPortal } from 'react-dom';
import { Event, Announcement } from '../types';
interface AdminEventStatusViewProps {
events: Event[];
announcements: Announcement[];
onAddAnnouncement: (ann: Announcement) => Promise<void>;
onUpdateAnnouncement?: (ann: Announcement) => Promise<void>;
onDeleteAnnouncement: (id: string) => Promise<void>;
onUpdateEvent: (event: Event) => void;
onBack: () => void;
currentUserId?: string;
localRegistrations?: any[];
}
type ExpiryOption = '1H' | '6H' | '24H' | 'NEVER' | 'CUSTOM';
const AdminEventStatusView: React.FC<AdminEventStatusViewProps> = ({
events,
announcements,
onAddAnnouncement,
onUpdateAnnouncement,
onDeleteAnnouncement,
onUpdateEvent,
onBack,
currentUserId,
localRegistrations = []
}) => {
const [selectedEventId, setSelectedEventId] = useState('');
const [message, setMessage] = useState('');
const [type, setType] = useState<Announcement['type']>('INFO');
const [expiryOption, setExpiryOption] = useState<ExpiryOption>('NEVER');
const [customExpiry, setCustomExpiry] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [deletingId, setDeletingId] = useState<string | null>(null);
const [editingId, setEditingId] = useState<string | null>(null);
const [showOverwriteConfirm, setShowOverwriteConfirm] = useState<Announcement | null>(null);
const [confirmDeleteId, setConfirmDeleteId] = useState<string | null>(null);
const calculateExpiry = (): string | null => {
const now = new Date();
switch (expiryOption) {
case '1H': return new Date(now.getTime() + 60 * 60 * 1000).toISOString();
case '6H': return new Date(now.getTime() + 6 * 60 * 60 * 1000).toISOString();
case '24H': return new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString();
case 'CUSTOM': return customExpiry ? new Date(customExpiry).toISOString() : null;
default: return null;
}
};
const handleEdit = (ann: Announcement) => {
setEditingId(ann.id);
setSelectedEventId(ann.eventId || '');
const cleanMessage = ann.message.includes(']') ? ann.message.split(']').slice(1).join(']').trim() : ann.message;
setMessage(cleanMessage);
setType(ann.type);
setExpiryOption(ann.expiresAt ? 'CUSTOM' : 'NEVER');
if (ann.expiresAt) setCustomExpiry(new Date(ann.expiresAt).toISOString().slice(0, 16));
window.scrollTo({ top: 0, behavior: 'smooth' });
};
const cancelEdit = () => {
setEditingId(null);
setMessage('');
setSelectedEventId('');
setExpiryOption('NEVER');
setCustomExpiry('');
};
const processDelete = async () => {
if (!confirmDeleteId) return;
const id = confirmDeleteId;
setConfirmDeleteId(null);
setDeletingId(id);
try {
await onDeleteAnnouncement(id);
} catch (err: any) {
console.error("UI Delete Trigger Error:", err);
alert("Database failed to delete. Check console for error.");
} finally {
setDeletingId(null);
}
};
const executeSubmit = async (overrideId?: string) => {
setIsSubmitting(true);
try {
const selectedEvent = events.find(ev => ev.id === selectedEventId);
const expiresAt = calculateExpiry();
const targetId = overrideId || editingId;
const announcementPayload: Announcement = {
id: targetId || '',
title: type === 'DELAY' ? 'DELAY ALERT' : type === 'URGENT' ? 'URGENT' : type === 'ENDED' ? 'CONCLUDED' : type === 'ONGOING' ? 'LIVE NOW' : 'NOTICE',
message: selectedEvent ? `[${selectedEvent.title}] ${message}` : message,
type,
timestamp: new Date().toISOString(),
expiresAt: expiresAt,
eventId: selectedEventId || undefined
};
if (targetId && onUpdateAnnouncement) {
await onUpdateAnnouncement(announcementPayload);
} else {
await onAddAnnouncement(announcementPayload);
}
if (type === 'ENDED' && selectedEvent) {
onUpdateEvent({ ...selectedEvent, status: 'Completed' });
} else if (type === 'ONGOING' && selectedEvent) {
onUpdateEvent({ ...selectedEvent, status: 'Event Ongoing' });
}
cancelEdit();
setShowOverwriteConfirm(null);
} catch (err) {
console.error("Broadcast operation failed:", err);
} finally {
setIsSubmitting(false);
}
};
const handleDownloadExcel = () => {
if (!selectedEventId || selectedEventId === 'GENERAL') return;
const event = events.find(e => e.id === selectedEventId);
if (!event) return;
const filteredRegs = localRegistrations.filter(reg => String(reg.event_id) === String(selectedEventId));
if (filteredRegs.length === 0) {
alert("No participants found for this event.");
return;
}
const headers = ['Name', 'Registration No', 'Department', 'Year', 'Email', 'Phone', 'College'];
const rows = filteredRegs.map(r => [
r.user_name || 'N/A',
`\t${r.reg_no || 'N/A'}`,
r.dept || 'N/A',
r.year || 'N/A',
r.email || 'N/A',
r.phone || 'N/A',
r.college || 'RAJALAKSHMI INSTITUTE OF TECHNOLOGY'
]);
const csvContent = [
[`EVENT: ${event.title}`].join(','),
[`EXPORTED AT: ${new Date().toLocaleString()}`].join(','),
[''],
headers.join(','),
...rows.map(row => row.map(cell => `"${cell}"`).join(','))
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.setAttribute('download', `${event.title.replace(/[^a-z0-9]/gi, '_').toLowerCase()}_participants.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const handlePublish = async () => {
if (!message.trim() || !selectedEventId) return;
if (selectedEventId !== 'GENERAL') {
const event = events.find(e => e.id === selectedEventId);
if (!event || event.created_by !== currentUserId) {
alert("Unauthorized: Only the creator can broadcast for this event.");
return;
}
}
executeSubmit();
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!message) return;
if ((type === 'ENDED' || type === 'ONGOING') && selectedEventId && !editingId) {
const existingStatusNotice = announcements.find(a => a.eventId === selectedEventId && a.type === type);
if (existingStatusNotice) {
setShowOverwriteConfirm(existingStatusNotice);
return;
}
}
handlePublish();
};
const getStatusColor = (t: Announcement['type']) => {
switch (t) {
case 'URGENT': return 'border-rose-500/30 text-rose-500 bg-rose-500/5';
case 'DELAY': return 'border-amber-500/30 text-amber-500 bg-amber-500/5';
case 'ENDED': return 'border-emerald-500/30 text-emerald-500 bg-emerald-500/5';
case 'ONGOING': return 'border-purple-500/30 text-purple-500 bg-purple-500/5';
default: return 'border-blue-500/30 text-blue-500 bg-blue-500/5';
}
};
return (
<div className="max-w-7xl mx-auto py-10 px-6 animate-in fade-in slide-in-from-bottom-10 duration-500">
{/* Overwrite Confirmation Modal */}
{showOverwriteConfirm && createPortal(
<div className="fixed inset-0 z-[10001] flex items-center justify-center p-6 bg-slate-900/40 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white border border-slate-200 rounded-[3.5rem] w-full max-w-md p-14 shadow-2xl flex flex-col items-center text-center animate-in zoom-in-95 duration-500">
<div className="w-24 h-24 bg-amber-50 text-amber-500 rounded-full flex items-center justify-center text-4xl mb-8 border border-amber-100">
<i className="fas fa-exclamation-triangle"></i>
</div>
<h3 className="text-3xl font-black text-slate-900 mb-3 uppercase tracking-tighter">Replace Previous Log?</h3>
<p className="text-slate-500 font-bold text-[10px] uppercase tracking-[0.2em] mb-12 leading-relaxed">
A conclusion log already exists for this scope.<br />
Replacing it will overwrite the history entry.
</p>
<div className="flex gap-4 w-full">
<button onClick={() => setShowOverwriteConfirm(null)} className="flex-1 py-5 bg-slate-50 text-slate-500 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-slate-100 hover:text-slate-900 transition-all border border-slate-200">Dismiss</button>
<button onClick={() => executeSubmit(showOverwriteConfirm.id)} className="flex-1 py-5 bg-amber-500 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-amber-600 transition-all shadow-xl shadow-amber-500/20">Overwrite</button>
</div>
</div>
</div>,
document.body
)}
{/* Delete Confirmation Modal */}
{confirmDeleteId && createPortal(
<div className="fixed inset-0 z-[10001] flex items-center justify-center p-6 bg-slate-900/40 backdrop-blur-sm animate-in fade-in duration-300">
<div className="bg-white border border-slate-200 rounded-[3.5rem] w-full max-w-md p-14 shadow-2xl flex flex-col items-center text-center animate-in zoom-in-95 duration-500">
<div className="w-24 h-24 bg-rose-50 text-rose-500 rounded-full flex items-center justify-center text-4xl mb-8 border border-rose-100">
<i className="fas fa-trash-can"></i>
</div>
<h3 className="text-3xl font-black text-slate-900 mb-3 uppercase tracking-tighter">Remove Broadcast?</h3>
<p className="text-slate-500 font-bold text-[10px] uppercase tracking-[0.2em] mb-12 leading-relaxed">
This action is permanent and cannot be reversed.
</p>
<div className="flex gap-4 w-full">
<button onClick={() => setConfirmDeleteId(null)} className="flex-1 py-5 bg-slate-50 text-slate-500 rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-slate-100 hover:text-slate-900 transition-all border border-slate-200">Cancel</button>
<button onClick={processDelete} className="flex-1 py-5 bg-rose-600 text-white rounded-2xl font-black uppercase text-[10px] tracking-widest hover:bg-rose-700 transition-all shadow-xl">Delete Now</button>
</div>
</div>
</div>,
document.body
)}
<div className="flex items-center justify-between mb-16 border-b border-slate-200 pb-8">
<div>
<h2 className="text-5xl font-black text-slate-900 tracking-tighter uppercase">ADMIN <span className="text-[#004a99]">PULSE</span></h2>
<p className="text-[10px] font-black text-slate-500 uppercase tracking-[0.4em] mt-3 ml-1">Live Management of System Notifications</p>
</div>
<button onClick={onBack} className="group flex items-center gap-3 text-slate-400 hover:text-[#004a99] transition-all text-xs font-black uppercase tracking-widest bg-white border border-slate-200 px-6 py-3 rounded-full shadow-sm">
<i className="fas fa-arrow-left transition-transform group-hover:-translate-x-1"></i> BACK
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12 items-start">
{/* Left Side: Creation Form */}
<div className="lg:col-span-4 lg:sticky lg:top-24">
<form onSubmit={handleSubmit} className="bg-white border border-slate-200 rounded-[3rem] p-10 space-y-8 shadow-sm">
<div className="flex justify-between items-center">
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em]">{editingId ? 'Refining Broadcast' : 'New Broadcast'}</h3>
{editingId && (
<button type="button" onClick={cancelEdit} className="text-[9px] text-rose-500 font-black uppercase hover:underline">Cancel Edit</button>
)}
</div>
<div>
<label className="block text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-3 ml-1">Target Scope</label>
<div className="flex gap-2">
<select
className="flex-1 bg-white border border-slate-200 rounded-xl px-5 py-4 text-slate-900 focus:ring-2 focus:ring-[#004a99] outline-none font-bold text-xs appearance-none shadow-sm cursor-pointer"
value={selectedEventId}
onChange={(e) => setSelectedEventId(e.target.value)}
>
<option value="">General Notice</option>
{events.map(ev => (
<option key={ev.id} value={ev.id} style={{ color: ev.created_by !== currentUserId ? '#94a3b8' : 'inherit' }}>
{ev.title} {ev.created_by !== currentUserId ? "(READ ONLY)" : ""}
</option>
))}
</select>
{selectedEventId && selectedEventId !== 'GENERAL' && (
<button
type="button"
onClick={handleDownloadExcel}
className="w-14 h-14 bg-emerald-50 border border-emerald-100 text-emerald-600 rounded-xl flex items-center justify-center hover:bg-emerald-600 hover:text-white transition-all shadow-sm active:scale-95"
title="Download Roster"
>
<i className="fas fa-file-csv"></i>
</button>
)}
</div>
</div>
<div>
<label className="block text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-3 ml-1">Broadcast Type</label>
<div className="grid grid-cols-2 gap-3">
{(['DELAY', 'INFO', 'URGENT', 'ENDED', 'ONGOING'] as Announcement['type'][]).map(t => (
<button
key={t}
type="button"
onClick={() => setType(t)}
className={`py-4 rounded-xl text-[9px] font-black uppercase tracking-widest transition-all border ${type === t ? 'bg-[#004a99] border-[#004a99] text-white shadow-md shadow-[#004a99]/20' : 'bg-slate-50 border-slate-200 text-slate-500 hover:text-slate-900 hover:border-slate-300'}`}
>
{t}
</button>
))}
</div>
</div>
<div className="pt-2">
<label className="block text-[9px] font-black text-slate-500 uppercase tracking-[0.2em] mb-4 ml-1">Auto-Expiry</label>
<div className="grid grid-cols-3 gap-2 mb-4">
{(['1H', 'NEVER', 'CUSTOM'] as ExpiryOption[]).map(opt => (
<button
key={opt}
type="button"
onClick={() => setExpiryOption(opt)}
className={`py-3 rounded-xl text-[7px] font-black uppercase tracking-widest transition-all border ${expiryOption === opt ? 'bg-blue-50 border-blue-200 text-[#004a99]' : 'bg-transparent border-slate-200 text-slate-500 hover:bg-slate-50'}`}
>
{opt}
</button>
))}
</div>
{expiryOption === 'CUSTOM' && (
<input
type="datetime-local"
className="w-full bg-white border border-slate-200 rounded-xl px-5 py-4 text-slate-900 focus:ring-2 focus:ring-[#004a99] outline-none text-[10px] font-bold shadow-sm"
value={customExpiry}
onChange={(e) => setCustomExpiry(e.target.value)}
required
/>
)}
</div>
<textarea
required
placeholder="Broadcast message..."
className="w-full bg-white border border-slate-200 rounded-xl px-6 py-5 text-slate-900 focus:ring-2 focus:ring-[#004a99] outline-none text-xs h-40 resize-none font-medium placeholder:text-slate-400 shadow-sm"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
<button
type="submit"
disabled={isSubmitting}
className={`w-full py-5 rounded-xl font-black uppercase text-[10px] tracking-[0.3em] transition-all disabled:opacity-50 shadow-md hover:shadow-lg active:scale-95 ${editingId ? 'bg-blue-100 text-blue-800 hover:bg-blue-200 border border-blue-200' : 'bg-[#004a99] text-white hover:bg-blue-800'} `}
>
{isSubmitting ? <><i className="fas fa-spinner fa-spin mr-2"></i> SYNCING...</> : editingId ? 'UPDATE HISTORY' : 'PUBLISH NOTICE'}
</button>
</form>
</div>
{/* Right Side: Live Stream History */}
<div className="lg:col-span-8 bg-white border border-slate-200 rounded-[3.5rem] p-10 min-h-[700px] flex flex-col shadow-sm">
<div className="flex items-center justify-between border-b border-slate-100 pb-10 mb-8">
<div>
<h3 className="text-2xl font-black uppercase tracking-tighter text-slate-900">Live Stream History</h3>
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest mt-2">Real-time Authenticated Activity Feed</p>
</div>
<div className="flex items-center gap-6">
<div className="flex flex-col items-end">
<span className="text-[10px] font-black text-[#004a99] uppercase tracking-widest">{announcements.length} ACTIVE BROADCASTS</span>
<div className="w-12 h-0.5 bg-blue-200 mt-1"></div>
</div>
</div>
</div>
<div className="flex-1 space-y-6 overflow-y-auto no-scrollbar pr-2 pb-10">
{announcements.length > 0 ? announcements.map((ann) => (
<div
key={ann.id}
className={`bg-white border rounded-[2.5rem] p-8 flex items-start justify-between group transition-all duration-500 hover:shadow-md ${editingId === ann.id ? 'border-blue-500 ring-1 ring-blue-500/20 bg-blue-50/50' : 'border-slate-200 hover:border-slate-300'
}`}
>
<div className="flex-1 pr-10 text-left">
<div className="flex items-center gap-4 mb-4">
<span className={`px-4 py-1 rounded-full text-[9px] font-black uppercase tracking-widest border ${getStatusColor(ann.type)}`}>
{ann.type}
</span>
<h4 className="text-base font-black text-slate-900 uppercase tracking-tight">{ann.title}</h4>
</div>
<p className="text-sm text-slate-600 font-medium leading-relaxed mb-8 max-w-2xl">{ann.message}</p>
<div className="flex flex-wrap items-center gap-8">
<div className="flex items-center gap-2 text-slate-500">
<i className="far fa-clock text-[10px]"></i>
<span className="text-[10px] font-black uppercase tracking-widest">
Logged: {new Date(ann.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-4 self-center relative z-20">
<button
type="button"
onClick={(e) => { e.preventDefault(); e.stopPropagation(); handleEdit(ann); }}
disabled={deletingId === ann.id}
className="w-14 h-14 rounded-2xl bg-slate-50 flex items-center justify-center text-slate-400 hover:text-[#004a99] hover:bg-blue-50 transition-all border border-slate-200 hover:border-blue-200 disabled:opacity-30 cursor-pointer pointer-events-auto"
>
<i className="fas fa-pen-nib text-sm"></i>
</button>
<button
type="button"
onClick={(e) => { e.preventDefault(); e.stopPropagation(); setConfirmDeleteId(ann.id); }}
disabled={deletingId === ann.id}
className="w-14 h-14 rounded-2xl bg-slate-50 flex items-center justify-center text-slate-400 hover:text-rose-500 hover:bg-rose-50 transition-all border border-slate-200 hover:border-rose-200 disabled:opacity-30 cursor-pointer pointer-events-auto"
>
{deletingId === ann.id ? <i className="fas fa-spinner fa-spin text-sm"></i> : <i className="fas fa-trash-can text-sm"></i>}
</button>
</div>
</div>
)) : (
<div className="flex flex-col items-center justify-center py-40 text-center animate-in fade-in duration-500 bg-slate-50 border border-dashed border-slate-200 rounded-[3rem]">
<div className="w-24 h-24 rounded-full bg-white border border-slate-200 flex items-center justify-center mb-8 shadow-sm text-slate-300">
<i className="fas fa-stream text-4xl"></i>
</div>
<h4 className="text-2xl font-black uppercase tracking-tighter text-slate-400">History Empty</h4>
<p className="text-[10px] font-black text-slate-500 uppercase tracking-widest mt-3">Ready for local broadcast input</p>
</div>
)}
</div>
</div>
</div>
</div>
);
};
export default AdminEventStatusView;