Dynamic class strength caclculations set

This commit is contained in:
2026-06-23 14:15:04 +05:30
parent 4deb09d930
commit 8af314c629
6 changed files with 132 additions and 670 deletions

View File

@@ -602,6 +602,8 @@ export const ClassManagement: React.FC = () => {
<div className="grid grid-cols-2 gap-3"> <div className="grid grid-cols-2 gap-3">
{batchDeptClassesList.map(cls => { {batchDeptClassesList.map(cls => {
const isChecked = batch.classes?.includes(cls); const isChecked = batch.classes?.includes(cls);
const isAssignedElsewhere = tempBatches.some(other => other.id !== batch.id && other.classes?.includes(cls));
if (isAssignedElsewhere) return null;
return ( return (
<label key={cls} className="flex items-center gap-2 bg-white p-3 rounded-xl border border-slate-200 hover:border-brand-indigo/30 transition-all cursor-pointer"> <label key={cls} className="flex items-center gap-2 bg-white p-3 rounded-xl border border-slate-200 hover:border-brand-indigo/30 transition-all cursor-pointer">
<input <input

View File

@@ -132,85 +132,7 @@ export const EventDetailsModal: React.FC<EventDetailsModalProps> = ({ isOpen, on
</div> </div>
)} )}
{/* Schedule & Itinerary */}
{event.dayConfigs && event.dayConfigs.length > 0 && (
<div>
<h3 className="text-sm font-black text-text-dark mb-3 flex items-center gap-2">
<CalendarIcon className="w-4 h-4 text-brand-indigo" />
Detailed Schedule & Itinerary
</h3>
<div className="space-y-4">
{event.dayConfigs.map((day: any, dIdx: number) => {
const dayDate = day.date ? new Date(day.date) : null;
return (
<div key={dIdx} className="p-5 rounded-2xl bg-slate-50 border border-slate-100 space-y-4">
<div className="flex justify-between items-center border-b border-slate-200 pb-2">
<span className="text-xs font-black text-brand-navy uppercase tracking-wider">
Day {dIdx + 1}: {dayDate && !isNaN(dayDate.getTime()) ? format(dayDate, 'MMMM d, yyyy') : day.date || 'TBD'}
</span>
<span className="px-2 py-0.5 bg-brand-glow text-brand-indigo text-[9px] font-black uppercase rounded-md">
{day.batches?.length || 0} {(day.batches?.length || 0) === 1 ? 'Batch' : 'Batches'}
</span>
</div>
<div className="space-y-4 divide-y divide-slate-200/50">
{day.batches?.map((batch: any, bIdx: number) => (
<div key={bIdx} className="pt-4 first:pt-0 space-y-3">
<div className="flex justify-between items-center">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">
Batch {batch.id || bIdx + 1}
</span>
<span className="text-xs font-bold text-brand-indigo bg-white px-2.5 py-1 rounded-lg border border-slate-200 shadow-sm flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5" />
{batch.startTime || 'TBD'} - {batch.endTime || 'TBD'}
</span>
</div>
{batch.resourcePerson && batch.resourcePerson.name && (
<div className="p-4 bg-white rounded-xl border border-slate-100/80 space-y-2">
<div className="flex justify-between items-start">
<div>
<p className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Resource Person</p>
<p className="text-xs font-bold text-text-dark">{batch.resourcePerson.name}</p>
</div>
<span className={cn(
"px-2.5 py-0.5 rounded-md text-[8px] font-black uppercase tracking-wider",
batch.resourcePerson.type === 'EXTERNAL' ? "bg-amber-50 text-amber-600 border border-amber-100" : "bg-brand-glow text-brand-indigo border border-brand-indigo/10"
)}>
{batch.resourcePerson.type || 'INTERNAL'}
</span>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2 text-[10px] text-text-muted font-semibold">
{batch.resourcePerson.dept && (
<div>
<span className="font-bold text-slate-400 uppercase tracking-wider">Dept:</span> {batch.resourcePerson.dept}
</div>
)}
{batch.resourcePerson.college_name && (
<div className="col-span-2">
<span className="font-bold text-slate-400 uppercase tracking-wider">Inst:</span> {batch.resourcePerson.college_name}
</div>
)}
{batch.resourcePerson.phone && (
<div>
<span className="font-bold text-slate-400 tracking-wider uppercase">Phone:</span> {batch.resourcePerson.phone}
</div>
)}
{batch.resourcePerson.email && (
<div>
<span className="font-bold text-slate-400 tracking-wider uppercase">Email:</span> {batch.resourcePerson.email}
</div>
)}
</div>
</div>
)}
</div>
))}
</div>
</div>
);
})}
</div>
</div>
)}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
{event.academicYears && event.academicYears.length > 0 && ( {event.academicYears && event.academicYears.length > 0 && (
@@ -252,45 +174,7 @@ export const EventDetailsModal: React.FC<EventDetailsModalProps> = ({ isOpen, on
)} )}
</div> </div>
{/* Department Limits & Quotas */}
{event.deptLimits && Object.keys(event.deptLimits).length > 0 && (
<div>
<h3 className="text-sm font-black text-text-dark mb-3 flex items-center gap-2">
<Target className="w-4 h-4 text-brand-indigo" />
Department Quotas & Limits
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{Object.entries(event.deptLimits).map(([dept, maxSeats]) => {
const current = event.currentDeptCounts?.[dept] || 0;
const sectionLimits = event.deptSectionLimits?.[dept];
const hasSectionLimits = sectionLimits && Object.keys(sectionLimits).length > 0;
return (
<div key={dept} className="bg-slate-50 border border-slate-100 rounded-2xl p-4 space-y-3">
<div className="flex justify-between items-center">
<span className="text-xs font-black text-brand-navy uppercase tracking-wider">{dept}</span>
<span className="px-2 py-0.5 bg-brand-indigo text-white text-[9px] font-black uppercase rounded-md">
{current} / {maxSeats} Seats
</span>
</div>
{hasSectionLimits && (
<div className="pt-2 border-t border-slate-200/60 grid grid-cols-3 gap-2">
{Object.entries(sectionLimits).map(([sec, limit]) => {
const secCount = event.currentDeptSectionCounts?.[dept]?.[sec] || 0;
return (
<div key={sec} className="bg-white px-2 py-1.5 rounded-xl border border-slate-100 text-center">
<span className="block text-[8px] font-black text-slate-400 uppercase mb-0.5">Sec {sec}</span>
<span className="text-[10px] font-black text-brand-navy">{secCount} / {limit}</span>
</div>
);
})}
</div>
)}
</div>
);
})}
</div>
</div>
)}
{/* Global registration capacity */} {/* Global registration capacity */}
{event.maxParticipants && ( {event.maxParticipants && (

View File

@@ -205,30 +205,26 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
} }
}; };
const buildDayConfigsFromSchedule = (): any[] => { const formatDateTimeForInput = (dateStr?: string) => {
const editData = initialData as any; if (!dateStr) return '';
if (!editData?.dayConfigs || editData.dayConfigs.length === 0) { try {
const launchDateStr = editData?.startDate ? parseDateForInput(editData.startDate) : ''; const date = new Date(dateStr);
const batchStartTime = editData?.startDate ? editData.startDate.substring(11, 16) : ''; if (isNaN(date.getTime())) return '';
const batchEndTime = editData?.endDate ? editData.endDate.substring(11, 16) : ''; const offset = date.getTimezoneOffset();
return [{ const localDate = new Date(date.getTime() - offset * 60 * 1000);
date: launchDateStr, return localDate.toISOString().substring(0, 16);
batches: [{ id: 1, startTime: batchStartTime, endTime: batchEndTime }], } catch {
startTime: '', return '';
endTime: '',
}];
} }
return editData.dayConfigs;
}; };
const [deptLimits, setDeptLimits] = useState<Record<string, number>>((initialData as any)?.deptLimits || {}); const deptLimits = {};
const [deptSectionLimits, setDeptSectionLimits] = useState<Record<string, Record<string, number>>>((initialData as any)?.deptSectionLimits || {}); const deptSectionLimits = {};
const [activeDeptForSections, setActiveDeptForSections] = useState<string | null>(null);
const [formData, setFormData] = useState({ const [formData, setFormData] = useState({
eventName: initialData?.eventName || '', eventName: initialData?.eventName || '',
startDate: initialData?.startDate || '', startDate: initialData?.startDate ? formatDateTimeForInput(initialData.startDate) : '',
endDate: initialData?.endDate || '', endDate: initialData?.endDate ? formatDateTimeForInput(initialData.endDate) : '',
eventType: isClubVariant ? 'CLUB' : (initialData?.eventType || 'Guest Lecture'), eventType: isClubVariant ? 'CLUB' : (initialData?.eventType || 'Guest Lecture'),
category: isClubVariant ? 'CLUB' : (initialData?.category || 'ACADEMIC'), category: isClubVariant ? 'CLUB' : (initialData?.category || 'ACADEMIC'),
institution: initialData?.institution || 'RIT', institution: initialData?.institution || 'RIT',
@@ -256,11 +252,6 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
image: initialData?.image || '', image: initialData?.image || '',
status: initialData?.status || 'REQUESTED', status: initialData?.status || 'REQUESTED',
targetedBatch: (initialData as any)?.targetedBatch || '', targetedBatch: (initialData as any)?.targetedBatch || '',
// New fields
launchDate: initialData?.startDate ? parseDateForInput(initialData.startDate) : '',
durationDays: (initialData as any)?.durationDays?.toString() || '1',
dayConfigs: buildDayConfigsFromSchedule(),
refreshment_expense: (initialData as any)?.refreshment_expense?.toString() || '', refreshment_expense: (initialData as any)?.refreshment_expense?.toString() || '',
transportation_expense: (initialData as any)?.transportation_expense?.toString() || '', transportation_expense: (initialData as any)?.transportation_expense?.toString() || '',
session_coverage_fee: (initialData as any)?.session_coverage_fee?.toString() || '', session_coverage_fee: (initialData as any)?.session_coverage_fee?.toString() || '',
@@ -268,39 +259,6 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
maxParticipants: (initialData as any)?.maxParticipants?.toString() || '' maxParticipants: (initialData as any)?.maxParticipants?.toString() || ''
}); });
// Sync Launch Date to Day 1 configuration
useEffect(() => {
if (formData.launchDate && formData.dayConfigs.length > 0) {
setFormData(prev => {
const newConfigs = [...prev.dayConfigs];
newConfigs[0] = { ...newConfigs[0], date: formData.launchDate };
return { ...prev, dayConfigs: newConfigs };
});
}
}, [formData.launchDate]);
// Sync Duration Days to dayConfigs array length
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++) {
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]);
// Sync financial totals // Sync financial totals
useEffect(() => { useEffect(() => {
const total = (parseFloat(formData.refreshment_expense) || 0) + const total = (parseFloat(formData.refreshment_expense) || 0) +
@@ -309,141 +267,9 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
setFormData(prev => ({ ...prev, total_expense: total.toFixed(2) })); setFormData(prev => ({ ...prev, total_expense: total.toFixed(2) }));
}, [formData.refreshment_expense, formData.transportation_expense, formData.session_coverage_fee]); }, [formData.refreshment_expense, formData.transportation_expense, formData.session_coverage_fee]);
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];
}
const sectionSum = newSectionLimits[dept]
? Object.values(newSectionLimits[dept]).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 {
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: string, value: any) => {
setFormData(prev => {
const newConfigs = [...prev.dayConfigs];
if (batchIndex === null) {
const currentRP = newConfigs[dayIndex].resourcePerson || { type: 'INTERNAL', name: '', phone: '', email: '' };
newConfigs[dayIndex] = { ...newConfigs[dayIndex], resourcePerson: { ...currentRP, [field]: value } };
} else {
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 calculateEventDateTimes = () => {
let earliestStart: Date | null = null;
let latestEnd: Date | null = null;
for (let dIdx = 0; dIdx < formData.dayConfigs.length; dIdx++) {
const day = formData.dayConfigs[dIdx];
if (!day.date) continue;
for (let bIdx = 0; bIdx < day.batches.length; bIdx++) {
const batch = day.batches[bIdx];
if (!batch.startTime || !batch.endTime) continue;
const batchStart = new Date(`${day.date}T${batch.startTime}`);
const batchEnd = new Date(`${day.date}T${batch.endTime}`);
if (isNaN(batchStart.getTime()) || isNaN(batchEnd.getTime())) continue;
if (batchEnd <= batchStart) continue;
if (!earliestStart || batchStart < earliestStart) {
earliestStart = batchStart;
}
if (!latestEnd || batchEnd > latestEnd) {
latestEnd = batchEnd;
}
}
}
return { earliestStart, latestEnd };
};
const [newSponsor, setNewSponsor] = useState(''); const [newSponsor, setNewSponsor] = useState('');
const [showAlternateEvent, setShowAlternateEvent] = useState(false); const [showAlternateEvent, setShowAlternateEvent] = useState(false);
@@ -500,14 +326,27 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
const response = await fetch(API_BASE_URL + '/api/admin/users'); const response = await fetch(API_BASE_URL + '/api/admin/users');
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
// Count students dynamically in each class combo
const studentCounts: Record<string, number> = {};
data.forEach((u: any) => {
if (u.role === 'STUDENT' && u.department && u.year && u.section) {
const key = `${u.department.trim().toLowerCase()}_${u.year.trim().toLowerCase()}_${u.section.trim().toLowerCase()}`;
studentCounts[key] = (studentCounts[key] || 0) + 1;
}
});
const strengths = data const strengths = data
.filter((u: any) => u.isClassIncharge) .filter((u: any) => u.isClassIncharge)
.map((u: any) => ({ .map((u: any) => {
dept: u.inchargeClass, const key = `${u.inchargeClass?.trim().toLowerCase()}_${u.inchargeBatch?.trim().toLowerCase()}_${u.inchargeSection?.trim().toLowerCase()}`;
year: u.inchargeBatch, return {
sec: u.inchargeSection, dept: u.inchargeClass,
strength: u.classStrength || 0 year: u.inchargeBatch,
})); sec: u.inchargeSection,
strength: studentCounts[key] || 0
};
});
setClassStrengths(strengths); setClassStrengths(strengths);
} }
} catch (err) { } catch (err) {
@@ -612,7 +451,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
for (const s of sectionsToTarget) { for (const s of sectionsToTarget) {
const match = classStrengths.find(cs => cs.dept === d && cs.year === y && cs.sec === s); const match = classStrengths.find(cs => cs.dept === d && cs.year === y && cs.sec === s);
if (match) { if (match && match.strength > 0) {
total += match.strength; total += match.strength;
} }
} }
@@ -621,7 +460,33 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
return total; return total;
}, [formData.targetDepartments, formData.department, formData.academicYears, formData.targetedSections, formData.allBatches, formData.allSections, formData.institution, classStrengths, eventScope, isPlacementCell, availableYears, classes]); }, [formData.targetDepartments, formData.department, formData.academicYears, formData.targetedSections, formData.allBatches, formData.allSections, formData.institution, classStrengths, eventScope, isPlacementCell, availableYears, classes]);
const filteredVenues = venues.filter(v => v.capacity === null || v.capacity >= totalExpectedStrength); 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]);
useEffect(() => {
if (formData.venue && formData.venue !== 'N/A' && formData.venue !== 'Others.') {
const isStillAvailable = filteredVenues.some(v => v.name === formData.venue);
if (!isStillAvailable) {
setFormData(prev => ({ ...prev, venue: 'N/A' }));
}
}
}, [filteredVenues]);
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
@@ -629,12 +494,11 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
setError(''); setError('');
const now = new Date(); const now = new Date();
const earliestStart = new Date(formData.startDate);
const latestEnd = new Date(formData.endDate);
// Calculate overall start and end dates from dayConfigs if (isNaN(earliestStart.getTime()) || isNaN(latestEnd.getTime())) {
const { earliestStart, latestEnd } = calculateEventDateTimes(); setError('Please select valid start and end dates & times.');
if (!earliestStart || !latestEnd) {
setError('Please configure the schedule and timing for the event.');
setIsSubmitting(false); setIsSubmitting(false);
return; return;
} }
@@ -645,6 +509,12 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
return; return;
} }
if (latestEnd <= earliestStart) {
setError('End date/time must be after the start date/time.');
setIsSubmitting(false);
return;
}
const groupRequestId = crypto.randomUUID(); const groupRequestId = crypto.randomUUID();
const batchesToAssign = formData.allBatches ? availableYears : formData.academicYears; const batchesToAssign = formData.allBatches ? availableYears : formData.academicYears;
let deptsToCreate = (eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? formData.targetDepartments : [formData.department]; let deptsToCreate = (eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) ? formData.targetDepartments : [formData.department];
@@ -682,7 +552,8 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
deptLimits, deptLimits,
deptSectionLimits, deptSectionLimits,
maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants), maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants),
durationDays: Number(formData.durationDays), durationDays: 1,
dayConfigs: [],
refreshment_expense: Number(formData.refreshment_expense) || 0, refreshment_expense: Number(formData.refreshment_expense) || 0,
transportation_expense: Number(formData.transportation_expense) || 0, transportation_expense: Number(formData.transportation_expense) || 0,
session_coverage_fee: Number(formData.session_coverage_fee) || 0, session_coverage_fee: Number(formData.session_coverage_fee) || 0,
@@ -724,7 +595,8 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
deptLimits, deptLimits,
deptSectionLimits, deptSectionLimits,
maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants), maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants),
durationDays: Number(formData.durationDays), durationDays: 1,
dayConfigs: [],
refreshment_expense: Number(formData.refreshment_expense) || 0, refreshment_expense: Number(formData.refreshment_expense) || 0,
transportation_expense: Number(formData.transportation_expense) || 0, transportation_expense: Number(formData.transportation_expense) || 0,
session_coverage_fee: Number(formData.session_coverage_fee) || 0, session_coverage_fee: Number(formData.session_coverage_fee) || 0,
@@ -776,7 +648,8 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
deptLimits, deptLimits,
deptSectionLimits, deptSectionLimits,
maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants), maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants),
durationDays: Number(formData.durationDays), durationDays: 1,
dayConfigs: [],
refreshment_expense: Number(formData.refreshment_expense) || 0, refreshment_expense: Number(formData.refreshment_expense) || 0,
transportation_expense: Number(formData.transportation_expense) || 0, transportation_expense: Number(formData.transportation_expense) || 0,
session_coverage_fee: Number(formData.session_coverage_fee) || 0, session_coverage_fee: Number(formData.session_coverage_fee) || 0,
@@ -1053,24 +926,29 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
/> />
</div> </div>
)} )}
{totalExpectedStrength > 0 && (
<p className="text-[9px] font-black text-brand-indigo uppercase tracking-widest mt-2 px-1">
Smart Allocation: Recommending venues matching ~{totalExpectedStrength} seats
</p>
)}
</div> </div>
<div> <div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Launch Date</label> <label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Start Date & Time</label>
<input <input
type="date" required type="datetime-local" required
value={formData.launchDate} value={formData.startDate}
onChange={e => setFormData({...formData, launchDate: e.target.value})} onChange={e => setFormData({...formData, startDate: e.target.value})}
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all" className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all"
/> />
</div> </div>
<div> <div>
<label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">Duration (Days)</label> <label className="text-[10px] font-black uppercase tracking-widest text-text-muted mb-3 block">End Date & Time</label>
<input <input
type="number" required min="1" type="datetime-local" required
value={formData.durationDays} value={formData.endDate}
onChange={e => setFormData({...formData, durationDays: e.target.value})} onChange={e => setFormData({...formData, endDate: e.target.value})}
className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all" className="w-full bg-slate-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold focus:bg-white focus:border-brand-indigo/30 transition-all"
/> />
</div> </div>
@@ -1081,112 +959,15 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
</label> </label>
<input <input
type="number" type="number"
placeholder={Object.keys(deptLimits).length > 0 ? "Controlled by Dept Quotas" : "Unlimited"} placeholder="Unlimited"
className={cn( className="w-full border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold transition-all focus:bg-white bg-slate-50 text-text-dark focus:border-brand-indigo/30"
"w-full border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold transition-all focus:bg-white",
Object.keys(deptLimits).length > 0
? "bg-slate-100 text-slate-400 cursor-not-allowed"
: "bg-slate-50 text-text-dark focus:border-brand-indigo/30"
)}
value={formData.maxParticipants} value={formData.maxParticipants}
disabled={Object.keys(deptLimits).length > 0}
onChange={e => setFormData({...formData, maxParticipants: e.target.value})} onChange={e => setFormData({...formData, maxParticipants: e.target.value})}
/> />
{Object.keys(deptLimits).length > 0 && (
<p className="text-[9px] font-black text-amber-500 uppercase tracking-widest mt-2 px-1">
Controlled by Dept Quotas
</p>
)}
</div> </div>
</div> </div>
<div className="h-px bg-slate-100" />
{/* Section 3: Itinerary Scheduling */}
<div className="space-y-6">
<h3 className="text-xl font-black text-text-dark flex items-center gap-3">
<Calendar className="w-6 h-6 text-brand-indigo" />
03. Itinerary Scheduling
</h3>
<div className="space-y-6">
{formData.dayConfigs.map((day, dIdx) => (
<div key={dIdx} className="bg-slate-50/50 border border-slate-100 rounded-[2rem] p-8 shadow-sm space-y-6">
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<span className="text-xs font-black text-brand-navy uppercase tracking-wider">Day {dIdx + 1} Configuration</span>
<div className="flex flex-wrap gap-4 w-full sm:w-auto">
<div className="flex-1 sm:flex-initial">
<input type="date" required className="w-full bg-white border border-slate-200 rounded-xl px-4 py-2 text-xs font-bold outline-none" value={day.date} onChange={e => handleDayDateChange(dIdx, e.target.value)} />
</div>
<div className="flex-1 sm:flex-initial">
<select className="w-full bg-white border border-slate-200 rounded-xl px-4 py-2 text-xs font-bold 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>
</div>
<div className="divide-y divide-slate-100/50">
{day.batches.map((batch, bIdx) => (
<div key={batch.id} className="py-6 first:pt-0 last:pb-0 space-y-6">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<span className="text-[10px] font-black text-slate-400 uppercase tracking-widest">Phase 0{batch.id} Configuration</span>
<div className="flex items-center gap-3 bg-white px-4 py-2 rounded-xl border border-slate-200 w-full sm:w-auto justify-between">
<span className="text-[10px] font-black text-slate-400 uppercase">Start</span>
<input type="time" required className="bg-transparent text-xs font-black outline-none text-brand-indigo" value={batch.startTime} onChange={e => handleBatchTimeChange(dIdx, bIdx, 'startTime', e.target.value)} />
<span className="text-slate-300">/</span>
<span className="text-[10px] font-black text-slate-400 uppercase">End</span>
<input type="time" required className="bg-transparent text-xs font-black outline-none text-brand-indigo" 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-6 bg-white p-6 rounded-[1.5rem] border border-slate-100">
<div className="space-y-2">
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Resource Type (Batch {batch.id})</label>
<div className="flex bg-slate-50 rounded-xl p-1 gap-1 border border-slate-100">
<button type="button" onClick={() => handleResourcePersonChange(dIdx, bIdx, 'type', 'INTERNAL')} className={cn("flex-1 py-2 rounded-lg text-[9px] font-black uppercase transition-all", batch.resourcePerson?.type !== 'EXTERNAL' ? "bg-brand-indigo text-white shadow-sm" : "text-slate-400")}>Internal</button>
<button type="button" onClick={() => handleResourcePersonChange(dIdx, bIdx, 'type', 'EXTERNAL')} className={cn("flex-1 py-2 rounded-lg text-[9px] font-black uppercase transition-all", batch.resourcePerson?.type === 'EXTERNAL' ? "bg-brand-indigo text-white shadow-sm" : "text-slate-400")}>External</button>
</div>
</div>
<div className="space-y-2">
<label className="text-[9px] 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-transparent rounded-xl px-4 py-3 text-xs font-bold outline-none focus:bg-white focus:border-brand-indigo/35 transition-all" value={batch.resourcePerson?.name || ''} onChange={e => handleResourcePersonChange(dIdx, bIdx, 'name', e.target.value)} />
</div>
{batch.resourcePerson?.type === 'EXTERNAL' ? (
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6 md:col-span-2">
<div className="space-y-2">
<label className="text-[9px] 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-transparent rounded-xl px-4 py-3 text-xs font-bold outline-none focus:bg-white focus:border-brand-indigo/35 transition-all" value={batch.resourcePerson?.college_name || ''} onChange={e => handleResourcePersonChange(dIdx, bIdx, 'college_name', e.target.value)} />
</div>
<div className="space-y-2">
<label className="text-[9px] 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-transparent rounded-xl px-4 py-3 text-xs font-bold outline-none focus:bg-white focus:border-brand-indigo/35 transition-all" 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-[9px] 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-transparent rounded-xl px-4 py-3 text-xs font-bold outline-none focus:bg-white focus:border-brand-indigo/35 transition-all" value={batch.resourcePerson?.dept || ''} onChange={e => handleResourcePersonChange(dIdx, bIdx, 'dept', e.target.value)} />
</div>
)}
<div className="space-y-2">
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Phone Number</label>
<input type="tel" placeholder="+91 ..." className="w-full bg-slate-50 border border-transparent rounded-xl px-4 py-3 text-xs font-bold outline-none focus:bg-white focus:border-brand-indigo/35 transition-all" value={batch.resourcePerson?.phone || ''} onChange={e => handleResourcePersonChange(dIdx, bIdx, 'phone', e.target.value)} />
</div>
<div className="space-y-2">
<label className="text-[9px] font-black text-slate-400 uppercase tracking-widest">Email Address</label>
<input type="email" placeholder="example@gmail.com" className="w-full bg-slate-50 border border-transparent rounded-xl px-4 py-3 text-xs font-bold outline-none focus:bg-white focus:border-brand-indigo/35 transition-all" value={batch.resourcePerson?.email || ''} onChange={e => handleResourcePersonChange(dIdx, bIdx, 'email', e.target.value)} />
</div>
</div>
</div>
))}
</div>
</div>
))}
</div>
</div>
<div className="h-px bg-slate-100" />
<div className="space-y-8"> <div className="space-y-8">
<h3 className="text-xl font-black text-text-dark flex items-center gap-3"> <h3 className="text-xl font-black text-text-dark flex items-center gap-3">
@@ -1568,49 +1349,6 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
</div> </div>
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
{deptBatches.length > 0 && (
<div>
<select
value={formData.targetedBatch || ''}
onChange={e => {
const selectedBatchName = e.target.value;
const matchedBatch = deptBatches.find(b => b.name === selectedBatchName);
if (matchedBatch) {
const targetYearsSet = new Set<string>();
const targetSecsSet = new Set<string>();
matchedBatch.classes.forEach((cls: string) => {
const parts = cls.split(' - ');
if (parts.length === 2) {
targetYearsSet.add(parts[0]);
targetSecsSet.add(parts[1]);
}
});
setFormData({
...formData,
targetedBatch: selectedBatchName,
academicYears: Array.from(targetYearsSet),
targetedSections: Array.from(targetSecsSet),
allSections: false
});
} else {
setFormData({
...formData,
targetedBatch: '',
academicYears: [],
targetedSections: [],
allSections: false
});
}
}}
className="w-full bg-indigo-50 border border-transparent rounded-2xl py-4 px-6 text-sm font-semibold appearance-none text-brand-indigo"
>
<option value="">-- Target Custom Batch --</option>
{deptBatches.map(b => <option key={b.id} value={b.name}>{b.name}</option>)}
</select>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-widest mt-1.5 ml-2">Or select a standard academic year below:</p>
</div>
)}
<select <select
value={formData.academicYears[0] || ''} value={formData.academicYears[0] || ''}
onChange={e => setFormData({...formData, targetedBatch: '', academicYears: [e.target.value], targetedSections: [], allSections: false})} onChange={e => setFormData({...formData, targetedBatch: '', academicYears: [e.target.value], targetedSections: [], allSections: false})}
@@ -1740,105 +1478,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
</div> </div>
)} )}
<div className="h-px bg-slate-100" />
{/* Section 5: Department Quotas */}
<div className="space-y-6">
<h3 className="text-xl font-black text-text-dark flex items-center gap-3">
<Split className="w-6 h-6 text-brand-indigo" />
05. Department Quotas
</h3>
<div className="bg-slate-50/50 border border-slate-100 rounded-[2rem] p-8 space-y-6">
{formData.maxParticipants !== '' && parseInt(formData.maxParticipants) > 0 && (
<div className="p-4 bg-amber-50 border border-amber-100 rounded-2xl flex items-center gap-3">
<AlertTriangle className="w-5 h-5 text-amber-500" />
<p className="text-[10px] 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-4 gap-4">
{(formData.institution === 'RIT' ? ritDepartments : rsbDepartments).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-[10px] font-black text-slate-400 uppercase mb-2">{dept}</span>
<input
type="number"
placeholder="∞"
className={cn(
"w-full bg-slate-50 border border-transparent rounded-xl py-2 px-3 text-xs font-black text-center outline-none transition-all",
(formData.maxParticipants !== '' && parseInt(formData.maxParticipants) > 0)
? "text-slate-300 bg-slate-100 cursor-not-allowed"
: hasSectionLimits
? "bg-brand-glow text-brand-indigo cursor-not-allowed"
: "focus:bg-white focus:border-brand-indigo/30 focus:text-brand-indigo"
)}
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={cn(
"mt-3 w-full py-2 text-[9px] font-black uppercase tracking-wider rounded-xl border transition-all disabled:opacity-50",
hasSectionLimits
? "bg-brand-glow text-brand-indigo border-brand-indigo/20 hover:bg-brand-glow/80 animate-pulse"
: "bg-slate-50 text-slate-400 border-transparent hover:bg-slate-100"
)}
>
{hasSectionLimits ? 'Sections Active' : 'Set Sections'}
</button>
</div>
);
})}
</div>
{/* Per-Department Section Quota Inputs */}
{activeDeptForSections && (
<div className="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-brand-navy uppercase tracking-widest flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-brand-indigo rounded-full"></span>
Section limits for {activeDeptForSections}
</h4>
<p className="text-[9px] font-bold text-text-muted 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-brand-navy hover:bg-brand-navy/90 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 text-center font-black text-xs outline-none focus:border-brand-indigo transition-all"
value={val}
onChange={(e) => handleSectionLimitChange(activeDeptForSections, sec, e.target.value)}
/>
</div>
);
})}
</div>
</div>
)}
</div>
</div>
<div className="h-px bg-slate-100" /> <div className="h-px bg-slate-100" />

View File

@@ -62,7 +62,7 @@ export const StatusTimeline: React.FC<{ event: Event; compact?: boolean }> = ({
} }
return ( return (
<div className={cn("w-full space-y-8", compact ? "py-2" : "py-6")}> <div className={cn("w-full", compact ? "py-2 space-y-4" : "py-6 space-y-8")}>
<div className="relative flex items-center justify-between w-full px-4"> <div className="relative flex items-center justify-between w-full px-4">
<div className="absolute left-10 right-10 h-[2px] bg-slate-100 top-[18px]" /> <div className="absolute left-10 right-10 h-[2px] bg-slate-100 top-[18px]" />
@@ -101,29 +101,42 @@ export const StatusTimeline: React.FC<{ event: Event; compact?: boolean }> = ({
<stage.icon className="w-4 h-4" />} <stage.icon className="w-4 h-4" />}
</motion.div> </motion.div>
<div className="absolute top-12 flex flex-col items-center min-w-[100px]"> {!compact && (
<span className={cn( <div className="absolute top-12 flex flex-col items-center min-w-[100px]">
"text-[8px] font-black uppercase tracking-[0.2em] whitespace-nowrap", <span className={cn(
stageRejected ? "text-red-500" : stageCompleted ? "text-emerald-600" : "text-slate-400" "text-[8px] font-black uppercase tracking-[0.2em] whitespace-nowrap",
)}> stageRejected ? "text-red-500" : stageCompleted ? "text-emerald-600" : "text-slate-400"
{stageRejected ? (isHODRejected ? 'HoD Rejected' : isPRRejected ? 'PR Rejected' : 'Cancelled') : stage.label} )}>
</span> {stageRejected ? (isHODRejected ? 'HoD Rejected' : isPRRejected ? 'PR Rejected' : 'Cancelled') : stage.label}
</span>
{isCurrent && !isRejected && ( {isCurrent && !isRejected && (
<motion.span <motion.span
animate={{ opacity: [0.4, 1, 0.4] }} animate={{ opacity: [0.4, 1, 0.4] }}
transition={{ repeat: Infinity, duration: 2 }} transition={{ repeat: Infinity, duration: 2 }}
className="text-[6px] font-bold text-brand-indigo mt-1 uppercase tracking-tighter" className="text-[6px] font-bold text-brand-indigo mt-1 uppercase tracking-tighter"
> >
Current Phase Current Phase
</motion.span> </motion.span>
)} )}
</div> </div>
)}
</div> </div>
); );
})} })}
</div> </div>
{compact && (
<div className="text-center pt-2">
<span className={cn(
"text-[9px] font-black uppercase tracking-[0.15em]",
isRejected ? "text-red-500 bg-red-50 px-3 py-1 rounded-full border border-red-100" : "text-brand-indigo bg-brand-glow px-3 py-1 rounded-full border border-brand-indigo/10"
)}>
Status: {isRejected ? (isHODRejected ? 'HoD Rejected' : isPRRejected ? 'PR Rejected' : 'Cancelled') : stages[currentStageIdx]?.label}
</span>
</div>
)}
<AnimatePresence> <AnimatePresence>
{isRejected && event.rejectionReason && ( {isRejected && event.rejectionReason && (
<motion.div <motion.div

View File

@@ -149,31 +149,12 @@ const EventCard: React.FC<EventCardProps> = ({ event, isBooked, onToggle, onTrac
if (now > deadline) return 'DEADLINE_PASSED'; if (now > deadline) return 'DEADLINE_PASSED';
} }
if (userDept && event.deptLimits?.[userDept]) {
const sectionLimits = event.deptSectionLimits?.[userDept];
const hasSectionLimits = sectionLimits && Object.keys(sectionLimits).length > 0;
if (hasSectionLimits) {
const limit = sectionLimits[userSection];
if (!limit || limit <= 0) {
return 'SECTION_NOT_ALLOWED';
}
const currentSectionCount = event.currentDeptSectionCounts?.[userDept]?.[userSection] || 0;
if (currentSectionCount >= limit) {
return 'SECTION_FULL';
}
} else {
const currentDeptCount = event.currentDeptCounts?.[userDept] || 0;
if (currentDeptCount >= event.deptLimits[userDept]) return 'DEPT_FULL';
}
}
if (event.maxParticipants && (event.currentParticipants || 0) >= event.maxParticipants) { if (event.maxParticipants && (event.currentParticipants || 0) >= event.maxParticipants) {
return 'TOTAL_FULL'; return 'TOTAL_FULL';
} }
return 'OPEN'; return 'OPEN';
}, [event, now, userDept, userSection]); }, [event, now]);
const timeLeft = useMemo(() => { const timeLeft = useMemo(() => {
const targetDate = new Date(event.date).getTime(); const targetDate = new Date(event.date).getTime();
@@ -286,69 +267,11 @@ const EventCard: React.FC<EventCardProps> = ({ event, isBooked, onToggle, onTrac
</div> </div>
{event.schedule && event.schedule.length > 0 && (() => {
// Group schedule entries by day_idx
const dayMap = new Map<number, typeof event.schedule>();
event.schedule!.forEach(s => {
if (!dayMap.has(s.day_idx)) dayMap.set(s.day_idx, []);
dayMap.get(s.day_idx)!.push(s);
});
const sortedDays = Array.from(dayMap.entries()).sort((a, b) => a[0] - b[0]);
return (
<div className="mb-8 space-y-4">
<span className="text-[9px] font-black text-gray-400 uppercase tracking-widest block mb-2">Event Schedule</span>
<div className="space-y-4">
{sortedDays.map(([dayIdx, slots]) => (
<div key={dayIdx} className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-[10px] font-black text-[#1A202C] uppercase tracking-widest">Day {dayIdx}</span>
<span className="text-[9px] font-bold text-gray-400 uppercase">{slots![0]?.date}</span>
</div>
<div className="grid grid-cols-2 gap-2">
{slots!.map((slot) => (
<div key={slot.id} className="bg-gray-50 border border-gray-100 rounded-xl px-3 py-2 flex flex-col">
<span className="text-[8px] font-black text-[#f97316] uppercase tracking-tighter">Batch {slot.batch_idx}</span>
<span className="text-[10px] font-bold text-gray-600">{slot.start_time} - {slot.end_time}</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
);
})()}
<div className="mb-8 p-6 bg-gray-50 rounded-3xl border border-gray-200"> <div className="mb-8 p-6 bg-gray-50 rounded-3xl border border-gray-200">
<div className="flex justify-between items-center mb-3"> <div className="flex justify-between items-center">
<span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Global Seats</span> <span className="text-[10px] font-black text-gray-400 uppercase tracking-widest">Global Seats</span>
<span className="text-xs font-black text-[#1A202C]">{event.currentParticipants || 0} / {event.maxParticipants || (event.deptLimits && Object.keys(event.deptLimits).length > 0 ? Object.values(event.deptLimits).map(Number).reduce((a, b) => a + b, 0) : '∞')}</span> <span className="text-xs font-black text-[#1A202C]">{event.currentParticipants || 0} / {event.maxParticipants || '∞'}</span>
</div> </div>
{userDept && event.deptLimits?.[userDept] && (() => {
const sectionLimits = event.deptSectionLimits?.[userDept];
const hasSectionLimits = sectionLimits && Object.keys(sectionLimits).length > 0;
if (hasSectionLimits) {
const limit = sectionLimits[userSection] || 0;
const current = event.currentDeptSectionCounts?.[userDept]?.[userSection] || 0;
return (
<div className="flex justify-between items-center pt-3 border-t border-gray-200">
<span className="text-[10px] font-black text-[#f97316] uppercase tracking-widest">{userDept} Sec {userSection || 'N/A'} Allocation</span>
<span className={`text-xs font-black ${(admissionStatus === 'SECTION_FULL' || admissionStatus === 'SECTION_NOT_ALLOWED') ? 'text-rose-500' : 'text-[#1A202C]'}`}>
{limit > 0 ? `${current} / ${limit}` : 'RESTRICTED'}
</span>
</div>
);
}
return (
<div className="flex justify-between items-center pt-3 border-t border-gray-200">
<span className="text-[10px] font-black text-[#f97316] uppercase tracking-widest">{userDept} Allocation</span>
<span className={`text-xs font-black ${admissionStatus === 'DEPT_FULL' ? 'text-rose-500' : 'text-[#1A202C]'}`}>
{event.currentDeptCounts?.[userDept] || 0} / {event.deptLimits[userDept]}
</span>
</div>
);
})()}
</div> </div>
<div className="flex justify-between items-center mb-10 text-center"> <div className="flex justify-between items-center mb-10 text-center">

View File

@@ -104,7 +104,7 @@ async function seedDatabaseIfEmpty() {
console.log("[Firebase Backend] Seeding users..."); console.log("[Firebase Backend] Seeding users...");
const defaultUsers = [ const defaultUsers = [
{ id: 1, email: "admin@rit.edu", password: "admin123", fullName: "System Administrator", role: "ADMIN", department: "ADMIN", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] }, { id: 1, email: "admin@rit.edu", password: "admin123", fullName: "System Administrator", role: "ADMIN", department: "ADMIN", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] },
{ id: 2, email: "faculty@rit.edu", password: "faculty123", fullName: "Dr. Faculty Member", role: "FACULTY", department: "CSE", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: true, inchargeClass: "CSE", inchargeBatch: "3rd Year", inchargeSection: "A", classStrength: 60, assignedClubs: [] }, { id: 2, email: "faculty@rit.edu", password: "faculty123", fullName: "Dr. Faculty Member", role: "FACULTY", department: "CSE", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: true, inchargeClass: "CSE", inchargeBatch: "3rd Year", inchargeSection: "A", classStrength: 0, assignedClubs: [] },
{ id: 3, email: "hod@rit.edu", password: "hod123", fullName: "Prof. Head of Dept", role: "HOD", department: "AI&ML", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] }, { id: 3, email: "hod@rit.edu", password: "hod123", fullName: "Prof. Head of Dept", role: "HOD", department: "AI&ML", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] },
{ id: 4, email: "principal@rit.edu", password: "principal123", fullName: "Dr. College Principal", role: "PRINCIPAL", department: "ADMIN", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] }, { id: 4, email: "principal@rit.edu", password: "principal123", fullName: "Dr. College Principal", role: "PRINCIPAL", department: "ADMIN", isClubCoordinator: false, isPlacementStaff: false, isClassIncharge: false, assignedClubs: [] },
{ id: 5, email: "placement@rit.edu", password: "placement123", fullName: "Placement Coordinator", role: "PLACEMENT", department: "Placement Department", isClubCoordinator: false, isPlacementStaff: true, isClassIncharge: false, assignedClubs: [] }, { id: 5, email: "placement@rit.edu", password: "placement123", fullName: "Placement Coordinator", role: "PLACEMENT", department: "Placement Department", isClubCoordinator: false, isPlacementStaff: true, isClassIncharge: false, assignedClubs: [] },