feat: implement target audience visibility and registration restrictions

This commit is contained in:
2026-06-25 05:12:02 +05:30
parent 0b1053a4e8
commit 025ea01afe
6 changed files with 111 additions and 8 deletions

View File

@@ -60,7 +60,8 @@ const AppContent: React.FC = () => {
socialProfile: event.guestSocialProfile, socialProfile: event.guestSocialProfile,
maxParticipants: event["total capacity"] || (event.maxParticipants ?? ''), maxParticipants: event["total capacity"] || (event.maxParticipants ?? ''),
sponsors: event.sponsors ?? [], sponsors: event.sponsors ?? [],
isEditMode: true isEditMode: true,
openToAll: !!event.openToAll
}); });
setActiveItem('propose'); setActiveItem('propose');
}; };

View File

@@ -70,6 +70,7 @@ interface Event {
department: string; department: string;
academicYears: string[]; academicYears: string[];
targetedSections: string[]; targetedSections: string[];
openToAll?: boolean;
} }
interface EventProposalFormProps { interface EventProposalFormProps {
@@ -267,6 +268,7 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
centreName: initialData?.centreName || (eventScope === 'CENTRE' ? ((CENTRE_MAPPING[user?.department || ''] || CENTRE_MAPPING['Placement Department'] || [])[0] || '') : ''), centreName: initialData?.centreName || (eventScope === 'CENTRE' ? ((CENTRE_MAPPING[user?.department || ''] || CENTRE_MAPPING['Placement Department'] || [])[0] || '') : ''),
isPublicEvent: initialData?.isPublicEvent || false, isPublicEvent: initialData?.isPublicEvent || false,
image: initialData?.image || '', image: initialData?.image || '',
openToAll: (initialData as any)?.openToAll || false,
status: initialData?.status || 'REQUESTED', status: initialData?.status || 'REQUESTED',
targetedBatch: (initialData as any)?.targetedBatch || '', targetedBatch: (initialData as any)?.targetedBatch || '',
refreshment_expense: (initialData as any)?.refreshment_expense?.toString() || '', refreshment_expense: (initialData as any)?.refreshment_expense?.toString() || '',
@@ -1304,6 +1306,22 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
</div> </div>
) : ( ) : (
<div className="space-y-8"> <div className="space-y-8">
<div className="p-6 bg-slate-50 rounded-3xl border border-slate-100 flex items-center justify-between mb-2">
<div>
<h4 className="text-sm font-black text-text-dark uppercase">Open to All Students</h4>
<p className="text-[10px] text-text-muted mt-1">If enabled, this event will be visible and open to all departments, batches, and sections.</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={formData.openToAll}
onChange={e => setFormData({ ...formData, openToAll: e.target.checked })}
className="sr-only peer"
/>
<div className="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-brand-indigo"></div>
</label>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8"> <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div className={cn((eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) && "md:col-span-2")}> <div className={cn((eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) && "md:col-span-2")}>
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">

View File

@@ -128,7 +128,11 @@ export const StudentDashboard: React.FC = () => {
currentDeptSectionCounts: deptSectionCounts, currentDeptSectionCounts: deptSectionCounts,
created_by: dbEvent.proposer?.email || dbEvent.created_by || '', created_by: dbEvent.proposer?.email || dbEvent.created_by || '',
participantType: dbEvent.participantType || 'BOTH', participantType: dbEvent.participantType || 'BOTH',
verificationStatus: dbEvent.status === 'COMPLETED' ? 'APPROVED' : 'APPROVED' verificationStatus: dbEvent.status === 'COMPLETED' ? 'APPROVED' : 'APPROVED',
openToAll: !!dbEvent.openToAll,
academicYears: dbEvent.academicYears || [],
targetedSections: dbEvent.targetedSections || [],
department: dbEvent.department || ''
}; };
}, []); }, []);
@@ -194,8 +198,33 @@ export const StudentDashboard: React.FC = () => {
const events = useMemo(() => { const events = useMemo(() => {
const list = rawEvents.map(e => mapDbToEvent(e, allRegistrations)); const list = rawEvents.map(e => mapDbToEvent(e, allRegistrations));
// Students only see live (approved / ongoing / completed) events // Students only see live (approved / ongoing / completed) events
return list.filter(e => e.status === 'APPROVED' || e.status === 'COMPLETED' || e.status === 'Event Ongoing' || e.status === 'ONGOING'); return list.filter(e => {
}, [rawEvents, allRegistrations, mapDbToEvent]); const isLive = e.status === 'APPROVED' || e.status === 'COMPLETED' || e.status === 'Event Ongoing' || e.status === 'ONGOING';
if (!isLive) return false;
// If it is open to all, it is visible to everyone
if (e.openToAll) return true;
// Otherwise, filter by student's department, year, and section
const userDept = user?.department || '';
const userYear = user?.year || '';
const userSec = user?.section || '';
const normalize = (s: string) => (s || '').trim().replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
const deptMatches = normalize(userDept) === normalize(e.domain || e.department || '');
const yearMatches = e.academicYears && e.academicYears.length > 0
? e.academicYears.map((y: string) => normalize(y)).includes(normalize(userYear))
: true;
const sectionMatches = e.targetedSections && e.targetedSections.length > 0
? e.targetedSections.map((s: string) => normalize(s)).includes(normalize(userSec))
: true;
return deptMatches && yearMatches && sectionMatches;
});
}, [rawEvents, allRegistrations, mapDbToEvent, user]);
const userRegistrations = useMemo(() => { const userRegistrations = useMemo(() => {
return allRegistrations.filter(r => String(r.userId || r.user_id) === String(user?.id)); return allRegistrations.filter(r => String(r.userId || r.user_id) === String(user?.id));

View File

@@ -153,8 +153,35 @@ const EventCard: React.FC<EventCardProps> = ({ event, isBooked, onToggle, onTrac
return 'TOTAL_FULL'; return 'TOTAL_FULL';
} }
if (!event.openToAll && user && user.role === 'STUDENT') {
const normalize = (s: string) => (s || '').trim().replace(/[^a-zA-Z0-9]/g, '').toLowerCase();
const userDept = normalize(user.department);
const eventDept = normalize(event.domain || event.department);
if (userDept === 'others' || !eventDept || userDept !== eventDept) {
return 'DEPT_RESTRICTED';
}
const userYear = normalize(user.year);
if (event.academicYears && event.academicYears.length > 0) {
const yearMatches = event.academicYears.map((y: string) => normalize(y)).includes(userYear);
if (!yearMatches) {
return 'YEAR_RESTRICTED';
}
}
const userSec = normalize(user.section);
if (event.targetedSections && event.targetedSections.length > 0) {
const sectionMatches = event.targetedSections.map((s: string) => normalize(s)).includes(userSec);
if (!sectionMatches) {
return 'SECTION_RESTRICTED';
}
}
}
return 'OPEN'; return 'OPEN';
}, [event, now]); }, [event, now, user]);
const timeLeft = useMemo(() => { const timeLeft = useMemo(() => {
const targetDate = new Date(event.date).getTime(); const targetDate = new Date(event.date).getTime();
@@ -320,7 +347,13 @@ const EventCard: React.FC<EventCardProps> = ({ event, isBooked, onToggle, onTrac
? 'SEC SEATS FULL' ? 'SEC SEATS FULL'
: admissionStatus === 'SECTION_NOT_ALLOWED' : admissionStatus === 'SECTION_NOT_ALLOWED'
? 'SEC RESTRICTED' ? 'SEC RESTRICTED'
: 'Event Full'} : admissionStatus === 'DEPT_RESTRICTED'
? 'DEPT RESTRICTED'
: admissionStatus === 'YEAR_RESTRICTED'
? 'YEAR RESTRICTED'
: admissionStatus === 'SECTION_RESTRICTED'
? 'SEC RESTRICTED'
: 'Event Full'}
</button> </button>
)} )}
</div> </div>

View File

@@ -755,6 +755,7 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
isPublicEvent: !!payload.isPublicEvent, isPublicEvent: !!payload.isPublicEvent,
image: payload.image || null, image: payload.image || null,
targetedBatch: payload.targetedBatch || null, targetedBatch: payload.targetedBatch || null,
openToAll: !!payload.openToAll,
"total capacity": payload.maxParticipants === '' || payload.maxParticipants === undefined ? null : Number(payload.maxParticipants), "total capacity": payload.maxParticipants === '' || payload.maxParticipants === undefined ? null : Number(payload.maxParticipants),
maxParticipants: payload.maxParticipants === '' || payload.maxParticipants === undefined ? null : Number(payload.maxParticipants), maxParticipants: payload.maxParticipants === '' || payload.maxParticipants === undefined ? null : Number(payload.maxParticipants),
proposer: { proposer: {
@@ -812,7 +813,22 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
const ay = payload.targetBatch ? [payload.targetBatch] : (payload.academicYears || []); const ay = payload.targetBatch ? [payload.targetBatch] : (payload.academicYears || []);
const venue = payload.venue || payload.location || "TBD"; const venue = payload.venue || payload.location || "TBD";
const proposer = payload.proposer || { id: 1, fullName: "Admin", email: "admin@rit.edu", role: "ADMIN" }; let proposer = { id: 1, fullName: "Admin", email: "admin@rit.edu", role: "ADMIN", department: "ADMIN" };
if (payload.proposer && payload.proposer.id) {
const userSnap = await getDoc(doc(db, 'ems_users', String(payload.proposer.id)));
if (userSnap.exists()) {
const u = userSnap.data();
proposer = {
id: u.id,
fullName: u.fullName,
email: u.email,
role: u.role,
department: u.department || "N/A"
};
}
} else if (payload.proposer) {
proposer = { ...proposer, ...payload.proposer };
}
const event = { const event = {
id: generateNumericId(), id: generateNumericId(),
@@ -829,7 +845,8 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
status: payload.status || "APPROVED", status: payload.status || "APPROVED",
requirements: payload.requirements || [], requirements: payload.requirements || [],
image: payload.image || null, image: payload.image || null,
proposer proposer,
openToAll: !!payload.openToAll
}; };
const conflictMsg = await getConflictMessage(event); const conflictMsg = await getConflictMessage(event);
@@ -1027,6 +1044,7 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
if (payload.centreName !== undefined) updatedFields.centreName = payload.centreName; if (payload.centreName !== undefined) updatedFields.centreName = payload.centreName;
if (payload.isPublicEvent !== undefined) updatedFields.isPublicEvent = !!payload.isPublicEvent; if (payload.isPublicEvent !== undefined) updatedFields.isPublicEvent = !!payload.isPublicEvent;
if (payload.image !== undefined) updatedFields.image = payload.image; if (payload.image !== undefined) updatedFields.image = payload.image;
if (payload.openToAll !== undefined) updatedFields.openToAll = !!payload.openToAll;
if (payload.targetedBatch !== undefined) updatedFields.targetedBatch = payload.targetedBatch; if (payload.targetedBatch !== undefined) updatedFields.targetedBatch = payload.targetedBatch;
if (payload.dayConfigs !== undefined) updatedFields.dayConfigs = payload.dayConfigs; if (payload.dayConfigs !== undefined) updatedFields.dayConfigs = payload.dayConfigs;
if (payload.deptLimits !== undefined) updatedFields.deptLimits = payload.deptLimits; if (payload.deptLimits !== undefined) updatedFields.deptLimits = payload.deptLimits;

View File

@@ -80,6 +80,10 @@ export interface Event {
request_by_faculty?: string; request_by_faculty?: string;
request_by_HOD?: string; request_by_HOD?: string;
targetedBatch?: string; targetedBatch?: string;
openToAll?: boolean;
academicYears?: string[];
targetedSections?: string[];
department?: string;
} }
export interface Announcement { export interface Announcement {