1135 lines
66 KiB
TypeScript
1135 lines
66 KiB
TypeScript
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<CreateEventFormProps> = ({ 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 parseDateTimeForInput = (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');
|
|
const hh = String(date.getHours()).padStart(2, '0');
|
|
const min = String(date.getMinutes()).padStart(2, '0');
|
|
return `${yyyy}-${mm}-${dd}T${hh}:${min}`;
|
|
} 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<number, any[]>();
|
|
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 ? parseDateTimeForInput(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<any[]>([]);
|
|
const [bookedEvents, setBookedEvents] = useState<any[]>([]);
|
|
const [locationDropdownOpen, setLocationDropdownOpen] = useState(false);
|
|
const locationDropdownRef = useRef<HTMLDivElement>(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<Record<string, number>>(eventToEdit?.deptLimits || {});
|
|
const [deptSectionLimits, setDeptSectionLimits] = useState<Record<string, Record<string, number>>>(eventToEdit?.deptSectionLimits || {});
|
|
const [activeDeptForSections, setActiveDeptForSections] = useState<string | null>(null);
|
|
|
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
|
|
const [availableDomains, setAvailableDomains] = useState<any[]>([]);
|
|
|
|
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<HTMLInputElement>) => {
|
|
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;
|
|
}
|
|
|
|
if (formData.registrationDeadline) {
|
|
const deadline = new Date(formData.registrationDeadline);
|
|
if (isNaN(deadline.getTime())) {
|
|
alert("Please enter a valid registration deadline.");
|
|
return;
|
|
}
|
|
if (!eventToEdit && deadline < new Date()) {
|
|
alert("Validation Error: Registration deadline cannot be in the past.");
|
|
return;
|
|
}
|
|
const eventStart = new Date(formData.dayConfigs[0]?.date || formData.date);
|
|
if (!isNaN(eventStart.getTime()) && deadline > eventStart) {
|
|
alert("Validation Error: Registration deadline cannot be after the event start date.");
|
|
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<string, any> = {
|
|
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 (
|
|
<div className="w-full max-w-4xl mx-auto bg-white border border-slate-200 rounded-[2.5rem] shadow-[0_32px_64px_rgba(0,0,0,0.05)] animate-in zoom-in-95 duration-500 overflow-y-auto max-h-[92vh] no-scrollbar relative">
|
|
<div className="sticky top-0 z-50 bg-white/80 backdrop-blur-xl border-b border-slate-100 px-10 py-6 flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-2xl font-black text-slate-900 tracking-tight uppercase">
|
|
{eventToEdit ? 'Refine Session' : 'Generate Event'}
|
|
</h2>
|
|
<p className="text-[#004a99] font-black text-[9px] tracking-[0.3em] uppercase">Coordinator Hub Portal</p>
|
|
</div>
|
|
<button onClick={onCancel} className="w-10 h-10 rounded-xl bg-slate-50 flex items-center justify-center text-slate-400 border border-slate-100 hover:bg-rose-50 hover:text-rose-500 transition-all">
|
|
<i className="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className="p-10 space-y-12 pb-24">
|
|
{/* Section 1: Event Identity */}
|
|
<section className="space-y-6">
|
|
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] px-2 flex items-center gap-3">
|
|
<span className="w-1.5 h-1.5 bg-[#004a99] rounded-full"></span>
|
|
01. Core Identity
|
|
</h3>
|
|
<div className="bg-slate-50/50 border border-slate-100 rounded-3xl p-8 space-y-6">
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Session Title</label>
|
|
<input required type="text" placeholder="e.g. Innovate Summit 2024" className="w-full bg-white border border-slate-200 rounded-2xl px-6 py-4 text-slate-900 focus:border-[#004a99] outline-none font-bold text-sm shadow-sm transition-all" value={formData.title} onChange={e => setFormData({ ...formData, title: e.target.value })} />
|
|
</div>
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Faculty Coordinator</label>
|
|
<input required type="text" placeholder="Prof. Jane Doe" className="w-full bg-white border border-slate-200 rounded-2xl px-6 py-4 text-slate-900 focus:border-[#004a99] outline-none font-bold text-sm shadow-sm transition-all" value={formData.coordinator} onChange={e => setFormData({ ...formData, coordinator: e.target.value })} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Conducting Dept</label>
|
|
<select required className="w-full bg-white border border-slate-200 rounded-2xl px-6 py-4 text-slate-900 focus:border-[#004a99] font-bold text-sm outline-none shadow-sm transition-all appearance-none cursor-pointer" value={formData.conducting_dept} onChange={e => setFormData({ ...formData, conducting_dept: e.target.value })}>
|
|
<option value="">Select Dept</option>
|
|
{DEPARTMENTS.map(dept => <option key={dept} value={dept}>{dept}</option>)}
|
|
</select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Hosting Entity</label>
|
|
<select required className="w-full bg-white border border-slate-200 rounded-2xl px-6 py-4 text-slate-900 focus:border-[#004a99] font-bold text-sm outline-none shadow-sm transition-all appearance-none cursor-pointer" value={formData.club} onChange={e => setFormData({ ...formData, club: e.target.value })}>
|
|
<option value="">Select Entity</option>
|
|
<option value="Non-Club Event">Non-Club Event</option>
|
|
{CLUBS.map(club => <option key={club.name} value={club.name}>{club.name}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Section 1.5: Financial Governance (Admin Only View) */}
|
|
<section className="space-y-6">
|
|
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] px-2 flex items-center gap-3">
|
|
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full"></span>
|
|
01B. Financial Projections
|
|
</h3>
|
|
<div className="bg-slate-50/50 border border-slate-100 rounded-3xl p-8 space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Refreshment Coverage</label>
|
|
<div className="relative">
|
|
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 font-black text-sm">₹</span>
|
|
<input type="number" min="0" placeholder="0.00" className="w-full bg-white border border-slate-200 rounded-2xl pl-8 pr-6 py-4 text-slate-900 focus:border-[#004a99] outline-none font-bold text-sm shadow-sm transition-all" value={formData.refreshment_expense} onChange={e => setFormData({ ...formData, refreshment_expense: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Transportation Coverage</label>
|
|
<div className="relative">
|
|
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 font-black text-sm">₹</span>
|
|
<input type="number" min="0" placeholder="0.00" className="w-full bg-white border border-slate-200 rounded-2xl pl-8 pr-6 py-4 text-slate-900 focus:border-[#004a99] outline-none font-bold text-sm shadow-sm transition-all" value={formData.transportation_expense} onChange={e => setFormData({ ...formData, transportation_expense: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Session Coverage Fee</label>
|
|
<div className="relative">
|
|
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-slate-400 font-black text-sm">₹</span>
|
|
<input type="number" min="0" placeholder="0.00" className="w-full bg-white border border-slate-200 rounded-2xl pl-8 pr-6 py-4 text-slate-900 focus:border-[#004a99] outline-none font-bold text-sm shadow-sm transition-all" value={formData.session_coverage_fee} onChange={e => setFormData({ ...formData, session_coverage_fee: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Total Projected Expense</label>
|
|
<div className="relative">
|
|
<span className="absolute left-4 top-1/2 -translate-y-1/2 text-emerald-500 font-black text-sm">₹</span>
|
|
<input readOnly type="text" className="w-full bg-emerald-50 border border-emerald-100 rounded-2xl pl-8 pr-6 py-4 text-emerald-700 font-black text-sm outline-none cursor-not-allowed" value={formData.total_expense} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<p className="text-[8px] font-bold text-slate-400 uppercase tracking-widest flex items-center gap-2">
|
|
<i className="fas fa-lock text-amber-500"></i>
|
|
Financial data is visible to you and administrative auditors only.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Section 2: Logistics */}
|
|
<section className="space-y-6">
|
|
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] px-2 flex items-center gap-3">
|
|
<span className="w-1.5 h-1.5 bg-orange-500 rounded-full"></span>
|
|
02. Logistics & Hosting
|
|
</h3>
|
|
<div className="bg-slate-50/50 border border-slate-100 rounded-3xl p-8 space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
|
<div className="md:col-span-2 space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Launch Date</label>
|
|
<input required type="date" className="w-full bg-white border border-slate-200 rounded-2xl px-6 py-4 text-slate-900 focus:border-[#004a99] font-bold text-sm outline-none shadow-sm" value={formData.date} onChange={e => {
|
|
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 };
|
|
});
|
|
}} />
|
|
</div>
|
|
<div className="md:col-span-2 space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Duration (Days)</label>
|
|
<input type="number" min="1" className="w-full bg-white border border-slate-200 rounded-2xl px-6 py-4 text-slate-900 focus:border-[#004a99] font-bold text-sm outline-none shadow-sm" value={formData.durationDays} onChange={e => setFormData({ ...formData, durationDays: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Venue / Hub Location</label>
|
|
{/* Custom dropdown for proper color support */}
|
|
<div className="relative" ref={locationDropdownRef}>
|
|
<button
|
|
type="button"
|
|
onClick={() => setLocationDropdownOpen(!locationDropdownOpen)}
|
|
className="w-full bg-white border border-slate-200 rounded-2xl px-6 py-4 text-left focus:border-[#004a99] outline-none font-bold text-sm shadow-sm cursor-pointer flex items-center justify-between"
|
|
>
|
|
<span className={formData.location ? 'text-slate-900' : 'text-slate-400'}>
|
|
{formData.location || 'Select Venue'}
|
|
</span>
|
|
<svg className={`w-4 h-4 text-slate-400 transition-transform ${locationDropdownOpen ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" /></svg>
|
|
</button>
|
|
{locationDropdownOpen && (
|
|
<div className="absolute z-50 mt-2 w-full bg-white border border-slate-200 rounded-2xl shadow-xl max-h-72 overflow-y-auto">
|
|
{Array.from(new Set(allLocations.map(l => l.block))).map(blockName => (
|
|
<div key={blockName}>
|
|
<div className="px-5 py-2 text-[9px] font-black text-[#004a99] uppercase tracking-widest bg-slate-50 sticky top-0 border-b border-slate-100">
|
|
{blockName}
|
|
</div>
|
|
{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 (
|
|
<button
|
|
type="button"
|
|
key={loc.id}
|
|
disabled={!!isBooked}
|
|
onClick={() => {
|
|
if (!isBooked) {
|
|
setFormData({ ...formData, location: loc.name });
|
|
setLocationDropdownOpen(false);
|
|
}
|
|
}}
|
|
className={`w-full text-left px-5 py-3 text-[11px] font-bold flex items-center justify-between transition-all
|
|
${isBooked ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-slate-50'}
|
|
${isSelected && !isBooked ? 'bg-[#004a99]/5 text-[#004a99]' : ''}
|
|
${isPending ? 'bg-orange-50' : ''}
|
|
${isApproved ? 'bg-slate-50' : ''}
|
|
`}
|
|
>
|
|
<span className={`flex items-center gap-2 ${
|
|
isPending ? 'text-orange-500' : isApproved ? 'text-slate-400' : 'text-slate-800'
|
|
}`}>
|
|
{isPending && <span className="w-2 h-2 bg-orange-400 rounded-full animate-pulse"></span>}
|
|
{isApproved && <span className="w-2 h-2 bg-slate-400 rounded-full"></span>}
|
|
{isSelected && !isBooked && <span className="w-2 h-2 bg-[#004a99] rounded-full"></span>}
|
|
{loc.name}
|
|
</span>
|
|
<span className={`text-[8px] font-black uppercase tracking-widest ${
|
|
isPending ? 'text-orange-400' : isApproved ? 'text-slate-400' : 'hidden'
|
|
}`}>
|
|
{isPending ? 'Under Verification' : isApproved ? 'Booked' : ''}
|
|
</span>
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
{/* Hidden required input for form validation */}
|
|
<input type="text" required value={formData.location} className="sr-only" tabIndex={-1} onChange={() => {}} />
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Section 3: Itinerary */}
|
|
<section className="space-y-6">
|
|
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] px-2 flex items-center gap-3">
|
|
<span className="w-1.5 h-1.5 bg-emerald-500 rounded-full"></span>
|
|
03. Itinerary Scheduling
|
|
</h3>
|
|
<div className="space-y-4">
|
|
{formData.dayConfigs.map((day, dIdx) => (
|
|
<div key={dIdx} className="bg-white border border-slate-100 rounded-[2rem] p-8 shadow-sm space-y-8">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-[10px] font-black text-[#004a99] uppercase tracking-widest">Day {dIdx + 1} Configuration</span>
|
|
<div className="flex gap-4">
|
|
<input type="date" className="bg-slate-50 border border-slate-100 rounded-lg px-4 py-2 text-[10px] font-black outline-none" value={day.date} onChange={e => handleDayDateChange(dIdx, e.target.value)} />
|
|
<select className="bg-slate-50 border border-slate-100 rounded-lg px-4 py-2 text-[10px] font-black outline-none appearance-none cursor-pointer" value={day.batches.length} onChange={e => handleNumBatchesChange(dIdx, e.target.value)}>
|
|
{[1,2,3,4].map(n => <option key={n} value={n}>{n === 1 ? '1 Batch' : `${n} Batches`}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{day.batches.length === 0 ? (
|
|
/* Unified Day View */
|
|
<div className="space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="bg-slate-50 border border-slate-100 rounded-2xl px-6 py-4 flex items-center justify-between">
|
|
<span className="text-[9px] font-black text-slate-400 uppercase">Start Time</span>
|
|
<input type="time" className="bg-transparent text-[11px] font-black outline-none text-[#004a99]" value={day.startTime} onChange={e => handleDayTimeChange(dIdx, 'startTime', e.target.value)} />
|
|
</div>
|
|
<div className="bg-slate-50 border border-slate-100 rounded-2xl px-6 py-4 flex items-center justify-between">
|
|
<span className="text-[9px] font-black text-slate-400 uppercase">End Time</span>
|
|
<input type="time" className="bg-transparent text-[11px] font-black outline-none text-orange-500" value={day.endTime} onChange={e => handleDayTimeChange(dIdx, 'endTime', e.target.value)} />
|
|
</div>
|
|
</div>
|
|
{/* Resource Person for Unified Day */}
|
|
<div className="border-t border-slate-100 pt-6 space-y-4">
|
|
<p className="text-[9px] font-black text-[#004a99] uppercase tracking-widest">Resource Person</p>
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Identity Type</label>
|
|
<div className="flex bg-slate-50 rounded-xl p-1 gap-1">
|
|
<button type="button" onClick={() => handleResourcePersonChange(dIdx, null, 'type', 'INTERNAL')} className={`flex-1 py-2 rounded-lg text-[8px] font-black uppercase transition-all ${day.resourcePerson?.type !== 'EXTERNAL' ? 'bg-white text-[#004a99] shadow-sm' : 'text-slate-400'}`}>Internal</button>
|
|
<button type="button" onClick={() => handleResourcePersonChange(dIdx, null, 'type', 'EXTERNAL')} className={`flex-1 py-2 rounded-lg text-[8px] font-black uppercase transition-all ${day.resourcePerson?.type === 'EXTERNAL' ? 'bg-white text-orange-600 shadow-sm' : 'text-slate-400'}`}>External</button>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Resource Name</label>
|
|
<input type="text" placeholder="Full Name" className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2 text-[10px] font-black outline-none" value={day.resourcePerson?.name || ''} onChange={e => handleResourcePersonChange(dIdx, null, 'name', e.target.value)} />
|
|
</div>
|
|
{day.resourcePerson?.type === 'EXTERNAL' ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:col-span-2">
|
|
<div className="space-y-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Institution / College Name</label>
|
|
<input type="text" placeholder="Enter College Name" className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2 text-[10px] font-black outline-none" value={day.resourcePerson?.college_name || ''} onChange={e => handleResourcePersonChange(dIdx, null, 'college_name', e.target.value)} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Department</label>
|
|
<input type="text" placeholder="e.g. CSE, IT" className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2 text-[10px] font-black outline-none" value={day.resourcePerson?.dept || ''} onChange={e => handleResourcePersonChange(dIdx, null, 'dept', e.target.value)} />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2 md:col-span-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Department</label>
|
|
<input type="text" placeholder="e.g. CSE, IT" className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2 text-[10px] font-black outline-none" value={day.resourcePerson?.dept || ''} onChange={e => handleResourcePersonChange(dIdx, null, 'dept', e.target.value)} />
|
|
</div>
|
|
)}
|
|
<div className="space-y-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Phone Number</label>
|
|
<input type="tel" placeholder="+91 ..." className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2 text-[10px] font-black outline-none" value={day.resourcePerson?.phone || ''} onChange={e => handleResourcePersonChange(dIdx, null, 'phone', e.target.value)} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Gmail Address</label>
|
|
<input type="email" placeholder="example@gmail.com" className="w-full bg-slate-50 border border-slate-100 rounded-xl px-4 py-2 text-[10px] font-black outline-none" value={day.resourcePerson?.email || ''} onChange={e => handleResourcePersonChange(dIdx, null, 'email', e.target.value)} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="divide-y divide-slate-50">
|
|
{day.batches.map((batch, bIdx) => (
|
|
<div key={batch.id} className="py-8 first:pt-0 last:pb-0 space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Phase 0{batch.id} Configuration</span>
|
|
<div className="flex items-center gap-4 bg-slate-50 px-4 py-2 rounded-xl border border-slate-100">
|
|
<input type="time" className="bg-transparent text-[11px] font-black outline-none text-[#004a99]" value={batch.startTime} onChange={e => handleBatchTimeChange(dIdx, bIdx, 'startTime', e.target.value)} />
|
|
<span className="text-slate-300">/</span>
|
|
<input type="time" className="bg-transparent text-[11px] font-black outline-none text-orange-500" value={batch.endTime} onChange={e => handleBatchTimeChange(dIdx, bIdx, 'endTime', e.target.value)} />
|
|
</div>
|
|
</div>
|
|
|
|
{/* Resource Person for Batch */}
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 bg-slate-50/50 p-6 rounded-[1.5rem] border border-slate-100">
|
|
<div className="space-y-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Resource Type (Batch {batch.id})</label>
|
|
<div className="flex bg-white rounded-xl p-1 gap-1 border border-slate-100">
|
|
<button type="button" onClick={() => handleResourcePersonChange(dIdx, bIdx, 'type', 'INTERNAL')} className={`flex-1 py-2 rounded-lg text-[8px] font-black uppercase transition-all ${batch.resourcePerson?.type !== 'EXTERNAL' ? 'bg-[#004a99] text-white shadow-sm' : 'text-slate-400'}`}>Internal</button>
|
|
<button type="button" onClick={() => handleResourcePersonChange(dIdx, bIdx, 'type', 'EXTERNAL')} className={`flex-1 py-2 rounded-lg text-[8px] font-black uppercase transition-all ${batch.resourcePerson?.type === 'EXTERNAL' ? 'bg-orange-600 text-white shadow-sm' : 'text-slate-400'}`}>External</button>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Resource Name</label>
|
|
<input type="text" placeholder="Full Name" className="w-full bg-white border border-slate-100 rounded-xl px-4 py-2 text-[10px] font-black outline-none" value={batch.resourcePerson?.name || ''} onChange={e => handleResourcePersonChange(dIdx, bIdx, 'name', e.target.value)} />
|
|
</div>
|
|
{batch.resourcePerson?.type === 'EXTERNAL' ? (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:col-span-2">
|
|
<div className="space-y-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Institution / College Name</label>
|
|
<input type="text" placeholder="Enter College Name" className="w-full bg-white border border-slate-100 rounded-xl px-4 py-2 text-[10px] font-black outline-none" value={batch.resourcePerson?.college_name || ''} onChange={e => handleResourcePersonChange(dIdx, bIdx, 'college_name', e.target.value)} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Department</label>
|
|
<input type="text" placeholder="e.g. CSE, IT" className="w-full bg-white border border-slate-100 rounded-xl px-4 py-2 text-[10px] font-black outline-none" value={batch.resourcePerson?.dept || ''} onChange={e => handleResourcePersonChange(dIdx, bIdx, 'dept', e.target.value)} />
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2 md:col-span-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Department</label>
|
|
<input type="text" placeholder="e.g. CSE, IT" className="w-full bg-white border border-slate-100 rounded-xl px-4 py-2 text-[10px] font-black outline-none" value={batch.resourcePerson?.dept || ''} onChange={e => handleResourcePersonChange(dIdx, bIdx, 'dept', e.target.value)} />
|
|
</div>
|
|
)}
|
|
<div className="space-y-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Phone Number</label>
|
|
<input type="tel" placeholder="+91 ..." className="w-full bg-white border border-slate-100 rounded-xl px-4 py-2 text-[10px] font-black outline-none" value={batch.resourcePerson?.phone || ''} onChange={e => handleResourcePersonChange(dIdx, bIdx, 'phone', e.target.value)} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[8px] font-black text-slate-400 uppercase tracking-widest">Gmail Address</label>
|
|
<input type="email" placeholder="example@gmail.com" className="w-full bg-white border border-slate-100 rounded-xl px-4 py-2 text-[10px] font-black outline-none" value={batch.resourcePerson?.email || ''} onChange={e => handleResourcePersonChange(dIdx, bIdx, 'email', e.target.value)} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Section 4: Governance */}
|
|
<section className="space-y-6">
|
|
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] px-2 flex items-center gap-3">
|
|
<span className="w-1.5 h-1.5 bg-rose-500 rounded-full"></span>
|
|
04. Access Control
|
|
</h3>
|
|
<div className="bg-slate-50/50 border border-slate-100 rounded-3xl p-8 space-y-8">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Registration Closing Deadline</label>
|
|
<input type="datetime-local" className="w-full bg-white border border-slate-200 rounded-2xl px-6 py-4 text-slate-900 font-bold text-sm outline-none shadow-sm" value={formData.registrationDeadline} onChange={e => setFormData({ ...formData, registrationDeadline: e.target.value })} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Total Capacity</label>
|
|
<input
|
|
type="number"
|
|
placeholder="Unlimited"
|
|
className={`w-full border rounded-2xl px-6 py-4 font-bold text-sm outline-none shadow-sm transition-all ${
|
|
Object.keys(deptLimits).length > 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 && (
|
|
<p className="text-[8px] font-black text-amber-500 uppercase tracking-widest px-1">↓ Controlled by Dept Quotas</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between bg-white border border-slate-100 p-6 rounded-2xl">
|
|
<div>
|
|
<p className="text-[11px] font-black text-slate-900 uppercase tracking-widest">Collective Entry</p>
|
|
<p className="text-[8px] text-slate-400 uppercase font-black">Enable Team Registrations</p>
|
|
</div>
|
|
<div className="flex items-center gap-4">
|
|
{formData.isTeamEvent && (
|
|
<div className="flex gap-2">
|
|
<input type="number" min="1" onKeyDown={(e) => ['-', '+', '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 })} />
|
|
<select className="bg-slate-50 border border-slate-100 rounded-xl px-3 py-2 text-[10px] font-black outline-none" value={formData.teamComposition} onChange={e => setFormData({ ...formData, teamComposition: e.target.value as any })}>
|
|
<option value="MIXED">MIXED</option>
|
|
<option value="INTER_DEPT">INTER</option>
|
|
</select>
|
|
</div>
|
|
)}
|
|
<button type="button" onClick={() => setFormData(p => ({ ...p, isTeamEvent: !p.isTeamEvent }))} className={`w-12 h-6 rounded-full transition-all relative ${formData.isTeamEvent ? 'bg-[#004a99]' : 'bg-slate-200'}`}>
|
|
<div className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-all ${formData.isTeamEvent ? 'left-7' : 'left-1'}`}></div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Authorization Scope</label>
|
|
<div className="grid grid-cols-3 bg-white border border-slate-100 p-1.5 rounded-2xl gap-2">
|
|
{['INTERNAL', 'EXTERNAL', 'BOTH'].map(t => (
|
|
<button key={t} type="button" onClick={() => setFormData({ ...formData, participantType: t as any })} className={`py-3 rounded-xl text-[8px] font-black uppercase tracking-widest transition-all ${formData.participantType === t ? 'bg-[#004a99] text-white shadow-md' : 'text-slate-400 hover:text-slate-600'}`}>{t === 'INTERNAL' ? 'INT' : t === 'EXTERNAL' ? 'EXT' : 'ALL'}</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Section 5: Quotas */}
|
|
<section className="space-y-6">
|
|
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] px-2 flex items-center gap-3">
|
|
<span className="w-1.5 h-1.5 bg-indigo-500 rounded-full"></span>
|
|
05. Department Quotas
|
|
</h3>
|
|
<div className="bg-slate-50/50 border border-slate-100 rounded-3xl p-8">
|
|
{formData.maxParticipants !== '' && parseInt(formData.maxParticipants) > 0 && (
|
|
<div className="mb-6 p-4 bg-amber-50 border border-amber-100 rounded-2xl flex items-center gap-3">
|
|
<i className="fas fa-lock text-amber-500 text-sm"></i>
|
|
<p className="text-[9px] font-black text-amber-600 uppercase tracking-widest">Dept quotas locked — Total Capacity is set. Clear it first to use department-level limits.</p>
|
|
</div>
|
|
)}
|
|
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
|
{DEPARTMENTS.slice(0, 10).map(dept => {
|
|
const hasSectionLimits = deptSectionLimits[dept] && Object.keys(deptSectionLimits[dept]).length > 0;
|
|
return (
|
|
<div key={dept} className="bg-white border border-slate-100 rounded-2xl p-4 shadow-sm text-center flex flex-col justify-between">
|
|
<span className="block text-[8px] font-black text-slate-400 uppercase mb-2">{dept}</span>
|
|
<input
|
|
type="number"
|
|
placeholder="∞"
|
|
className={`w-full border-none text-[11px] font-black text-center outline-none rounded-lg py-1 transition-all ${
|
|
formData.maxParticipants !== '' && parseInt(formData.maxParticipants) > 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)}
|
|
/>
|
|
<button
|
|
type="button"
|
|
disabled={formData.maxParticipants !== '' && parseInt(formData.maxParticipants) > 0}
|
|
onClick={() => setActiveDeptForSections(activeDeptForSections === dept ? null : dept)}
|
|
className={`mt-2 w-full py-1.5 text-[8px] font-black uppercase tracking-wider rounded-lg border transition-all disabled:opacity-50 ${
|
|
hasSectionLimits
|
|
? 'bg-blue-50 text-[#004a99] border-blue-200 hover:bg-blue-100'
|
|
: 'bg-slate-50 text-slate-400 border-slate-100 hover:bg-slate-100'
|
|
}`}
|
|
>
|
|
{hasSectionLimits ? 'Sections Active' : 'Set Sections'}
|
|
</button>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
|
|
{/* Per-Department Section Quota Inputs */}
|
|
{activeDeptForSections && (
|
|
<div className="mt-8 p-6 bg-white border border-slate-100 rounded-3xl shadow-sm space-y-4 animate-in slide-in-from-top-4 duration-300">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h4 className="text-xs font-black text-slate-950 uppercase tracking-widest flex items-center gap-2">
|
|
<span className="w-1.5 h-1.5 bg-[#004a99] rounded-full"></span>
|
|
Section limits for {activeDeptForSections}
|
|
</h4>
|
|
<p className="text-[8px] font-black text-slate-400 uppercase tracking-widest mt-1">
|
|
Specify capacity for each section. Leaving a section blank will prevent students in that section from registering.
|
|
</p>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setActiveDeptForSections(null)}
|
|
className="px-4 py-2 bg-slate-900 hover:bg-slate-800 text-white rounded-xl text-[9px] font-black uppercase tracking-widest shadow-md transition-all active:scale-95"
|
|
>
|
|
Done
|
|
</button>
|
|
</div>
|
|
<div className="grid grid-cols-3 md:grid-cols-9 gap-3">
|
|
{['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I'].map(sec => {
|
|
const val = deptSectionLimits[activeDeptForSections]?.[sec] ?? '';
|
|
return (
|
|
<div key={sec} className="bg-slate-50 border border-slate-100 rounded-xl p-3 text-center">
|
|
<span className="block text-[8px] font-black text-slate-400 uppercase mb-2">Sec {sec}</span>
|
|
<input
|
|
type="number"
|
|
placeholder="0"
|
|
min="0"
|
|
className="w-full bg-white border border-slate-200 rounded-lg py-1.5 text-center font-black text-xs outline-none focus:border-[#004a99] transition-all"
|
|
value={val}
|
|
onChange={(e) => handleSectionLimitChange(activeDeptForSections, sec, e.target.value)}
|
|
/>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
|
|
{/* Section 6: Taxonomy */}
|
|
<section className="space-y-6">
|
|
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] px-2 flex items-center gap-3">
|
|
<span className="w-1.5 h-1.5 bg-blue-500 rounded-full"></span>
|
|
06. Taxonomy & Domain
|
|
</h3>
|
|
<div className="bg-slate-50/50 border border-slate-100 rounded-3xl p-8 space-y-6">
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Category</label>
|
|
<select className="w-full bg-white border border-slate-200 rounded-2xl px-6 py-4 text-slate-900 font-bold text-sm outline-none shadow-sm" value={formData.category} onChange={e => setFormData({ ...formData, category: e.target.value as any, domain: '' })}>
|
|
{categories.map(cat => <option key={cat} value={cat}>{cat}</option>)}
|
|
</select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Operational Domain</label>
|
|
<select required className="w-full bg-white border border-slate-200 rounded-2xl px-6 py-4 text-slate-900 font-bold text-sm outline-none shadow-sm" value={formData.domain} onChange={e => setFormData({ ...formData, domain: e.target.value })}>
|
|
<option value="" disabled>Select Pathway</option>
|
|
{availableDomains.map(dom => <option key={dom.id} value={dom.name}>{dom.name}</option>)}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center justify-between bg-white border border-slate-100 p-6 rounded-2xl">
|
|
<p className="text-[10px] font-black text-slate-900 uppercase tracking-widest">Pricing Model</p>
|
|
<div className="flex bg-slate-50 rounded-xl p-1">
|
|
<button type="button" onClick={() => setFormData({ ...formData, pricingType: 'FREE' })} className={`px-4 py-2 rounded-lg text-[9px] font-black uppercase tracking-widest transition-all ${formData.pricingType === 'FREE' ? 'bg-[#004a99] text-white shadow-md' : 'text-slate-400'}`}>FREE</button>
|
|
<button type="button" onClick={() => setFormData({ ...formData, pricingType: 'PAID' })} className={`px-4 py-2 rounded-lg text-[9px] font-black uppercase tracking-widest transition-all ${formData.pricingType === 'PAID' ? 'bg-rose-500 text-white shadow-md' : 'text-slate-400'}`}>PAID</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
{/* Section 7: Assets */}
|
|
<section className="space-y-6">
|
|
<h3 className="text-[10px] font-black text-slate-400 uppercase tracking-[0.3em] px-2 flex items-center gap-3">
|
|
<span className="w-1.5 h-1.5 bg-yellow-500 rounded-full"></span>
|
|
07. Digital Assets & Summary
|
|
</h3>
|
|
<div className="bg-slate-50/50 border border-slate-100 rounded-3xl p-8 space-y-8">
|
|
<button type="button" onClick={() => fileInputRef.current?.click()} className="w-full h-48 bg-white border-2 border-dashed border-slate-200 rounded-[2rem] flex flex-col items-center justify-center gap-3 hover:border-[#004a99] transition-all overflow-hidden relative group">
|
|
{formData.image ? (
|
|
<div className="absolute inset-0">
|
|
<img src={formData.image} alt="Banner" className="w-full h-full object-cover" />
|
|
<div className="absolute inset-0 bg-black/40 opacity-0 group-hover:opacity-100 transition-all flex items-center justify-center text-white text-[10px] font-black uppercase tracking-widest">Update Artwork</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<i className="fas fa-image text-3xl text-slate-200 group-hover:text-[#004a99] transition-colors"></i>
|
|
<span className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Upload Cover Banner</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
<input type="file" ref={fileInputRef} onChange={handleImageChange} accept="image/*" className="hidden" />
|
|
|
|
<div className="space-y-2">
|
|
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest px-1">Executive Summary</label>
|
|
<textarea placeholder="Describe the mission and scope..." className="w-full bg-white border border-slate-200 rounded-2xl px-6 py-4 text-sm font-bold min-h-[120px] outline-none focus:border-[#004a99] shadow-sm" value={formData.event_summary} onChange={e => setFormData({ ...formData, event_summary: e.target.value })} />
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
|
|
|
|
{/* Submit Section */}
|
|
<div className="pt-10 space-y-8">
|
|
<div className="p-8 bg-slate-900 rounded-[2rem] text-center space-y-3">
|
|
<i className="fas fa-shield-alt text-amber-500 text-xl"></i>
|
|
<h4 className="text-[10px] font-black text-white uppercase tracking-widest leading-none">Security Protocol Active</h4>
|
|
<p className="text-[8px] text-slate-500 font-black uppercase tracking-widest leading-relaxed">Broadcast will be suspended until administrative audit is complete.</p>
|
|
</div>
|
|
|
|
<button type="submit" disabled={isSubmitting} className="w-full py-8 bg-[#004a99] text-white font-black uppercase tracking-[0.4em] text-xs rounded-3xl shadow-xl shadow-[#004a99]/20 hover:bg-[#003a7a] transition-all active:scale-[0.98] disabled:opacity-50">
|
|
{isSubmitting ? <i className="fas fa-circle-notch fa-spin"></i> : "GENERATE SESSION BROADCAST"}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
|
|
};
|
|
|
|
export default CreateEventForm; |