import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react'; import { Event, EventDay, Batch } from '../types'; import { CLUBS } from '../constants'; import { uploadToSupabase, supabase } from '../supabase'; const DEPARTMENTS = ['AIDS', 'CSBS', 'CSE', 'CCE', 'MECH', 'VLSI', 'BIO-TECH', 'AIML', 'ECE', 'H&S']; interface CreateEventFormProps { onCancel: () => void; onSuccess: (event: Event) => void; eventToEdit?: Event; onSupabaseError?: () => void; } const CreateEventForm: React.FC = ({ onCancel, onSuccess, eventToEdit }) => { const parseDateForInput = (dateStr: string) => { if (!dateStr) return ''; try { const date = new Date(dateStr); if (isNaN(date.getTime())) return ''; const yyyy = date.getFullYear(); const mm = String(date.getMonth() + 1).padStart(2, '0'); const dd = String(date.getDate()).padStart(2, '0'); return `${yyyy}-${mm}-${dd}`; } catch { return ''; } }; const mapDBRowToEvent = (row: any): Event => ({ ...row, registrationDeadline: row.registration_deadline, maxParticipants: row.max_participants, durationDays: row.duration_days, isTeamEvent: row.is_team_event, teamSizeLimit: row.team_size_limit, teamComposition: row.team_composition, participantType: row.participant_type, verificationStatus: row.verification_status, pricingType: row.pricing_type, deptLimits: deptLimits, deptSectionLimits: deptSectionLimits, }); // Build dayConfigs from normalized schedule data when editing const buildDayConfigsFromSchedule = (): EventDay[] => { if (!eventToEdit?.schedule || eventToEdit.schedule.length === 0) { return [{ date: eventToEdit ? parseDateForInput(eventToEdit.date) : '', batches: [{ id: 1, startTime: '', endTime: '' }], startTime: '', endTime: '', }]; } // Group schedule entries by day_idx const dayMap = new Map(); eventToEdit.schedule.forEach(s => { if (!dayMap.has(s.day_idx)) dayMap.set(s.day_idx, []); dayMap.get(s.day_idx)!.push(s); }); const days: EventDay[] = []; const sortedDayIdxs = Array.from(dayMap.keys()).sort((a, b) => a - b); sortedDayIdxs.forEach(dayIdx => { const slots = dayMap.get(dayIdx)!.sort((a, b) => a.batch_idx - b.batch_idx); const batches: Batch[] = slots.map((s, i) => ({ id: i + 1, startTime: s.start_time || '', endTime: s.end_time || '', resourcePerson: s.resource_person || undefined, })); days.push({ date: slots[0]?.date ? parseDateForInput(slots[0].date) : '', batches, startTime: '', endTime: '', }); }); return days; }; const [formData, setFormData] = useState({ title: eventToEdit?.title || '', location: eventToEdit?.location || '', date: eventToEdit ? parseDateForInput(eventToEdit.date) : '', category: eventToEdit?.category || 'TECHNICAL' as Event['category'], domain: eventToEdit?.domain || '', pricingType: eventToEdit?.pricingType || 'FREE' as Event['pricingType'], coordinator: eventToEdit?.coordinator || '', club: eventToEdit?.club || '', image: eventToEdit?.image || '', registrationDeadline: eventToEdit?.registrationDeadline || '', maxParticipants: eventToEdit?.maxParticipants?.toString() || '', durationDays: eventToEdit?.durationDays?.toString() || '1', event_summary: eventToEdit?.event_summary || '', isTeamEvent: eventToEdit?.isTeamEvent || false, teamSizeLimit: eventToEdit?.teamSizeLimit?.toString() || '', teamComposition: eventToEdit?.teamComposition || 'MIXED' as Event['teamComposition'], participantType: eventToEdit?.participantType || 'INTERNAL' as Event['participantType'], conducting_dept: eventToEdit?.conducting_dept || '', refreshment_expense: eventToEdit?.refreshment_expense?.toString() || '', transportation_expense: eventToEdit?.transportation_expense?.toString() || '', session_coverage_fee: eventToEdit?.session_coverage_fee?.toString() || '', total_expense: eventToEdit?.total_expense?.toString() || '0', dayConfigs: buildDayConfigsFromSchedule(), }); const [allLocations, setAllLocations] = useState([]); const [bookedEvents, setBookedEvents] = useState([]); const [locationDropdownOpen, setLocationDropdownOpen] = useState(false); const locationDropdownRef = useRef(null); // Close dropdown on click outside useEffect(() => { const handleClickOutside = (e: MouseEvent) => { if (locationDropdownRef.current && !locationDropdownRef.current.contains(e.target as Node)) { setLocationDropdownOpen(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); useEffect(() => { 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]); // Sync Launch Date to Day 1 configuration useEffect(() => { if (formData.date && formData.dayConfigs.length > 0) { setFormData(prev => { const newConfigs = [...prev.dayConfigs]; newConfigs[0] = { ...newConfigs[0], date: formData.date }; return { ...prev, dayConfigs: newConfigs }; }); } }, [formData.date]); useEffect(() => { const fetchLogistics = async () => { const { data: locs } = await supabase.from('locations').select('*'); if (locs) setAllLocations(locs); // Fetch all events that might overlap (Not rejected) const { data: evts } = await supabase.from('events') .select('id, location, date, duration_days, verification_status') .neq('verification_status', 'REJECTED'); if (evts) setBookedEvents(evts); }; fetchLogistics(); }, []); useEffect(() => { const days = parseInt(formData.durationDays) || 1; setFormData(prev => { const newConfigs = [...prev.dayConfigs]; if (days > newConfigs.length) { for (let i = newConfigs.length; i < days; i++) { // Default to next day let nextDate = ''; if (newConfigs[i - 1]?.date) { const d = new Date(newConfigs[i - 1].date); d.setDate(d.getDate() + 1); nextDate = d.toISOString().split('T')[0]; } newConfigs.push({ date: nextDate, batches: [{ id: 1, startTime: '', endTime: '' }], startTime: '', endTime: '' }); } } else if (days < newConfigs.length) { newConfigs.splice(days); } return { ...prev, dayConfigs: newConfigs }; }); }, [formData.durationDays]); const [deptLimits, setDeptLimits] = useState>(eventToEdit?.deptLimits || {}); const [deptSectionLimits, setDeptSectionLimits] = useState>>(eventToEdit?.deptSectionLimits || {}); const [activeDeptForSections, setActiveDeptForSections] = useState(null); const [isSubmitting, setIsSubmitting] = useState(false); const fileInputRef = useRef(null); const [availableDomains, setAvailableDomains] = useState([]); useEffect(() => { const fetchDomains = async () => { const { data } = await supabase.from('domains').select('*').eq('status', 'APPROVED').eq('category', formData.category); if (data) { setAvailableDomains(data.sort((a, b) => a.name.localeCompare(b.name))); } }; fetchDomains(); }, [formData.category]); const handleImageChange = (e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (file) { const reader = new FileReader(); reader.onloadend = () => { const img = new Image(); img.onload = () => { const canvas = document.createElement('canvas'); const max_width = 800; let width = img.width; let height = img.height; if (width > max_width) { height = Math.round((height * max_width) / width); width = max_width; } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (ctx) { ctx.drawImage(img, 0, 0, width, height); const compressedBase64 = canvas.toDataURL('image/jpeg', 0.7); setFormData(prev => ({ ...prev, image: compressedBase64 })); } else { setFormData(prev => ({ ...prev, image: reader.result as string })); } }; img.src = reader.result as string; }; reader.readAsDataURL(file); } }; const handleDeptLimitChange = (dept: string, value: string) => { const numValue = parseInt(value); setDeptLimits(prev => { const newLimits = { ...prev }; if (isNaN(numValue) || numValue <= 0) { delete newLimits[dept]; } else { newLimits[dept] = numValue; } return newLimits; }); }; const handleSectionLimitChange = (dept: string, sec: string, value: string) => { const numValue = parseInt(value); setDeptSectionLimits(prev => { const newSectionLimits = { ...prev }; if (!newSectionLimits[dept]) { newSectionLimits[dept] = {}; } if (isNaN(numValue) || numValue <= 0) { delete newSectionLimits[dept][sec]; } else { newSectionLimits[dept][sec] = numValue; } if (Object.keys(newSectionLimits[dept]).length === 0) { delete newSectionLimits[dept]; } // Calculate overall department limit as the sum of section limits const sectionSum: number = newSectionLimits[dept] ? (Object.values(newSectionLimits[dept]) as any[]).reduce((sum: number, val: any) => sum + (Number(val) || 0), 0) : 0; setDeptLimits(prevDept => { const newDeptLimits = { ...prevDept }; if (sectionSum > 0) { newDeptLimits[dept] = sectionSum; } else { delete newDeptLimits[dept]; } return newDeptLimits; }); return newSectionLimits; }); }; const handleNumBatchesChange = (dayIndex: number, value: string) => { const count = parseInt(value); setFormData(prev => { const newConfigs = [...prev.dayConfigs]; const dayBatches = [...newConfigs[dayIndex].batches]; if (count >= 1 && count > dayBatches.length) { for (let i = dayBatches.length; i < count; i++) { dayBatches.push({ id: i + 1, startTime: '', endTime: '' }); } } else { // Always keep at least 1 batch dayBatches.splice(Math.max(count, 1)); } newConfigs[dayIndex] = { ...newConfigs[dayIndex], batches: dayBatches }; return { ...prev, dayConfigs: newConfigs }; }); }; const handleBatchTimeChange = (dayIndex: number, batchIndex: number, field: 'startTime' | 'endTime', value: string) => { setFormData(prev => { const newConfigs = [...prev.dayConfigs]; const dayBatches = [...newConfigs[dayIndex].batches]; dayBatches[batchIndex] = { ...dayBatches[batchIndex], [field]: value }; newConfigs[dayIndex] = { ...newConfigs[dayIndex], batches: dayBatches }; return { ...prev, dayConfigs: newConfigs }; }); }; const handleDayDateChange = (dayIndex: number, value: string) => { setFormData(prev => { const newConfigs = [...prev.dayConfigs]; newConfigs[dayIndex] = { ...newConfigs[dayIndex], date: value }; return { ...prev, dayConfigs: newConfigs }; }); }; const handleDayTimeChange = (dayIndex: number, field: 'startTime' | 'endTime', value: string) => { setFormData(prev => { const newConfigs = [...prev.dayConfigs]; newConfigs[dayIndex] = { ...newConfigs[dayIndex], [field]: value }; return { ...prev, dayConfigs: newConfigs }; }); }; const handleResourcePersonChange = (dayIndex: number, batchIndex: number | null, field: keyof any, value: any) => { setFormData(prev => { const newConfigs = [...prev.dayConfigs]; if (batchIndex === null) { // Day level (Unified) const currentRP = newConfigs[dayIndex].resourcePerson || { type: 'INTERNAL', name: '', phone: '', email: '' }; newConfigs[dayIndex] = { ...newConfigs[dayIndex], resourcePerson: { ...currentRP, [field]: value } }; } else { // Batch level const dayBatches = [...newConfigs[dayIndex].batches]; const currentRP = dayBatches[batchIndex].resourcePerson || { type: 'INTERNAL', name: '', phone: '', email: '' }; dayBatches[batchIndex] = { ...dayBatches[batchIndex], resourcePerson: { ...currentRP, [field]: value } }; newConfigs[dayIndex] = { ...newConfigs[dayIndex], batches: dayBatches }; } return { ...prev, dayConfigs: newConfigs }; }); }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.domain || !formData.image) { alert("Please complete all required fields including image."); return; } setIsSubmitting(true); try { // Keep Base64 string directly in the database, ignoring Firebase/Supabase storage const finalImageUrl = formData.image; const totalLimit = parseInt(formData.maxParticipants); // Build the DB row — no longer includes time, batches, event_days, dept_limits const dbRow: Record = { title: formData.title, location: formData.location, date: new Date(formData.dayConfigs[0].date || formData.date).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }), category: formData.category, domain: formData.domain, pricing_type: formData.pricingType, coordinator: formData.coordinator, club: formData.club, image: finalImageUrl, registration_deadline: formData.registrationDeadline || null, max_participants: isNaN(totalLimit) ? null : totalLimit, duration_days: parseInt(formData.durationDays) || 1, event_summary: formData.event_summary, is_team_event: formData.isTeamEvent, team_size_limit: parseInt(formData.teamSizeLimit) || null, team_composition: formData.teamComposition, participant_type: formData.participantType, verification_status: eventToEdit?.verificationStatus || (formData.category === 'CENTRE-ACTIVITY' ? 'PENDING_ADMIN' : 'PENDING_HOD'), status: eventToEdit?.status || 'Scheduled', refreshment_expense: parseFloat(formData.refreshment_expense) || 0, transportation_expense: parseFloat(formData.transportation_expense) || 0, session_coverage_fee: parseFloat(formData.session_coverage_fee) || 0, total_expense: parseFloat(formData.total_expense) || 0, conducting_dept: formData.conducting_dept || null, created_by: (await supabase.auth.getUser()).data.user?.id || null, request_by_faculty: eventToEdit?.request_by_faculty || new Date().toISOString(), request_by_hod: eventToEdit?.request_by_HOD || null }; // If editing, update; if creating, insert let createdEvent: any; if (eventToEdit?.id) { const { data, error } = await supabase.from('events').update(dbRow).eq('id', eventToEdit.id).select().single(); if (error) throw error; createdEvent = data; // Clean up old schedule + resource persons + dept limits when updating (cascade handles RP/sched/limits cleanup) await supabase.from('event_schedule').delete().eq('event_id', eventToEdit.id); await supabase.from('resource_persons').delete().eq('event_id', eventToEdit.id); await supabase.from('event_dept_limits').delete().eq('event_id', eventToEdit.id); } else { const { data, error } = await supabase.from('events').insert([dbRow]).select().single(); if (error) throw error; createdEvent = data; } // Save schedule rows to event_schedule table const scheduleRows: any[] = []; formData.dayConfigs.forEach((day: any, dIdx: number) => { const formattedDate = day.date ? new Date(day.date).toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' }) : ''; if (day.batches && day.batches.length > 0) { day.batches.forEach((batch: any, bIdx: number) => { scheduleRows.push({ event_id: createdEvent.id, day_idx: dIdx + 1, batch_idx: bIdx + 1, date: formattedDate, start_time: batch.startTime || '', end_time: batch.endTime || '', }); }); } else { // Single unified slot for the day scheduleRows.push({ event_id: createdEvent.id, day_idx: dIdx + 1, batch_idx: 1, date: formattedDate, start_time: day.startTime || '', end_time: day.endTime || '', }); } }); let insertedSchedule: any[] = []; if (scheduleRows.length > 0) { const { data: schedData, error: schedError } = await supabase .from('event_schedule') .insert(scheduleRows) .select(); if (schedError) { console.error("Schedule save error:", schedError); } else { insertedSchedule = schedData || []; } } // Save Resource Persons with schedule_id linking const resourcePersonsToSave: any[] = []; formData.dayConfigs.forEach((day: any, dIdx: number) => { if (day.batches.length === 0 || (day.batches.length === 1 && !day.batches[0].resourcePerson?.name)) { if (day.resourcePerson?.name) { const schedRow = insertedSchedule.find((s: any) => s.day_idx === dIdx + 1 && s.batch_idx === 1); resourcePersonsToSave.push({ event_id: createdEvent.id, day_idx: dIdx + 1, batch_idx: 1, schedule_id: schedRow?.id || null, ...day.resourcePerson }); } } else { day.batches.forEach((batch: any, bIdx: number) => { if (batch.resourcePerson?.name) { const schedRow = insertedSchedule.find((s: any) => s.day_idx === dIdx + 1 && s.batch_idx === bIdx + 1); resourcePersonsToSave.push({ event_id: createdEvent.id, day_idx: dIdx + 1, batch_idx: bIdx + 1, schedule_id: schedRow?.id || null, ...batch.resourcePerson }); } }); } }); if (resourcePersonsToSave.length > 0) { const { error: rpError } = await supabase .from('resource_persons') .insert(resourcePersonsToSave); if (rpError) { console.error("Resource Person save error:", rpError); } } // Save department limits to event_dept_limits table const deptLimitRows = Object.entries(deptLimits).map(([dept, maxSeats]) => ({ event_id: createdEvent.id, department: dept, max_seats: maxSeats, section_limits: deptSectionLimits[dept] || {}, })); if (deptLimitRows.length > 0) { const { error: deptLimitsError } = await supabase .from('event_dept_limits') .insert(deptLimitRows); if (deptLimitsError) { console.error("Dept limits save error:", deptLimitsError); } } onSuccess(mapDBRowToEvent(createdEvent)); } catch (err: any) { console.error("Submission error:", err); alert(`Failed to save event: ${err?.message || JSON.stringify(err)}`); } finally { setIsSubmitting(false); } }; const categories: Event['category'][] = ['TECHNICAL', 'NON-TECHNICAL', 'WORKSHOP', 'CENTRE-ACTIVITY']; return (

{eventToEdit ? 'Refine Session' : 'Generate Event'}

Coordinator Hub Portal

{/* Section 1: Event Identity */}

01. Core Identity

setFormData({ ...formData, title: e.target.value })} />
setFormData({ ...formData, coordinator: e.target.value })} />
{/* Section 1.5: Financial Governance (Admin Only View) */}

01B. Financial Projections

setFormData({ ...formData, refreshment_expense: e.target.value })} />
setFormData({ ...formData, transportation_expense: e.target.value })} />
setFormData({ ...formData, session_coverage_fee: e.target.value })} />

Financial data is visible to you and administrative auditors only.

{/* Section 2: Logistics */}

02. Logistics & Hosting

{ const newDate = e.target.value; setFormData(prev => { const newConfigs = [...prev.dayConfigs]; if (newConfigs.length > 0) newConfigs[0] = { ...newConfigs[0], date: newDate }; return { ...prev, date: newDate, dayConfigs: newConfigs }; }); }} />
setFormData({ ...formData, durationDays: e.target.value })} />
{/* Custom dropdown for proper color support */}
{locationDropdownOpen && (
{Array.from(new Set(allLocations.map(l => l.block))).map(blockName => (
{blockName}
{allLocations.filter(l => l.block === blockName).map(loc => { const currentSelectedDate = formData.dayConfigs[0].date; const isBooked = bookedEvents.find(ev => { // Must match the location name if (!ev.date || ev.location !== loc.name) return false; // Skip the current event if we're editing it if (eventToEdit?.id && ev.id === eventToEdit.id) return false; // If no date is selected yet, treat ALL existing bookings for this location as conflicts if (!currentSelectedDate) return true; // Date overlap check: does the new event's date range overlap the existing one? const existingStart = new Date(ev.date); const existingEnd = new Date(ev.date); existingEnd.setDate(existingEnd.getDate() + (ev.duration_days || 1)); const newStart = new Date(currentSelectedDate); const newEnd = new Date(currentSelectedDate); newEnd.setDate(newEnd.getDate() + (parseInt(formData.durationDays) || 1)); return newStart < existingEnd && existingStart < newEnd; }); const isPending = isBooked?.verification_status === 'PENDING'; const isApproved = isBooked?.verification_status === 'APPROVED'; const isSelected = formData.location === loc.name; return ( ); })}
))}
)}
{/* Hidden required input for form validation */} {}} />
{/* Section 3: Itinerary */}

03. Itinerary Scheduling

{formData.dayConfigs.map((day, dIdx) => (
Day {dIdx + 1} Configuration
handleDayDateChange(dIdx, e.target.value)} />
{day.batches.length === 0 ? ( /* Unified Day View */
Start Time handleDayTimeChange(dIdx, 'startTime', e.target.value)} />
End Time handleDayTimeChange(dIdx, 'endTime', e.target.value)} />
{/* Resource Person for Unified Day */}

Resource Person

handleResourcePersonChange(dIdx, null, 'name', e.target.value)} />
{day.resourcePerson?.type === 'EXTERNAL' ? (
handleResourcePersonChange(dIdx, null, 'college_name', e.target.value)} />
handleResourcePersonChange(dIdx, null, 'dept', e.target.value)} />
) : (
handleResourcePersonChange(dIdx, null, 'dept', e.target.value)} />
)}
handleResourcePersonChange(dIdx, null, 'phone', e.target.value)} />
handleResourcePersonChange(dIdx, null, 'email', e.target.value)} />
) : (
{day.batches.map((batch, bIdx) => (
Phase 0{batch.id} Configuration
handleBatchTimeChange(dIdx, bIdx, 'startTime', e.target.value)} /> / handleBatchTimeChange(dIdx, bIdx, 'endTime', e.target.value)} />
{/* Resource Person for Batch */}
handleResourcePersonChange(dIdx, bIdx, 'name', e.target.value)} />
{batch.resourcePerson?.type === 'EXTERNAL' ? (
handleResourcePersonChange(dIdx, bIdx, 'college_name', e.target.value)} />
handleResourcePersonChange(dIdx, bIdx, 'dept', e.target.value)} />
) : (
handleResourcePersonChange(dIdx, bIdx, 'dept', e.target.value)} />
)}
handleResourcePersonChange(dIdx, bIdx, 'phone', e.target.value)} />
handleResourcePersonChange(dIdx, bIdx, 'email', e.target.value)} />
))}
)}
))}
{/* Section 4: Governance */}

04. Access Control

setFormData({ ...formData, registrationDeadline: e.target.value })} />
0 ? 'bg-slate-100 border-slate-100 text-slate-400 cursor-not-allowed' : 'bg-white border-slate-200 text-slate-900 focus:border-[#004a99]' }`} value={formData.maxParticipants} disabled={Object.keys(deptLimits).length > 0} onChange={e => setFormData({ ...formData, maxParticipants: e.target.value })} /> {Object.keys(deptLimits).length > 0 && (

↓ Controlled by Dept Quotas

)}

Collective Entry

Enable Team Registrations

{formData.isTeamEvent && (
['-', '+', 'e', 'E'].includes(e.key) && e.preventDefault()} placeholder="Size" className="w-20 bg-slate-50 border border-slate-100 rounded-xl px-3 py-2 text-[10px] font-black" value={formData.teamSizeLimit} onChange={e => setFormData({ ...formData, teamSizeLimit: e.target.value })} />
)}
{['INTERNAL', 'EXTERNAL', 'BOTH'].map(t => ( ))}
{/* Section 5: Quotas */}

05. Department Quotas

{formData.maxParticipants !== '' && parseInt(formData.maxParticipants) > 0 && (

Dept quotas locked — Total Capacity is set. Clear it first to use department-level limits.

)}
{DEPARTMENTS.slice(0, 10).map(dept => { const hasSectionLimits = deptSectionLimits[dept] && Object.keys(deptSectionLimits[dept]).length > 0; return (
{dept} 0 ? 'bg-slate-100 text-slate-300 cursor-not-allowed' : hasSectionLimits ? 'bg-blue-50 text-[#004a99] cursor-not-allowed' : 'bg-slate-50 focus:text-[#004a99]' }`} value={deptLimits[dept] || ''} disabled={(formData.maxParticipants !== '' && parseInt(formData.maxParticipants) > 0) || hasSectionLimits} onChange={(e) => handleDeptLimitChange(dept, e.target.value)} />
); })}
{/* Per-Department Section Quota Inputs */} {activeDeptForSections && (

Section limits for {activeDeptForSections}

Specify capacity for each section. Leaving a section blank will prevent students in that section from registering.

{['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'].map(sec => { const val = deptSectionLimits[activeDeptForSections]?.[sec] ?? ''; return (
Sec {sec} handleSectionLimitChange(activeDeptForSections, sec, e.target.value)} />
); })}
)}
{/* Section 6: Taxonomy */}

06. Taxonomy & Domain

Pricing Model

{/* Section 7: Assets */}

07. Digital Assets & Summary