Dynamic class strength caclculations set
This commit is contained in:
@@ -602,6 +602,8 @@ export const ClassManagement: React.FC = () => {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{batchDeptClassesList.map(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 (
|
||||
<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
|
||||
|
||||
@@ -132,85 +132,7 @@ export const EventDetailsModal: React.FC<EventDetailsModalProps> = ({ isOpen, on
|
||||
</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">
|
||||
{event.academicYears && event.academicYears.length > 0 && (
|
||||
@@ -252,45 +174,7 @@ export const EventDetailsModal: React.FC<EventDetailsModalProps> = ({ isOpen, on
|
||||
)}
|
||||
</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 */}
|
||||
{event.maxParticipants && (
|
||||
|
||||
@@ -205,30 +205,26 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
}
|
||||
};
|
||||
|
||||
const buildDayConfigsFromSchedule = (): any[] => {
|
||||
const editData = initialData as any;
|
||||
if (!editData?.dayConfigs || editData.dayConfigs.length === 0) {
|
||||
const launchDateStr = editData?.startDate ? parseDateForInput(editData.startDate) : '';
|
||||
const batchStartTime = editData?.startDate ? editData.startDate.substring(11, 16) : '';
|
||||
const batchEndTime = editData?.endDate ? editData.endDate.substring(11, 16) : '';
|
||||
return [{
|
||||
date: launchDateStr,
|
||||
batches: [{ id: 1, startTime: batchStartTime, endTime: batchEndTime }],
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
}];
|
||||
const formatDateTimeForInput = (dateStr?: string) => {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
const date = new Date(dateStr);
|
||||
if (isNaN(date.getTime())) return '';
|
||||
const offset = date.getTimezoneOffset();
|
||||
const localDate = new Date(date.getTime() - offset * 60 * 1000);
|
||||
return localDate.toISOString().substring(0, 16);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
return editData.dayConfigs;
|
||||
};
|
||||
|
||||
const [deptLimits, setDeptLimits] = useState<Record<string, number>>((initialData as any)?.deptLimits || {});
|
||||
const [deptSectionLimits, setDeptSectionLimits] = useState<Record<string, Record<string, number>>>((initialData as any)?.deptSectionLimits || {});
|
||||
const [activeDeptForSections, setActiveDeptForSections] = useState<string | null>(null);
|
||||
const deptLimits = {};
|
||||
const deptSectionLimits = {};
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
eventName: initialData?.eventName || '',
|
||||
startDate: initialData?.startDate || '',
|
||||
endDate: initialData?.endDate || '',
|
||||
startDate: initialData?.startDate ? formatDateTimeForInput(initialData.startDate) : '',
|
||||
endDate: initialData?.endDate ? formatDateTimeForInput(initialData.endDate) : '',
|
||||
eventType: isClubVariant ? 'CLUB' : (initialData?.eventType || 'Guest Lecture'),
|
||||
category: isClubVariant ? 'CLUB' : (initialData?.category || 'ACADEMIC'),
|
||||
institution: initialData?.institution || 'RIT',
|
||||
@@ -256,11 +252,6 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
image: initialData?.image || '',
|
||||
status: initialData?.status || 'REQUESTED',
|
||||
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() || '',
|
||||
transportation_expense: (initialData as any)?.transportation_expense?.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() || ''
|
||||
});
|
||||
|
||||
// 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
|
||||
useEffect(() => {
|
||||
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) }));
|
||||
}, [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 [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');
|
||||
if (response.ok) {
|
||||
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
|
||||
.filter((u: any) => u.isClassIncharge)
|
||||
.map((u: any) => ({
|
||||
dept: u.inchargeClass,
|
||||
year: u.inchargeBatch,
|
||||
sec: u.inchargeSection,
|
||||
strength: u.classStrength || 0
|
||||
}));
|
||||
.map((u: any) => {
|
||||
const key = `${u.inchargeClass?.trim().toLowerCase()}_${u.inchargeBatch?.trim().toLowerCase()}_${u.inchargeSection?.trim().toLowerCase()}`;
|
||||
return {
|
||||
dept: u.inchargeClass,
|
||||
year: u.inchargeBatch,
|
||||
sec: u.inchargeSection,
|
||||
strength: studentCounts[key] || 0
|
||||
};
|
||||
});
|
||||
setClassStrengths(strengths);
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -612,7 +451,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
|
||||
for (const s of sectionsToTarget) {
|
||||
const match = classStrengths.find(cs => cs.dept === d && cs.year === y && cs.sec === s);
|
||||
if (match) {
|
||||
if (match && match.strength > 0) {
|
||||
total += match.strength;
|
||||
}
|
||||
}
|
||||
@@ -621,7 +460,33 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
return total;
|
||||
}, [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) => {
|
||||
e.preventDefault();
|
||||
@@ -629,12 +494,11 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
setError('');
|
||||
|
||||
const now = new Date();
|
||||
const earliestStart = new Date(formData.startDate);
|
||||
const latestEnd = new Date(formData.endDate);
|
||||
|
||||
// Calculate overall start and end dates from dayConfigs
|
||||
const { earliestStart, latestEnd } = calculateEventDateTimes();
|
||||
|
||||
if (!earliestStart || !latestEnd) {
|
||||
setError('Please configure the schedule and timing for the event.');
|
||||
if (isNaN(earliestStart.getTime()) || isNaN(latestEnd.getTime())) {
|
||||
setError('Please select valid start and end dates & times.');
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
@@ -645,6 +509,12 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
return;
|
||||
}
|
||||
|
||||
if (latestEnd <= earliestStart) {
|
||||
setError('End date/time must be after the start date/time.');
|
||||
setIsSubmitting(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const groupRequestId = crypto.randomUUID();
|
||||
const batchesToAssign = formData.allBatches ? availableYears : formData.academicYears;
|
||||
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,
|
||||
deptSectionLimits,
|
||||
maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants),
|
||||
durationDays: Number(formData.durationDays),
|
||||
durationDays: 1,
|
||||
dayConfigs: [],
|
||||
refreshment_expense: Number(formData.refreshment_expense) || 0,
|
||||
transportation_expense: Number(formData.transportation_expense) || 0,
|
||||
session_coverage_fee: Number(formData.session_coverage_fee) || 0,
|
||||
@@ -724,7 +595,8 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
deptLimits,
|
||||
deptSectionLimits,
|
||||
maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants),
|
||||
durationDays: Number(formData.durationDays),
|
||||
durationDays: 1,
|
||||
dayConfigs: [],
|
||||
refreshment_expense: Number(formData.refreshment_expense) || 0,
|
||||
transportation_expense: Number(formData.transportation_expense) || 0,
|
||||
session_coverage_fee: Number(formData.session_coverage_fee) || 0,
|
||||
@@ -776,7 +648,8 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
deptLimits,
|
||||
deptSectionLimits,
|
||||
maxParticipants: formData.maxParticipants === '' ? null : Number(formData.maxParticipants),
|
||||
durationDays: Number(formData.durationDays),
|
||||
durationDays: 1,
|
||||
dayConfigs: [],
|
||||
refreshment_expense: Number(formData.refreshment_expense) || 0,
|
||||
transportation_expense: Number(formData.transportation_expense) || 0,
|
||||
session_coverage_fee: Number(formData.session_coverage_fee) || 0,
|
||||
@@ -1053,24 +926,29 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
/>
|
||||
</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>
|
||||
<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
|
||||
type="date" required
|
||||
value={formData.launchDate}
|
||||
onChange={e => setFormData({...formData, launchDate: e.target.value})}
|
||||
type="datetime-local" required
|
||||
value={formData.startDate}
|
||||
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"
|
||||
/>
|
||||
</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
|
||||
type="number" required min="1"
|
||||
value={formData.durationDays}
|
||||
onChange={e => setFormData({...formData, durationDays: e.target.value})}
|
||||
type="datetime-local" required
|
||||
value={formData.endDate}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
@@ -1081,112 +959,15 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
placeholder={Object.keys(deptLimits).length > 0 ? "Controlled by Dept Quotas" : "Unlimited"}
|
||||
className={cn(
|
||||
"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"
|
||||
)}
|
||||
placeholder="Unlimited"
|
||||
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"
|
||||
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-[9px] font-black text-amber-500 uppercase tracking-widest mt-2 px-1">
|
||||
↓ Controlled by Dept Quotas
|
||||
</p>
|
||||
)}
|
||||
</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">
|
||||
<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 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
|
||||
value={formData.academicYears[0] || ''}
|
||||
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 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" />
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ export const StatusTimeline: React.FC<{ event: Event; compact?: boolean }> = ({
|
||||
}
|
||||
|
||||
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="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" />}
|
||||
</motion.div>
|
||||
|
||||
<div className="absolute top-12 flex flex-col items-center min-w-[100px]">
|
||||
<span className={cn(
|
||||
"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>
|
||||
{!compact && (
|
||||
<div className="absolute top-12 flex flex-col items-center min-w-[100px]">
|
||||
<span className={cn(
|
||||
"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>
|
||||
|
||||
{isCurrent && !isRejected && (
|
||||
<motion.span
|
||||
animate={{ opacity: [0.4, 1, 0.4] }}
|
||||
transition={{ repeat: Infinity, duration: 2 }}
|
||||
className="text-[6px] font-bold text-brand-indigo mt-1 uppercase tracking-tighter"
|
||||
>
|
||||
Current Phase
|
||||
</motion.span>
|
||||
)}
|
||||
</div>
|
||||
{isCurrent && !isRejected && (
|
||||
<motion.span
|
||||
animate={{ opacity: [0.4, 1, 0.4] }}
|
||||
transition={{ repeat: Infinity, duration: 2 }}
|
||||
className="text-[6px] font-bold text-brand-indigo mt-1 uppercase tracking-tighter"
|
||||
>
|
||||
Current Phase
|
||||
</motion.span>
|
||||
)}
|
||||
</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>
|
||||
{isRejected && event.rejectionReason && (
|
||||
<motion.div
|
||||
|
||||
@@ -149,31 +149,12 @@ const EventCard: React.FC<EventCardProps> = ({ event, isBooked, onToggle, onTrac
|
||||
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) {
|
||||
return 'TOTAL_FULL';
|
||||
}
|
||||
|
||||
return 'OPEN';
|
||||
}, [event, now, userDept, userSection]);
|
||||
}, [event, now]);
|
||||
|
||||
const timeLeft = useMemo(() => {
|
||||
const targetDate = new Date(event.date).getTime();
|
||||
@@ -286,69 +267,11 @@ const EventCard: React.FC<EventCardProps> = ({ event, isBooked, onToggle, onTrac
|
||||
</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="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-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>
|
||||
{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 className="flex justify-between items-center mb-10 text-center">
|
||||
|
||||
@@ -104,7 +104,7 @@ async function seedDatabaseIfEmpty() {
|
||||
console.log("[Firebase Backend] Seeding users...");
|
||||
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: 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: 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: [] },
|
||||
|
||||
Reference in New Issue
Block a user