From 0b1053a4e8268761b3c330ec3335a46f81699cc5 Mon Sep 17 00:00:00 2001 From: SACHIN Date: Thu, 25 Jun 2026 08:13:09 +0530 Subject: [PATCH] Update frontend components and integrate firebase backend --- frontend/src/App.tsx | 4 + .../src/components/dashboard/AllEvents.tsx | 31 +- .../src/components/dashboard/EventHistory.tsx | 243 +++++- .../dashboard/EventProposalForm.tsx | 768 ++++++++++-------- .../dashboard/ManageNoticesView.tsx | 46 +- .../src/components/student/CategoryGrid.tsx | 4 +- frontend/src/components/student/EventList.tsx | 23 +- .../components/student/EventsHeroSlider.tsx | 12 +- .../src/components/student/HomeDashboard.tsx | 46 +- .../src/components/student/ProfileView.tsx | 19 +- .../student/SpecialEventsBanner.tsx | 25 +- .../src/components/student/StatsSection.tsx | 4 +- .../student/UpcomingEventsSlider.tsx | 196 +++-- frontend/src/lib/firebaseBackend.ts | 15 +- 14 files changed, 923 insertions(+), 513 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e0cef03..067615a 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -55,7 +55,11 @@ const AppContent: React.FC = () => { ...event, eventName: event.title, venue: event.location, + category: event.category, + eventType: event.type, socialProfile: event.guestSocialProfile, + maxParticipants: event["total capacity"] || (event.maxParticipants ?? ''), + sponsors: event.sponsors ?? [], isEditMode: true }); setActiveItem('propose'); diff --git a/frontend/src/components/dashboard/AllEvents.tsx b/frontend/src/components/dashboard/AllEvents.tsx index 718ddbf..4cbb57a 100644 --- a/frontend/src/components/dashboard/AllEvents.tsx +++ b/frontend/src/components/dashboard/AllEvents.tsx @@ -4,12 +4,10 @@ import { motion } from 'framer-motion'; import { Calendar, MapPin, - Building2, Clock, - ChevronRight, Filter, Search, - MoreVertical, + Trash2, FileSpreadsheet } from 'lucide-react'; import { cn } from '../../lib/utils'; @@ -54,6 +52,21 @@ export const AllEvents: React.FC = ({ onEditEvent }) => { } }; + const deleteEvent = async (eventId: number) => { + if (!window.confirm('Are you sure you want to permanently delete this event? This action cannot be undone.')) return; + try { + const res = await fetch(`${API_BASE_URL}/api/events/${eventId}`, { method: 'DELETE' }); + if (res.ok) { + setEvents(prev => prev.filter(e => e.id !== eventId)); + } else { + alert('Failed to delete event. Please try again.'); + } + } catch (err) { + console.error('Delete failed:', err); + alert('An error occurred while deleting the event.'); + } + }; + const getStatusColor = (status: Event['status']) => { switch (status) { case 'REQUESTED': return 'bg-brand-glow text-brand-indigo border-brand-indigo/20'; @@ -204,9 +217,15 @@ export const AllEvents: React.FC = ({ onEditEvent }) => { Edit )} - + {user?.role === 'ADMIN' && ( + + )} diff --git a/frontend/src/components/dashboard/EventHistory.tsx b/frontend/src/components/dashboard/EventHistory.tsx index 646e85f..42d0faa 100644 --- a/frontend/src/components/dashboard/EventHistory.tsx +++ b/frontend/src/components/dashboard/EventHistory.tsx @@ -20,7 +20,8 @@ import { Building2, Users, Wallet, - Ticket + Ticket, + Layers } from 'lucide-react'; import { cn } from '../../lib/utils'; import { format } from 'date-fns'; @@ -41,13 +42,27 @@ interface Event { institution: string; category: string; academicYears: string[]; + targetDepartments?: string[]; + targetedSections?: string[]; proposer?: { fullName: string; + email?: string; + department?: string; + role?: string; }; description?: string; sponsors?: string[]; hasRegistrationFee: boolean; registrationFee: number; + maxParticipants?: number | null; + requirements?: string[]; + refreshment_expense?: number; + transportation_expense?: number; + session_coverage_fee?: number; + total_expense?: number; + paymentLink?: string; + isClubEvent?: boolean; + eventScope?: string; } export const EventHistory: React.FC = () => { @@ -294,38 +309,129 @@ export const EventHistory: React.FC = () => { )} -
-
-
-
- - Date & Time -
-

{new Date(selectedEvent.startDate).toLocaleString()}

-
-
-
- - Location -
-

{selectedEvent.location}

+ {/* Event Classification */} +
+
+
+ + Event Type
+

{selectedEvent.type || 'N/A'}

- -
-
-
- - Department -
-

{selectedEvent.department}

+
+
+ + Hub Category
-
-
- - Target Batches -
-

{selectedEvent.academicYears?.join(', ') || 'N/A'}

+

+ {selectedEvent.category === 'TECHNICAL' ? 'Technical' + : selectedEvent.category === 'NON-TECHNICAL' ? 'Non-Technical' + : selectedEvent.category === 'WORKSHOP' ? 'Workshop' + : selectedEvent.category === 'CENTRE-ACTIVITY' ? 'Centre Based Activity' + : selectedEvent.category || 'N/A'} +

+
+
+ + {/* Dates */} +
+
+
+ + Start Date & Time +
+

+ {selectedEvent.startDate + ? new Date(selectedEvent.startDate).toLocaleString('en-IN', { timeZone: 'Asia/Kolkata', day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }) + ' IST' + : 'N/A'} +

+
+
+
+ + End Date & Time +
+

+ {selectedEvent.endDate + ? new Date(selectedEvent.endDate).toLocaleString('en-IN', { timeZone: 'Asia/Kolkata', day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }) + ' IST' + : 'N/A'} +

+
+
+ + {/* Location & Capacity */} +
+
+
+ + Location +
+

{selectedEvent.location || 'N/A'}

+
+
+
+ + Total Capacity +
+

+ {selectedEvent.maxParticipants ? selectedEvent.maxParticipants.toLocaleString() : 'Unlimited'} +

+
+
+ + {/* Scope */} +
+
+
+ + Event Scope +
+

+ {selectedEvent.department === 'Institutional' ? 'Institutional Event' : 'Departmental Event'} +

+
+
+
+ + Proposing Department +
+

+ {(selectedEvent.proposer?.department && selectedEvent.proposer?.department !== 'N/A') + ? selectedEvent.proposer.department + : (selectedEvent.proposer?.role === 'ADMIN' ? 'System Administrator' : selectedEvent.department) + || 'N/A'} +

+
+
+ + {/* Target Departments & Batches */} +
+
+ + Target Audience +
+
+
+

Departments

+

+ {(selectedEvent.targetDepartments && selectedEvent.targetDepartments.length > 0) + ? selectedEvent.targetDepartments.join(', ') + : selectedEvent.department || 'N/A'} +

+
+
+

Batches

+

+ {selectedEvent.academicYears?.length > 0 ? selectedEvent.academicYears.join(', ') : 'All'} +

+
+
+

Sections

+

+ {(selectedEvent.targetedSections && selectedEvent.targetedSections.length > 0) + ? selectedEvent.targetedSections.join(', ') + : 'All Sections'} +

@@ -347,16 +453,77 @@ export const EventHistory: React.FC = () => {
)} -
-
-
- - Budget + {/* Event Amenities / Requirements */} + {selectedEvent.requirements && selectedEvent.requirements.length > 0 && ( +
+
+ + Event Amenities +
+
+ {selectedEvent.requirements.map((req, idx) => ( + + {req} + + ))}
-

₹{selectedEvent.budget?.toLocaleString() || '0'}

-
-
+ )} + + {/* Financial Details */} +
+
+ + Financial Details +
+
+
+

Budget

+

₹{selectedEvent.budget?.toLocaleString() || '0'}

+
+ {(selectedEvent.refreshment_expense || 0) > 0 && ( +
+

Refreshments

+

₹{selectedEvent.refreshment_expense?.toLocaleString()}

+
+ )} + {(selectedEvent.transportation_expense || 0) > 0 && ( +
+

Transportation

+

₹{selectedEvent.transportation_expense?.toLocaleString()}

+
+ )} + {(selectedEvent.session_coverage_fee || 0) > 0 && ( +
+

Session Coverage

+

₹{selectedEvent.session_coverage_fee?.toLocaleString()}

+
+ )} +
+

Total Expense

+

₹{(selectedEvent.total_expense || selectedEvent.budget || 0).toLocaleString()}

+
+
+
+ + {/* Registration / Free Event */} +
+
+
+ + Registration +
+

+ {selectedEvent.hasRegistrationFee + ? `Paid — ₹${selectedEvent.registrationFee?.toLocaleString()}` + : 'Free Event'} +

+ {selectedEvent.hasRegistrationFee && selectedEvent.paymentLink && ( + Payment Link ↗ + )} +
+
+
Status
diff --git a/frontend/src/components/dashboard/EventProposalForm.tsx b/frontend/src/components/dashboard/EventProposalForm.tsx index 0f4a0a2..a9fefd8 100644 --- a/frontend/src/components/dashboard/EventProposalForm.tsx +++ b/frontend/src/components/dashboard/EventProposalForm.tsx @@ -1,11 +1,13 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import React, { useState, useEffect, useRef, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { - Calendar, - MapPin, - Building2, - Users, - PlusCircle, +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + Calendar, + MapPin, + Building2, + Users, + PlusCircle, CheckCircle2, AlertCircle, ChevronRight, @@ -25,6 +27,7 @@ import { AlertTriangle, FileSpreadsheet } from 'lucide-react'; +/* eslint-enable @typescript-eslint/no-unused-vars */ import { useAuth } from '../../context/AuthContext'; import { cn } from '../../lib/utils'; import { API_BASE_URL } from '../../lib/config'; @@ -35,9 +38,9 @@ const eventTypes = ['Workshop', 'Seminar', 'Conference', 'Guest Lecture', 'Indus const ritDepartments = ['AI&DS', 'AI&ML', 'CSE', 'CCE', 'CSBS', 'ECE', 'MECH', 'EE(VLSI)', 'BIOTECH', 'Placement Department', 'H&S Dept']; const rsbDepartments = ['PGDM']; const amenitiesList = [ - 'Sound System', 'Projector', 'Wi-Fi', 'Refreshments', 'Photography', - 'Stage Decor', 'Mementos', 'Certificates', 'Mike/PA System', 'White Board', - 'Lunch', 'Breakfast', 'Guest Transport', 'Remuneration', 'Bouquet', + 'Sound System', 'Projector', 'Wi-Fi', 'Refreshments', 'Photography', + 'Stage Decor', 'Mementos', 'Certificates', 'Mike/PA System', 'White Board', + 'Lunch', 'Breakfast', 'Guest Transport', 'Remuneration', 'Bouquet', 'LED Screen', 'Flower decoration', 'Computer lab', 'Board Room', 'Guest Hospitality', 'Transportation for Guest' ]; @@ -106,7 +109,7 @@ export const EventProposalForm: React.FC = ({ initialDat const isClubVariant = initialData?.isClubEvent || false; const isFaculty = user?.role === 'FACULTY'; const isPlacementCell = user?.isPlacementStaff || user?.role === 'PLACEMENT' || user?.department === 'Placement Department'; - + const [isSubmitting, setIsSubmitting] = useState(false); const [submitted, setSubmitted] = useState(false); const [error, setError] = useState(''); @@ -116,7 +119,7 @@ export const EventProposalForm: React.FC = ({ initialDat const fileInputRef = useRef(null); - const [venues, setVenues] = useState<{name: string, capacity: number | null}[]>([ + const [venues, setVenues] = useState<{ name: string, capacity: number | null }[]>([ { name: 'N/A', capacity: null }, { name: 'Others.', capacity: null } ]); @@ -175,22 +178,36 @@ export const EventProposalForm: React.FC = ({ initialDat } }; + const getInitialScope = () => { + if (initialData?.category) { + if (['TECHNICAL', 'NON-TECHNICAL', 'WORKSHOP', 'ACADEMIC'].includes(initialData.category)) { + return 'DEPARTMENT'; + } + if (initialData.category === 'CENTRE-ACTIVITY') { + return 'CENTRE'; + } + return initialData.category as any; + } + return initialData?.isClubEvent ? 'INSTITUTIONAL' : (isPlacementCell ? 'PLACEMENT' : 'DEPARTMENT'); + }; + const [eventScope, setEventScope] = useState<'INSTITUTIONAL' | 'DEPARTMENT' | 'CLUB' | 'PLACEMENT' | 'CENTRE'>( - initialData?.isClubEvent ? 'INSTITUTIONAL' : (isPlacementCell ? 'PLACEMENT' : 'DEPARTMENT') + getInitialScope() ); const [conflictData, setConflictData] = useState<{ message: string, conflicts: any[], canOverride: boolean } | null>(null); const currentAcademicYear = getCurrentAcademicYear(); - + // Filter institutional events that haven't been approved/completed this year const availableInstitutionalEvents = INSTITUTIONAL_EVENTS.filter(ie => { - return !existingEvents.some(e => - e.title.toLowerCase() === ie.name.toLowerCase() && + return !existingEvents.some(e => + e.title.toLowerCase() === ie.name.toLowerCase() && (e.status?.toUpperCase() === 'APPROVED' || e.status?.toUpperCase() === 'COMPLETED') && e.academicYears?.includes(currentAcademicYear) ); }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars const parseDateForInput = (dateStr?: string) => { if (!dateStr) return ''; try { @@ -225,8 +242,8 @@ export const EventProposalForm: React.FC = ({ initialDat eventName: initialData?.eventName || '', startDate: initialData?.startDate ? formatDateTimeForInput(initialData.startDate) : '', endDate: initialData?.endDate ? formatDateTimeForInput(initialData.endDate) : '', - eventType: isClubVariant ? 'CLUB' : (initialData?.eventType || 'Guest Lecture'), - category: isClubVariant ? 'CLUB' : (initialData?.category || 'ACADEMIC'), + eventType: initialData?.eventType || (isClubVariant ? 'CLUB' : 'Guest Lecture'), + category: initialData?.category || (isClubVariant ? 'CLUB' : 'TECHNICAL'), institution: initialData?.institution || 'RIT', department: initialData?.department || (isFaculty ? (user?.department || 'CSE') : 'CSE'), academicYears: initialData?.academicYears || [] as string[], @@ -261,9 +278,9 @@ export const EventProposalForm: React.FC = ({ initialDat // Sync financial totals useEffect(() => { - const total = (parseFloat(formData.refreshment_expense) || 0) + - (parseFloat(formData.transportation_expense) || 0) + - (parseFloat(formData.session_coverage_fee) || 0); + const total = (parseFloat(formData.refreshment_expense) || 0) + + (parseFloat(formData.transportation_expense) || 0) + + (parseFloat(formData.session_coverage_fee) || 0); setFormData(prev => ({ ...prev, total_expense: total.toFixed(2) })); }, [formData.refreshment_expense, formData.transportation_expense, formData.session_coverage_fee]); @@ -281,15 +298,30 @@ export const EventProposalForm: React.FC = ({ initialDat customVenue: '', }); + const prevEventScope = useRef(eventScope); + const hasInitializedEdit = useRef(!!initialData?.isEditMode); + useEffect(() => { + const scopeChanged = prevEventScope.current !== eventScope; + prevEventScope.current = eventScope; + + // Skip reset entirely on initial mount when editing an existing event + if (hasInitializedEdit.current) { + hasInitializedEdit.current = false; + if (!scopeChanged) return; // user?.department changed, not scope — don't reset + } + + // Only reset when the scope actually changed (user clicked a different tab) + if (!scopeChanged) return; + // Reset event name and update default event type on scope change const availableCentres = CENTRE_MAPPING[user?.department || ''] || CENTRE_MAPPING['Placement Department'] || []; setFormData(prev => ({ ...prev, eventName: '', - eventType: eventScope === 'INSTITUTIONAL' ? 'Institutional' : - eventScope === 'CLUB' ? 'Club' : - eventScope === 'PLACEMENT' ? 'Placement' : '', + eventType: eventScope === 'INSTITUTIONAL' ? '' : + eventScope === 'CLUB' ? 'Club' : + eventScope === 'PLACEMENT' ? 'Placement' : '', category: eventScope === 'CENTRE' ? 'CENTRE' : (eventScope === 'CLUB' ? 'CLUB' : 'ACADEMIC'), centreName: eventScope === 'CENTRE' ? (availableCentres[0] || '') : prev.centreName })); @@ -297,12 +329,7 @@ export const EventProposalForm: React.FC = ({ initialDat const [batches, setBatches] = useState([]); - useEffect(() => { - fetchClasses(); - fetchExistingEvents(); - fetchClassStrengths(); - fetchBatches(); - }, []); + // data-fetch effect is declared after the fetch functions below const fetchBatches = async () => { try { @@ -316,6 +343,7 @@ export const EventProposalForm: React.FC = ({ initialDat } }; + // eslint-disable-next-line @typescript-eslint/no-unused-vars const deptBatches = useMemo(() => { const dept = eventScope === 'DEPARTMENT' ? formData.department : (user?.department || 'CSE'); return batches.filter(b => b.department === dept); @@ -326,7 +354,7 @@ export const EventProposalForm: React.FC = ({ initialDat const response = await fetch(API_BASE_URL + '/api/admin/users'); if (response.ok) { const data = await response.json(); - + // Count students dynamically in each class combo const studentCounts: Record = {}; data.forEach((u: any) => { @@ -378,16 +406,25 @@ export const EventProposalForm: React.FC = ({ initialDat } }; + /* eslint-disable react-hooks/set-state-in-effect */ + useEffect(() => { + fetchClasses(); + fetchExistingEvents(); + fetchClassStrengths(); + fetchBatches(); + }, []); + /* eslint-enable react-hooks/set-state-in-effect */ + const availableYears = Array.from(new Set( classes - .filter(c => c.institution.trim() === formData.institution.trim() && - (eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell) ? true : c.department.trim() === formData.department.trim())) + .filter(c => c.institution.trim() === formData.institution.trim() && + (eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell) ? true : c.department.trim() === formData.department.trim())) .map(c => c.academicYear) )).sort(); const availableSections = classes - .filter(c => - c.institution.trim() === formData.institution.trim() && + .filter(c => + c.institution.trim() === formData.institution.trim() && ((eventScope === 'DEPARTMENT' || eventScope === 'PLACEMENT') ? ( eventScope === 'DEPARTMENT' ? c.department.trim() === formData.department.trim() : formData.targetDepartments.includes(c.department.trim()) ) : true) && @@ -400,7 +437,7 @@ export const EventProposalForm: React.FC = ({ initialDat const current = prev[field] as string[]; const exists = current.includes(value); const updated = exists ? current.filter(v => v !== value) : [...current, value]; - + if (field === 'academicYears') return { ...prev, academicYears: updated, allBatches: false }; if (field === 'targetDepartments') return { ...prev, targetDepartments: updated, allDepts: false }; if (field === 'targetedSections') return { ...prev, targetedSections: updated, allSections: false }; @@ -424,15 +461,15 @@ export const EventProposalForm: React.FC = ({ initialDat return existingEvents.some(e => { const status = e.status?.toUpperCase(); const isActive = status === 'APPROVED' || status === 'COMPLETED' || status === 'PENDING_PR' || status === 'REQUESTED'; - return e.type?.toLowerCase() === formData.eventType?.toLowerCase() && - isActive && - e.targetedSections?.includes(section) && - formData.academicYears.some(y => e.academicYears?.includes(y)); + return e.type?.toLowerCase() === formData.eventType?.toLowerCase() && + isActive && + e.targetedSections?.includes(section) && + formData.academicYears.some(y => e.academicYears?.includes(y)); }); }; - const leftOutSections = availableSections.filter(s => - !formData.targetedSections.includes(s) && + const leftOutSections = availableSections.filter(s => + !formData.targetedSections.includes(s) && !isSectionDisabled(s) ); @@ -440,15 +477,15 @@ export const EventProposalForm: React.FC = ({ initialDat const deptsToTarget = (eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? formData.targetDepartments : [formData.department]; const batchesToTarget = formData.allBatches ? availableYears : formData.academicYears; let total = 0; - + for (const d of deptsToTarget) { for (const y of batchesToTarget) { const availableSecsForThisCombo = classes .filter(c => c.department?.trim() === d?.trim() && c.academicYear?.trim() === y?.trim() && c.institution?.trim() === formData.institution?.trim()) .reduce((acc, curr) => [...acc, ...curr.sections], [] as string[]); - + const sectionsToTarget = formData.allSections ? availableSecsForThisCombo : formData.targetedSections.filter(s => availableSecsForThisCombo.includes(s)); - + for (const s of sectionsToTarget) { const match = classStrengths.find(cs => cs.dept === d && cs.year === y && cs.sec === s); if (match && match.strength > 0) { @@ -461,32 +498,41 @@ export const EventProposalForm: React.FC = ({ initialDat }, [formData.targetDepartments, formData.department, formData.academicYears, formData.targetedSections, formData.allBatches, formData.allSections, formData.institution, classStrengths, eventScope, isPlacementCell, availableYears, classes]); const filteredVenues = React.useMemo(() => { - if (totalExpectedStrength === 0) return venues; - - // Find the smallest capacity venue that can accommodate the totalExpectedStrength - const validCapacities = venues - .map(v => v.capacity) - .filter((cap): cap is number => cap !== null && cap >= totalExpectedStrength); - - const minSufficientCapacity = validCapacities.length > 0 ? Math.min(...validCapacities) : totalExpectedStrength; - - return venues.filter(v => { - if (v.name === 'N/A' || v.name === 'Others.' || v.capacity === null) return true; - - // Capacity must be at least the audience size, and not exceed the max of (1.5x audience OR the smallest sufficient capacity) - const maxAllowedCapacity = Math.max(totalExpectedStrength * 1.5, minSufficientCapacity); - return v.capacity >= totalExpectedStrength && v.capacity <= maxAllowedCapacity; - }); - }, [venues, totalExpectedStrength]); + let list = venues; + if (totalExpectedStrength > 0) { + // Find the smallest capacity venue that can accommodate the totalExpectedStrength + const validCapacities = venues + .map(v => v.capacity) + .filter((cap): cap is number => cap !== null && cap >= totalExpectedStrength); + + const minSufficientCapacity = validCapacities.length > 0 ? Math.min(...validCapacities) : totalExpectedStrength; + + list = venues.filter(v => { + if (v.name === 'N/A' || v.name === 'Others.' || v.capacity === null) return true; + if (initialData?.venue && v.name === initialData.venue) return true; + + // Capacity must be at least the audience size, and not exceed the max of (1.5x audience OR the smallest sufficient capacity) + const maxAllowedCapacity = Math.max(totalExpectedStrength * 1.5, minSufficientCapacity); + return v.capacity >= totalExpectedStrength && v.capacity <= maxAllowedCapacity; + }); + } + if (initialData?.venue && !list.some(v => v.name === initialData.venue)) { + list = [...list, { name: initialData.venue, capacity: null }]; + } + return list; + }, [venues, totalExpectedStrength, initialData?.venue]); useEffect(() => { if (formData.venue && formData.venue !== 'N/A' && formData.venue !== 'Others.') { const isStillAvailable = filteredVenues.some(v => v.name === formData.venue); if (!isStillAvailable) { + if (initialData?.isEditMode && formData.venue === initialData.venue) { + return; + } setFormData(prev => ({ ...prev, venue: 'N/A' })); } } - }, [filteredVenues]); + }, [filteredVenues, formData.venue, initialData]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -517,12 +563,6 @@ export const EventProposalForm: React.FC = ({ initialDat const groupRequestId = crypto.randomUUID(); const batchesToAssign = formData.allBatches ? availableYears : formData.academicYears; - let deptsToCreate = (eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? formData.targetDepartments : [formData.department]; - - // For institutional events, create only ONE global event record - if (eventScope === 'INSTITUTIONAL') { - deptsToCreate = ['Institutional']; - } if (eventScope === 'CENTRE' && !formData.centreName) { setError('Please select a centre.'); @@ -530,7 +570,7 @@ export const EventProposalForm: React.FC = ({ initialDat return; } - if (deptsToCreate.length === 0) { + if (formData.targetDepartments.length === 0 && (eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell))) { setError(`Please select at least one target department for this ${eventScope.toLowerCase()} event.`); setIsSubmitting(false); return; @@ -572,23 +612,62 @@ export const EventProposalForm: React.FC = ({ initialDat if (!res.ok) { const err = await res.json(); - throw new Error(err.message || 'Update failed'); + throw new Error(err.message || 'Update failed'); } } else { - for (const dept of deptsToCreate) { - // Filter sections that belong to THIS department - const deptSections = (eventScope === 'DEPARTMENT' || eventScope === 'PLACEMENT') - ? formData.targetedSections.filter(s => - classes.some(c => c.department.trim() === dept.trim() && c.sections.includes(s)) - ) - : []; + const primaryPayload = { + ...formData, + startDate: earliestStart.toISOString(), + endDate: latestEnd.toISOString(), + deptLimits, + deptSectionLimits, + maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants), + durationDays: 1, + dayConfigs: [], + refreshment_expense: Number(formData.refreshment_expense) || 0, + transportation_expense: Number(formData.transportation_expense) || 0, + session_coverage_fee: Number(formData.session_coverage_fee) || 0, + total_expense: Number(formData.total_expense) || 0, + department: formData.department, + venue: formData.venue === 'Others.' ? formData.customVenue : formData.venue, + groupRequestId, + academicYears: batchesToAssign, + targetedSections: formData.targetedSections, + userId: user?.id, + budget: formData.budget === '' ? 0 : Number(formData.budget), + registrationFee: formData.hasRegistrationFee ? Number(formData.registrationFee) : 0, + category: eventScope === 'INSTITUTIONAL' ? 'INSTITUTIONAL' : + eventScope === 'CLUB' ? 'CLUB' : + eventScope === 'CENTRE' ? 'CENTRE-ACTIVITY' : + eventScope === 'PLACEMENT' ? 'PLACEMENT' : + formData.category, + eventType: formData.eventType, + centreName: eventScope === 'CENTRE' ? formData.centreName : undefined, + isPublicEvent: eventScope === 'CENTRE' ? formData.isPublicEvent : undefined, + image: formData.image + }; - // If it's a placement event and we're selective, only create for depts with selected sections - if (eventScope === 'PLACEMENT' && formData.targetedSections.length > 0 && deptSections.length === 0) { - continue; + const res = await fetch(API_BASE_URL + '/api/events/propose', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + ...primaryPayload, + cancelConflicting: false + }), + }); + + if (!res.ok) { + const err = await res.json(); + if (res.status === 409 && err.canOverride) { + setConflictData(err); + setIsSubmitting(false); + return; } + throw new Error(err.message || 'Submission failed'); + } - const primaryPayload = { + if (!formData.allSections && showAlternateEvent && (eventScope === 'DEPARTMENT' || eventScope === 'PLACEMENT') && leftOutSections.length > 0) { + const altPayload = { ...formData, startDate: earliestStart.toISOString(), endDate: latestEnd.toISOString(), @@ -601,78 +680,23 @@ export const EventProposalForm: React.FC = ({ initialDat transportation_expense: Number(formData.transportation_expense) || 0, session_coverage_fee: Number(formData.session_coverage_fee) || 0, total_expense: Number(formData.total_expense) || 0, - department: dept, - venue: formData.venue === 'Others.' ? formData.customVenue : formData.venue, + department: formData.department, groupRequestId, + eventName: alternateEvent.eventName || `${formData.eventName} (Division 2)`, + venue: alternateEvent.venue === 'Others.' ? alternateEvent.customVenue : alternateEvent.venue, + targetedSections: leftOutSections, academicYears: batchesToAssign, - targetedSections: deptSections, userId: user?.id, - budget: formData.budget === '' ? 0 : Number(formData.budget), + budget: 0, registrationFee: formData.hasRegistrationFee ? Number(formData.registrationFee) : 0, - category: eventScope === 'CLUB' ? 'CLUB' : (eventScope === 'INSTITUTIONAL' ? 'INSTITUTIONAL' : (eventScope === 'CENTRE' ? 'CENTRE' : 'ACADEMIC')), - eventType: formData.eventType, - centreName: eventScope === 'CENTRE' ? formData.centreName : undefined, - isPublicEvent: eventScope === 'CENTRE' ? formData.isPublicEvent : undefined, - image: formData.image + eventType: formData.eventType }; - const res = await fetch(API_BASE_URL + '/api/events/propose', { + await fetch(API_BASE_URL + '/api/events/propose', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - ...primaryPayload, - cancelConflicting: false - }), + body: JSON.stringify(altPayload), }); - - if (!res.ok) { - const err = await res.json(); - if (res.status === 409 && err.canOverride) { - setConflictData(err); - setIsSubmitting(false); - return; - } - throw new Error(`[${dept}] ${err.message || 'Submission failed'}`); - } - - if (!formData.allSections && showAlternateEvent && (eventScope === 'DEPARTMENT' || eventScope === 'PLACEMENT') && leftOutSections.length > 0) { - const deptLeftOut = leftOutSections.filter(s => - classes.some(c => c.department.trim() === dept.trim() && c.sections.includes(s)) - ); - - if (deptLeftOut.length > 0) { - const altPayload = { - ...formData, - startDate: earliestStart.toISOString(), - endDate: latestEnd.toISOString(), - deptLimits, - deptSectionLimits, - maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants), - durationDays: 1, - dayConfigs: [], - refreshment_expense: Number(formData.refreshment_expense) || 0, - transportation_expense: Number(formData.transportation_expense) || 0, - session_coverage_fee: Number(formData.session_coverage_fee) || 0, - total_expense: Number(formData.total_expense) || 0, - department: dept, - groupRequestId, - eventName: alternateEvent.eventName || `${formData.eventName} (Division 2)`, - venue: alternateEvent.venue === 'Others.' ? alternateEvent.customVenue : alternateEvent.venue, - targetedSections: deptLeftOut, - academicYears: batchesToAssign, - userId: user?.id, - budget: 0, - registrationFee: formData.hasRegistrationFee ? Number(formData.registrationFee) : 0, - eventType: formData.eventType - }; - - await fetch(API_BASE_URL + '/api/events/propose', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(altPayload), - }); - } - } } } setSubmitted(true); @@ -697,7 +721,7 @@ export const EventProposalForm: React.FC = ({ initialDat return (
{(user?.role === 'ADMIN' || user?.role === 'PRINCIPAL') && ( - = ({ initialDat

Have many events? Upload a spreadsheet instead.

- )} {(isFaculty || isPlacementCell) && ( - )} {!isPlacementCell && ( - )} {user?.isClubCoordinator && ( - )} {isPlacementCell && ( -