feat: implement target audience visibility and registration restrictions
This commit is contained in:
@@ -60,7 +60,8 @@ const AppContent: React.FC = () => {
|
||||
socialProfile: event.guestSocialProfile,
|
||||
maxParticipants: event["total capacity"] || (event.maxParticipants ?? ''),
|
||||
sponsors: event.sponsors ?? [],
|
||||
isEditMode: true
|
||||
isEditMode: true,
|
||||
openToAll: !!event.openToAll
|
||||
});
|
||||
setActiveItem('propose');
|
||||
};
|
||||
|
||||
@@ -70,6 +70,7 @@ interface Event {
|
||||
department: string;
|
||||
academicYears: string[];
|
||||
targetedSections: string[];
|
||||
openToAll?: boolean;
|
||||
}
|
||||
|
||||
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] || '') : ''),
|
||||
isPublicEvent: initialData?.isPublicEvent || false,
|
||||
image: initialData?.image || '',
|
||||
openToAll: (initialData as any)?.openToAll || false,
|
||||
status: initialData?.status || 'REQUESTED',
|
||||
targetedBatch: (initialData as any)?.targetedBatch || '',
|
||||
refreshment_expense: (initialData as any)?.refreshment_expense?.toString() || '',
|
||||
@@ -1304,6 +1306,22 @@ export const EventProposalForm: React.FC<EventProposalFormProps> = ({ initialDat
|
||||
</div>
|
||||
) : (
|
||||
<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={cn((eventScope === 'INSTITUTIONAL' || eventScope === 'CLUB' || eventScope === 'PLACEMENT' || (eventScope === 'DEPARTMENT' && isPlacementCell)) && "md:col-span-2")}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
|
||||
@@ -128,7 +128,11 @@ export const StudentDashboard: React.FC = () => {
|
||||
currentDeptSectionCounts: deptSectionCounts,
|
||||
created_by: dbEvent.proposer?.email || dbEvent.created_by || '',
|
||||
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 list = rawEvents.map(e => mapDbToEvent(e, allRegistrations));
|
||||
// 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');
|
||||
}, [rawEvents, allRegistrations, mapDbToEvent]);
|
||||
return list.filter(e => {
|
||||
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(() => {
|
||||
return allRegistrations.filter(r => String(r.userId || r.user_id) === String(user?.id));
|
||||
|
||||
@@ -153,8 +153,35 @@ const EventCard: React.FC<EventCardProps> = ({ event, isBooked, onToggle, onTrac
|
||||
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';
|
||||
}, [event, now]);
|
||||
}, [event, now, user]);
|
||||
|
||||
const timeLeft = useMemo(() => {
|
||||
const targetDate = new Date(event.date).getTime();
|
||||
@@ -320,7 +347,13 @@ const EventCard: React.FC<EventCardProps> = ({ event, isBooked, onToggle, onTrac
|
||||
? 'SEC SEATS FULL'
|
||||
: admissionStatus === 'SECTION_NOT_ALLOWED'
|
||||
? 'SEC RESTRICTED'
|
||||
: 'Event Full'}
|
||||
: admissionStatus === 'DEPT_RESTRICTED'
|
||||
? 'DEPT RESTRICTED'
|
||||
: admissionStatus === 'YEAR_RESTRICTED'
|
||||
? 'YEAR RESTRICTED'
|
||||
: admissionStatus === 'SECTION_RESTRICTED'
|
||||
? 'SEC RESTRICTED'
|
||||
: 'Event Full'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -755,6 +755,7 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
|
||||
isPublicEvent: !!payload.isPublicEvent,
|
||||
image: payload.image || null,
|
||||
targetedBatch: payload.targetedBatch || null,
|
||||
openToAll: !!payload.openToAll,
|
||||
"total capacity": payload.maxParticipants === '' || payload.maxParticipants === undefined ? null : Number(payload.maxParticipants),
|
||||
maxParticipants: payload.maxParticipants === '' || payload.maxParticipants === undefined ? null : Number(payload.maxParticipants),
|
||||
proposer: {
|
||||
@@ -812,7 +813,22 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
|
||||
const ay = payload.targetBatch ? [payload.targetBatch] : (payload.academicYears || []);
|
||||
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 = {
|
||||
id: generateNumericId(),
|
||||
@@ -829,7 +845,8 @@ window.fetch = async function(input: RequestInfo | URL, init?: RequestInit): Pro
|
||||
status: payload.status || "APPROVED",
|
||||
requirements: payload.requirements || [],
|
||||
image: payload.image || null,
|
||||
proposer
|
||||
proposer,
|
||||
openToAll: !!payload.openToAll
|
||||
};
|
||||
|
||||
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.isPublicEvent !== undefined) updatedFields.isPublicEvent = !!payload.isPublicEvent;
|
||||
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.dayConfigs !== undefined) updatedFields.dayConfigs = payload.dayConfigs;
|
||||
if (payload.deptLimits !== undefined) updatedFields.deptLimits = payload.deptLimits;
|
||||
|
||||
@@ -80,6 +80,10 @@ export interface Event {
|
||||
request_by_faculty?: string;
|
||||
request_by_HOD?: string;
|
||||
targetedBatch?: string;
|
||||
openToAll?: boolean;
|
||||
academicYears?: string[];
|
||||
targetedSections?: string[];
|
||||
department?: string;
|
||||
}
|
||||
|
||||
export interface Announcement {
|
||||
|
||||
Reference in New Issue
Block a user