669 lines
33 KiB
TypeScript
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>
|
|
);
|
|
};
|