Files
Event-Management-System/frontend/src/components/dashboard/ApprovalsView.tsx
2026-06-23 09:40:34 +05:30

669 lines
33 KiB
TypeScript

import { API_BASE_URL } from '../../lib/config';
import React, { useEffect, useState, useMemo } from 'react';
import { motion, AnimatePresence } from 'framer-motion';
import {
CheckCircle,
XCircle,
Clock,
User,
Building2,
ChevronRight,
AlertCircle,
ShieldCheck,
MapPin,
Wallet,
Users,
Layers,
Ticket,
AlertTriangle,
FileText,
Heart,
Calendar
} from 'lucide-react';
import { useAuth } from '../../context/AuthContext';
import { cn } from '../../lib/utils';
import { Pagination } from './Pagination';
interface Event {
id: number;
title: string;
startDate: string;
endDate: string;
type: string;
institution: string;
department: string;
academicYears: string[];
status: string;
location: string;
proposer?: {
fullName: string;
department: string;
};
budget: number;
hasRegistrationFee: boolean;
registrationFee: number;
category: string;
targetedSections: string[];
conflictMessage?: string;
description?: string;
sponsors?: string[];
rejectionReason?: string;
dayConfigs?: any[];
deptLimits?: Record<string, number>;
deptSectionLimits?: Record<string, Record<string, number>>;
refreshment_expense?: number;
transportation_expense?: number;
session_coverage_fee?: number;
total_expense?: number;
durationDays?: number;
}
export const ApprovalsView: React.FC = () => {
const { user } = useAuth();
const [events, setEvents] = useState<Event[]>([]);
const [allEvents, setAllEvents] = useState<Event[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [processingId, setProcessingId] = useState<number | null>(null);
const [selectedEvent, setSelectedEvent] = useState<Event | null>(null);
const [selectedVenue, setSelectedVenue] = useState<string>('');
const [currentPage, setCurrentPage] = useState(1);
const [itemsPerPage, setItemsPerPage] = useState(5);
const [dbVenues, setDbVenues] = useState<string[]>([
'GB 4th floor auditorium',
'Wozniak Auditorium',
'C6-02 Indoor Theatre',
'H Block Guest Lecture Theatre',
'Steve Jobs Computer Centre 1',
'Steve Jobs Computer Centre 2'
]);
useEffect(() => {
fetchEvents();
fetchVenues();
}, []);
const fetchVenues = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/venues');
if (response.ok) {
const data = await response.json();
setDbVenues(data.map((v: any) => v.name));
}
} catch (error) {
console.error('Failed to fetch venues for approvals:', error);
}
};
const fetchEvents = async () => {
try {
const response = await fetch(API_BASE_URL + '/api/events');
if (response.ok) {
const data = await response.json();
setAllEvents(data);
// Filter based on role
if (user?.role === 'HOD') {
const userDepts = user.department ? user.department.split(',').map(d => d.trim().toLowerCase()) : [];
setEvents(data.filter((e: Event) => e.status === 'REQUESTED' && userDepts.includes(e.department?.trim().toLowerCase())));
} else if (user?.role === 'PRINCIPAL') {
setEvents(data.filter((e: Event) => e.status === 'PENDING_PR'));
}
}
} catch (error) {
console.error('Failed to fetch events:', error);
} finally {
setIsLoading(false);
}
};
const handleSelectEvent = (event: Event | null) => {
setSelectedEvent(event);
setSelectedVenue('');
};
const handleAction = async (id: number, action: 'approve' | 'reject', allocatedVenue?: string) => {
let reason = '';
if (action === 'reject') {
reason = window.prompt('Please enter a rejection reason:') || '';
if (!reason) return;
}
setProcessingId(id);
try {
let url = `${API_BASE_URL}/api/events/${id}/${action}?userId=${user?.id}`;
if (allocatedVenue) {
url += `&newVenue=${encodeURIComponent(allocatedVenue)}`;
}
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: action === 'reject' ? JSON.stringify({ reason }) : undefined
});
if (response.ok) {
setEvents(events.filter(e => e.id !== id));
handleSelectEvent(null);
// Refresh all events to reflect the changes
fetchEvents();
} else {
const err = await response.json();
alert(err.message || 'Operation failed');
}
} catch (error) {
console.error(`Failed to ${action} event:`, error);
} finally {
setProcessingId(null);
}
};
const getAvailableVenues = (event: Event) => {
const start = new Date(event.startDate);
const end = new Date(event.endDate);
const busyVenues = new Set<string>();
allEvents.forEach(other => {
if (
other.id === event.id ||
other.status === 'CANCELLED' ||
other.status === 'HOD_REJECTED' ||
other.status === 'PRINCIPAL_REJECTED'
) return;
const oStart = new Date(other.startDate);
const oEnd = new Date(other.endDate);
if (start < oEnd && end > oStart) {
if (other.location) {
busyVenues.add(other.location.trim().toLowerCase());
}
}
});
return dbVenues.filter(v => !busyVenues.has(v.trim().toLowerCase()));
};
const sortedEvents = useMemo(() => {
return [...events].sort((a: any, b: any) => {
const dateA = new Date(a.updatedAt || a.createdAt || 0).getTime();
const dateB = new Date(b.updatedAt || b.createdAt || 0).getTime();
return dateB - dateA;
});
}, [events]);
const currentEvents = sortedEvents.slice(
(currentPage - 1) * itemsPerPage,
currentPage * itemsPerPage
);
return (
<div className="space-y-8">
<div>
<h2 className="text-3xl font-black text-text-dark tracking-tight">Pending Approvals</h2>
<p className="text-text-muted font-medium">Review and take action on event proposals from your {user?.role === 'HOD' ? 'department' : 'institution'}.</p>
</div>
<div className="space-y-4">
{isLoading ? (
<div className="flex items-center justify-center p-20">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-brand-indigo"></div>
</div>
) : events.length === 0 ? (
<div className="bg-white rounded-[2.5rem] p-20 text-center border border-slate-100 premium-shadow">
<CheckCircle className="w-16 h-16 text-emerald-500 mx-auto mb-6 opacity-20" />
<h3 className="text-xl font-black text-text-dark mb-2">Queue is Empty</h3>
<p className="text-text-muted italic">All proposals have been processed. Great work!</p>
</div>
) : (
<AnimatePresence mode="popLayout">
{currentEvents.map((event) => (
<motion.div
key={event.id}
layout
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.95 }}
onClick={() => handleSelectEvent(event)}
className={cn(
"bg-white rounded-[2rem] p-6 border transition-all flex flex-col md:flex-row items-center justify-between gap-6 cursor-pointer group",
event.conflictMessage ? "border-red-200 bg-red-50/30" : "border-slate-100 hover:premium-shadow"
)}
>
<div className="flex items-center gap-6 w-full md:w-auto">
<div className={cn(
"w-16 h-16 rounded-2xl flex items-center justify-center font-black text-xl shadow-lg",
event.conflictMessage ? "bg-red-500 text-white" : "bg-brand-glow text-brand-indigo"
)}>
{event.title[0]}
</div>
<div>
<div className="flex items-center gap-2 mb-1">
{event.category === 'CLUB' && (
<span className="px-2 py-0.5 bg-emerald-50 text-emerald-600 rounded-md text-[8px] font-black uppercase tracking-widest border border-emerald-100">
Institutional Club
</span>
)}
<span className="text-[10px] font-black uppercase tracking-widest text-slate-400">
{event.type}
</span>
</div>
<h3 className="text-xl font-black text-text-dark leading-tight mb-2 group-hover:text-brand-indigo transition-colors">{event.title}</h3>
{event.conflictMessage ? (
<div className="flex items-center gap-2 text-red-500 text-[10px] font-black uppercase tracking-widest mt-1">
<AlertTriangle className="w-3.5 h-3.5" />
{event.conflictMessage}
</div>
) : (
<div className="flex items-center justify-center md:justify-start gap-4 text-xs font-bold text-text-muted">
<div className="flex items-center gap-1.5">
<User className="w-3.5 h-3.5" />
{event.proposer?.fullName || 'Faculty Member'}
</div>
<div className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5" />
{new Date(event.startDate).toLocaleDateString()}
</div>
</div>
)}
</div>
</div>
<div className="flex items-center gap-3 w-full md:w-auto" onClick={(e) => e.stopPropagation()}>
<button
onClick={() => handleAction(event.id, 'reject')}
disabled={processingId === event.id}
className="flex-1 md:flex-none flex items-center justify-center gap-2 px-6 py-3 rounded-xl bg-red-50 text-red-600 hover:bg-red-100 transition-all font-black text-[10px] uppercase tracking-widest border border-red-100/50"
>
<XCircle className="w-4 h-4" />
Reject
</button>
<button
onClick={() => handleAction(event.id, 'approve')}
disabled={processingId === event.id || !!event.conflictMessage}
className={cn(
"flex-1 md:flex-none flex items-center justify-center gap-2 px-8 py-3 rounded-xl transition-all font-black text-[10px] uppercase tracking-widest premium-shadow",
event.conflictMessage
? "bg-slate-200 text-slate-400 cursor-not-allowed"
: "bg-brand-navy text-white hover:scale-105"
)}
>
{processingId === event.id ? 'Processing...' : (
<>
<CheckCircle className="w-4 h-4" />
Approve
</>
)}
</button>
</div>
</motion.div>
))}
</AnimatePresence>
)}
</div>
{events.length > 0 && (
<Pagination
currentPage={currentPage}
totalItems={events.length}
itemsPerPage={itemsPerPage}
onPageChange={setCurrentPage}
onItemsPerPageChange={(val) => {
setItemsPerPage(val);
setCurrentPage(1);
}}
itemsPerPageOptions={[5, 10, 15, 20]}
/>
)}
{/* Detail Modal */}
<AnimatePresence>
{selectedEvent && (
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedEvent(null)}
className="absolute inset-0 bg-slate-900/40 backdrop-blur-sm"
/>
<motion.div
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
className="relative w-full max-w-2xl bg-white rounded-[2.5rem] premium-shadow overflow-hidden max-h-[90vh] overflow-y-auto"
>
<div className={cn(
"p-8 text-white flex justify-between items-start",
selectedEvent.conflictMessage ? "bg-red-600" : "bg-brand-navy"
)}>
<div>
<div className="flex items-center gap-2 mb-2">
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-white/60">Proposal Details</span>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-white/60">{selectedEvent.institution}</span>
{selectedEvent.category === 'CLUB' && (
<>
<span className="w-1 h-1 bg-white/30 rounded-full" />
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-emerald-400">Institutional Club Event</span>
</>
)}
</div>
<h3 className="text-2xl font-black tracking-tight">{selectedEvent.title}</h3>
</div>
<button
onClick={() => handleSelectEvent(null)}
className="p-2 hover:bg-white/10 rounded-xl transition-all"
>
<XCircle className="w-6 h-6" />
</button>
</div>
<div className="p-8 space-y-8">
{/* Description Section */}
{selectedEvent.description && (
<div className="space-y-2">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<FileText className="w-3.5 h-3.5" />
Event Description
</div>
<p className="text-sm font-medium text-text-dark leading-relaxed bg-slate-50 p-4 rounded-2xl border border-slate-100">
{selectedEvent.description}
</p>
</div>
)}
<div className="grid grid-cols-2 gap-8">
<div className="space-y-6">
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Calendar className="w-3.5 h-3.5" />
Schedule
</div>
<p className="text-sm font-bold text-text-dark">
{new Date(selectedEvent.startDate).toLocaleString()}
</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<MapPin className="w-3.5 h-3.5" />
Venue
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.location}</p>
</div>
</div>
<div className="space-y-6">
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<User className="w-3.5 h-3.5" />
Proposed By
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.proposer?.fullName || 'Faculty'}</p>
<p className="text-[10px] font-black text-brand-indigo uppercase">{selectedEvent.department}</p>
</div>
<div className="space-y-1">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Users className="w-3.5 h-3.5" />
Target Audience
</div>
<p className="text-sm font-bold text-text-dark">{selectedEvent.academicYears.join(', ')} Batches</p>
</div>
</div>
</div>
{/* Sponsors Section */}
{selectedEvent.sponsors && selectedEvent.sponsors.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Heart className="w-3.5 h-3.5 text-brand-indigo" />
Event Sponsors
</div>
<div className="flex flex-wrap gap-2">
{selectedEvent.sponsors.map((sponsor, idx) => (
<span key={idx} className="px-3 py-1 bg-brand-glow text-brand-indigo rounded-lg text-[10px] font-black uppercase tracking-widest border border-brand-indigo/10">
{sponsor}
</span>
))}
</div>
</div>
)}
{/* Itinerary & Day Configs */}
{selectedEvent.dayConfigs && selectedEvent.dayConfigs.length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Calendar className="w-3.5 h-3.5 text-brand-indigo" />
Detailed Itinerary & Schedule
</div>
<div className="space-y-4">
{selectedEvent.dayConfigs.map((day: any, dIdx: number) => {
const dayDate = day.date ? new Date(day.date) : null;
return (
<div key={dIdx} className="p-4 rounded-2xl bg-slate-50 border border-slate-100 space-y-3">
<div className="flex justify-between items-center border-b border-slate-200 pb-1.5">
<span className="text-[11px] font-black text-brand-navy uppercase tracking-wider">
Day {dIdx + 1}: {dayDate && !isNaN(dayDate.getTime()) ? dayDate.toLocaleDateString() : day.date || 'TBD'}
</span>
<span className="px-2 py-0.5 bg-brand-glow text-brand-indigo text-[8px] font-black uppercase tracking-widest rounded">
{day.batches?.length || 0} {(day.batches?.length || 0) === 1 ? 'Batch' : 'Batches'}
</span>
</div>
<div className="space-y-3 divide-y divide-slate-200/50">
{day.batches?.map((batch: any, bIdx: number) => (
<div key={bIdx} className="pt-3 first:pt-0 space-y-2">
<div className="flex justify-between items-center">
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">
Batch {batch.id || bIdx + 1}
</span>
<span className="text-[10px] font-bold text-brand-indigo bg-white px-2 py-0.5 rounded-lg border border-slate-200 shadow-sm flex items-center gap-1">
<Clock className="w-3.5 h-3.5" />
{batch.startTime || 'TBD'} - {batch.endTime || 'TBD'}
</span>
</div>
{batch.resourcePerson && batch.resourcePerson.name && (
<div className="p-3 bg-white rounded-xl border border-slate-100 space-y-1.5">
<div className="flex justify-between items-start">
<div>
<p className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Resource Person</p>
<p className="text-xs font-bold text-text-dark">{batch.resourcePerson.name}</p>
</div>
<span className={cn(
"px-2 py-0.5 rounded text-[8px] font-black uppercase tracking-wider",
batch.resourcePerson.type === 'EXTERNAL' ? "bg-amber-50 text-amber-600 border border-amber-100" : "bg-brand-glow text-brand-indigo border border-brand-indigo/10"
)}>
{batch.resourcePerson.type || 'INTERNAL'}
</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-1.5 text-[9px] text-text-muted font-semibold">
{batch.resourcePerson.dept && (
<div>
<span className="font-bold text-slate-400 uppercase tracking-wider">Dept:</span> {batch.resourcePerson.dept}
</div>
)}
{batch.resourcePerson.college_name && (
<div className="col-span-2">
<span className="font-bold text-slate-400 uppercase tracking-wider">Inst:</span> {batch.resourcePerson.college_name}
</div>
)}
{batch.resourcePerson.phone && (
<div>
<span className="font-bold text-slate-400 tracking-wider uppercase">Phone:</span> {batch.resourcePerson.phone}
</div>
)}
{batch.resourcePerson.email && (
<div>
<span className="font-bold text-slate-400 tracking-wider uppercase">Email:</span> {batch.resourcePerson.email}
</div>
)}
</div>
</div>
)}
</div>
))}
</div>
</div>
);
})}
</div>
</div>
)}
{/* Department limits and quotas */}
{selectedEvent.deptLimits && Object.keys(selectedEvent.deptLimits).length > 0 && (
<div className="space-y-3">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Users className="w-3.5 h-3.5 text-brand-indigo" />
Department Limits & Quotas
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.entries(selectedEvent.deptLimits).map(([dept, maxSeats]) => {
const sectionLimits = selectedEvent.deptSectionLimits?.[dept];
const hasSectionLimits = sectionLimits && Object.keys(sectionLimits).length > 0;
return (
<div key={dept} className="bg-slate-50 border border-slate-100 rounded-2xl p-4 space-y-3">
<div className="flex justify-between items-center">
<span className="text-xs font-black text-brand-navy uppercase tracking-wider">{dept}</span>
<span className="px-2 py-0.5 bg-brand-indigo text-white text-[9px] font-black uppercase rounded-md">
Quota: {maxSeats} Seats
</span>
</div>
{hasSectionLimits && (
<div className="pt-2 border-t border-slate-200/60 grid grid-cols-3 gap-2">
{Object.entries(sectionLimits).map(([sec, limit]) => (
<div key={sec} className="bg-white px-2 py-1 rounded-xl border border-slate-100 text-center">
<span className="block text-[8px] font-black text-slate-400 uppercase mb-0.5">Sec {sec}</span>
<span className="text-[10px] font-black text-brand-navy">{limit}</span>
</div>
))}
</div>
)}
</div>
);
})}
</div>
</div>
)}
<div className="grid grid-cols-2 gap-8 pt-4 border-t border-slate-50">
<div className="p-4 bg-slate-50 rounded-2xl">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">
<Wallet className="w-3.5 h-3.5 text-brand-indigo" />
Estimated Budget
</div>
<p className="text-lg font-black text-text-dark">{selectedEvent.budget?.toLocaleString() || '0'}</p>
</div>
<div className="p-4 bg-slate-50 rounded-2xl">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted mb-1">
<Ticket className="w-3.5 h-3.5 text-brand-indigo" />
Registration
</div>
<p className="text-lg font-black text-text-dark">
{selectedEvent.hasRegistrationFee ? `${selectedEvent.registrationFee}` : 'FREE'}
</p>
</div>
</div>
{/* Financial Projections Breakdown */}
{(selectedEvent.total_expense || selectedEvent.refreshment_expense || selectedEvent.transportation_expense || selectedEvent.session_coverage_fee) ? (
<div className="space-y-3">
<div className="flex items-center gap-2 text-[10px] font-black uppercase tracking-widest text-text-muted">
<Wallet className="w-3.5 h-3.5 text-emerald-500" />
Financial Projection Breakdown
</div>
<div className="p-4 rounded-2xl bg-emerald-50/30 border border-emerald-100">
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div className="bg-white p-3 rounded-xl border border-slate-100 text-center">
<span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Refreshments</span>
<span className="text-xs font-black text-slate-800">{selectedEvent.refreshment_expense || 0}</span>
</div>
<div className="bg-white p-3 rounded-xl border border-slate-100 text-center">
<span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Transport</span>
<span className="text-xs font-black text-slate-800">{selectedEvent.transportation_expense || 0}</span>
</div>
<div className="bg-white p-3 rounded-xl border border-slate-100 text-center">
<span className="block text-[8px] font-black text-slate-400 uppercase tracking-widest mb-1">Session Fee</span>
<span className="text-xs font-black text-slate-800">{selectedEvent.session_coverage_fee || 0}</span>
</div>
<div className="bg-emerald-100/50 p-3 rounded-xl border border-emerald-200 text-center">
<span className="block text-[8px] font-black text-emerald-700 uppercase tracking-widest mb-1">Total Expense</span>
<span className="text-xs font-black text-emerald-800">{selectedEvent.total_expense || 0}</span>
</div>
</div>
</div>
</div>
) : null}
{selectedEvent.conflictMessage && (
<div className="space-y-4">
<div className="p-4 bg-red-50 border border-red-100 rounded-2xl flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-red-500" />
<div>
<p className="text-[10px] font-black uppercase text-red-500">Critical Conflict Detected</p>
<p className="text-xs font-bold text-red-700">{selectedEvent.conflictMessage}</p>
</div>
</div>
<div className="p-5 bg-brand-glow/30 border border-brand-indigo/10 rounded-2xl space-y-3">
<h4 className="text-xs font-black text-brand-navy uppercase tracking-wider flex items-center gap-2">
<MapPin className="w-4 h-4 text-brand-indigo" />
Available Venues for Re-allocation
</h4>
{getAvailableVenues(selectedEvent).length > 0 ? (
<div className="space-y-3">
<p className="text-[11px] text-text-muted font-bold">
Select one of the free venues below to re-allocate and approve:
</p>
<div className="flex flex-wrap gap-2">
{getAvailableVenues(selectedEvent).map(v => (
<button
key={v}
onClick={() => setSelectedVenue(v)}
className={cn(
"px-3 py-2 rounded-xl text-[10px] font-black uppercase tracking-wider border transition-all",
selectedVenue === v
? "bg-brand-indigo text-white border-brand-indigo premium-shadow-sm"
: "bg-white text-text-dark border-slate-200 hover:border-brand-indigo/30"
)}
>
{v}
</button>
))}
</div>
</div>
) : (
<p className="text-xs font-bold text-red-600">
No other standard venues are available during this slot. You can reject the proposal or coordinate another slot.
</p>
)}
</div>
</div>
)}
<div className="flex items-center gap-4 pt-4">
<button
onClick={() => handleAction(selectedEvent.id, 'reject')}
disabled={processingId === selectedEvent.id}
className="flex-1 bg-red-50 text-red-600 py-4 rounded-2xl font-black text-[11px] uppercase tracking-widest hover:bg-red-100 transition-all"
>
Reject Proposal
</button>
<button
onClick={() => handleAction(selectedEvent.id, 'approve', selectedVenue || undefined)}
disabled={processingId === selectedEvent.id || (!!selectedEvent.conflictMessage && !selectedVenue)}
className={cn(
"flex-1 py-4 rounded-2xl font-black text-[11px] uppercase tracking-widest transition-all premium-shadow",
(!!selectedEvent.conflictMessage && !selectedVenue) ? "bg-slate-200 text-slate-400" : "bg-brand-navy text-white hover:scale-[1.02]"
)}
>
{processingId === selectedEvent.id ? 'Processing...' : selectedVenue ? 'Re-allocate & Approve' : 'Approve Event'}
</button>
</div>
</div>
</motion.div>
</div>
)}
</AnimatePresence>
</div>
);
};