Final commit
This commit is contained in:
668
frontend/src/components/dashboard/ApprovalsView.tsx
Normal file
668
frontend/src/components/dashboard/ApprovalsView.tsx
Normal file
@@ -0,0 +1,668 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user