feat: implement target audience visibility and registration restrictions
This commit is contained in:
@@ -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');
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -319,6 +346,12 @@ const EventCard: React.FC<EventCardProps> = ({ event, isBooked, onToggle, onTrac
|
|||||||
: admissionStatus === 'SECTION_FULL'
|
: admissionStatus === 'SECTION_FULL'
|
||||||
? 'SEC SEATS FULL'
|
? 'SEC SEATS FULL'
|
||||||
: admissionStatus === 'SECTION_NOT_ALLOWED'
|
: admissionStatus === 'SECTION_NOT_ALLOWED'
|
||||||
|
? 'SEC RESTRICTED'
|
||||||
|
: admissionStatus === 'DEPT_RESTRICTED'
|
||||||
|
? 'DEPT RESTRICTED'
|
||||||
|
: admissionStatus === 'YEAR_RESTRICTED'
|
||||||
|
? 'YEAR RESTRICTED'
|
||||||
|
: admissionStatus === 'SECTION_RESTRICTED'
|
||||||
? 'SEC RESTRICTED'
|
? 'SEC RESTRICTED'
|
||||||
: 'Event Full'}
|
: 'Event Full'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user