From 947c89167d24c5decda96581837320f0823a9685 Mon Sep 17 00:00:00 2001 From: Abiram070207 Date: Mon, 22 Jun 2026 12:16:30 +0530 Subject: [PATCH] updated UI and logics --- RIT-EMS-main/frontend/src/App.tsx | 13 +- .../frontend/src/components/Sidebar.tsx | 4 +- .../components/dashboard/ClassManagement.tsx | 270 +++++- .../dashboard/EventDetailsModal.tsx | 14 +- .../dashboard/EventProposalForm.tsx | 124 ++- .../dashboard/InstitutionalChecklist.tsx | 62 +- .../dashboard/ManageNoticesView.tsx | 798 ++++++++++++++++++ .../components/dashboard/StudentDashboard.tsx | 60 +- .../dashboard/StudentRegistrationsView.tsx | 142 +++- .../components/dashboard/UserManagement.tsx | 26 +- .../src/components/student/EventCard.tsx | 226 ++++- .../frontend/src/components/student/Hero.tsx | 2 +- .../src/components/student/HomeDashboard.tsx | 25 +- .../components/student/ScrollNotification.tsx | 128 +++ .../student/SpecialEventsBanner.tsx | 368 ++++---- .../components/student/StatusTrackerView.tsx | 2 +- .../frontend/src/context/DialogContext.tsx | 127 +++ .../frontend/src/lib/firebaseBackend.ts | 148 +++- RIT-EMS-main/frontend/src/types.ts | 10 + .../components/SpecialEventsBanner.tsx | 54 +- image copy.png | Bin 0 -> 634194 bytes image.png | Bin 45122 -> 654332 bytes 22 files changed, 2243 insertions(+), 360 deletions(-) create mode 100644 RIT-EMS-main/frontend/src/components/dashboard/ManageNoticesView.tsx create mode 100644 RIT-EMS-main/frontend/src/components/student/ScrollNotification.tsx create mode 100644 RIT-EMS-main/frontend/src/context/DialogContext.tsx create mode 100644 image copy.png diff --git a/RIT-EMS-main/frontend/src/App.tsx b/RIT-EMS-main/frontend/src/App.tsx index 8177832..bb64b25 100644 --- a/RIT-EMS-main/frontend/src/App.tsx +++ b/RIT-EMS-main/frontend/src/App.tsx @@ -9,6 +9,7 @@ import { UserManagement } from './components/dashboard/UserManagement'; import { ClassManagement } from './components/dashboard/ClassManagement'; import { LoginPage } from './pages/LoginPage'; import { AuthProvider, useAuth } from './context/AuthContext'; +import { DialogProvider } from './context/DialogContext'; import { motion, AnimatePresence } from 'framer-motion'; import { StatusTimeline, type Event } from './components/dashboard/EventStatusTimeline'; import { useEffect } from 'react'; @@ -22,6 +23,8 @@ import { Overview } from './components/dashboard/Overview'; import { ExcelImport } from './components/dashboard/ExcelImport'; import { StudentDashboard } from './components/dashboard/StudentDashboard'; import { StudentRegistrationsView } from './components/dashboard/StudentRegistrationsView'; +import { ManageNoticesView } from './components/dashboard/ManageNoticesView'; + const AppContent: React.FC = () => { const { isAuthenticated, user } = useAuth(); @@ -158,6 +161,8 @@ const AppContent: React.FC = () => { return ; case 'student-registrations': return (user?.role === 'FACULTY' || user?.role === 'HOD' || user?.role === 'ADMIN') ? : isDashboard; + case 'manage-notices': + return user?.role === 'ADMIN' ? : isDashboard; case 'dashboard': return isDashboard; default: @@ -187,9 +192,11 @@ const AppContent: React.FC = () => { function App() { return ( - - - + + + + + ); } diff --git a/RIT-EMS-main/frontend/src/components/Sidebar.tsx b/RIT-EMS-main/frontend/src/components/Sidebar.tsx index 69aa745..3d6473e 100644 --- a/RIT-EMS-main/frontend/src/components/Sidebar.tsx +++ b/RIT-EMS-main/frontend/src/components/Sidebar.tsx @@ -13,7 +13,8 @@ import { Calendar, HelpCircle, ExternalLink, - CheckCircle + CheckCircle, + Bell } from 'lucide-react'; import { cn } from '../lib/utils'; import { motion } from 'framer-motion'; @@ -39,6 +40,7 @@ export const Sidebar: React.FC = ({ activeItem, onItemClick }) => ...((user?.role === 'ADMIN' || user?.role === 'PRINCIPAL' || user?.role === 'HOD') ? [{ icon: Zap, label: 'Automation', id: 'automation' }] : []), ...(user?.role === 'ADMIN' ? [{ icon: BookOpen, label: 'Classes', id: 'classes' }] : []), ...(user?.role === 'ADMIN' ? [{ icon: Users, label: 'Manage Users', id: 'user-management' }] : []), + ...(user?.role === 'ADMIN' ? [{ icon: Bell, label: 'Manage Notices', id: 'manage-notices' }] : []), ...((user?.role === 'FACULTY' || user?.role === 'HOD' || user?.role === 'ADMIN') ? [{ icon: Users, label: 'Student Hub', id: 'student-registrations' }] : []) ]; diff --git a/RIT-EMS-main/frontend/src/components/dashboard/ClassManagement.tsx b/RIT-EMS-main/frontend/src/components/dashboard/ClassManagement.tsx index 675a9bf..afd69d2 100644 --- a/RIT-EMS-main/frontend/src/components/dashboard/ClassManagement.tsx +++ b/RIT-EMS-main/frontend/src/components/dashboard/ClassManagement.tsx @@ -16,6 +16,7 @@ import { } from 'lucide-react'; import { cn } from '../../lib/utils'; import { Pagination } from './Pagination'; +import { useDialog } from '../../context/DialogContext'; interface ClassMapping { id: number; @@ -34,6 +35,7 @@ import rsbLogo from '../../assets/images/rsb_logo.png'; import ritLogo from '../../assets/images/college-logo.png'; export const ClassManagement: React.FC = () => { + const { showAlert, showConfirm } = useDialog(); const [classes, setClasses] = useState([]); const [isLoading, setIsLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(''); @@ -48,6 +50,16 @@ export const ClassManagement: React.FC = () => { sections: [] as string[] }); + const [batches, setBatches] = useState([]); + const [newBatchName, setNewBatchName] = useState(''); + const [selectedBatchDept, setSelectedBatchDept] = useState('AI&DS'); + const [tempBatches, setTempBatches] = useState([]); + + useEffect(() => { + const deptBatches = batches.filter(b => b.department === selectedBatchDept); + setTempBatches(JSON.parse(JSON.stringify(deptBatches))); + }, [batches, selectedBatchDept]); + const currentDepartments = formData.institution === 'RIT' ? ritDepartments : rsbDepartments; useEffect(() => { @@ -58,8 +70,21 @@ export const ClassManagement: React.FC = () => { useEffect(() => { fetchClasses(); + fetchBatches(); }, []); + const fetchBatches = async () => { + try { + const response = await fetch(API_BASE_URL + '/api/batches'); + if (response.ok) { + const data = await response.json(); + setBatches(data); + } + } catch (error) { + console.error('Failed to fetch batches:', error); + } + }; + const fetchClasses = async () => { try { const response = await fetch(API_BASE_URL + '/api/classes'); @@ -94,7 +119,7 @@ export const ClassManagement: React.FC = () => { const handleSave = async () => { if (formData.sections.length === 0) { - alert('Please add at least one section'); + showAlert('Required', 'Please add at least one section', 'error'); return; } @@ -108,50 +133,120 @@ export const ClassManagement: React.FC = () => { if (response.ok) { fetchClasses(); setFormData({ ...formData, sections: [] }); + showAlert('Success', 'Class mapping saved successfully.', 'success'); } } catch (error) { console.error('Failed to save class mapping:', error); + showAlert('Error', 'Failed to save class mapping.', 'error'); } }; const handleDelete = async (id: number) => { - if (window.confirm('Are you sure you want to delete this mapping?')) { - try { - const response = await fetch(`${API_BASE_URL}/api/classes/${id}`, { - method: 'DELETE' - }); - if (response.ok) { - setClasses(prev => prev.filter(c => c.id !== id)); + showConfirm( + 'Delete Mapping', + 'Are you sure you want to delete this mapping?', + async () => { + try { + const response = await fetch(`${API_BASE_URL}/api/classes/${id}`, { + method: 'DELETE' + }); + if (response.ok) { + setClasses(prev => prev.filter(c => c.id !== id)); + showAlert('Success', 'Mapping deleted successfully.', 'success'); + } + } catch (error) { + console.error('Delete failed:', error); + showAlert('Error', 'Failed to delete mapping.', 'error'); } - } catch (error) { - console.error('Delete failed:', error); } - } + ); }; const handlePromote = async () => { - const confirmed = window.confirm( - `⚠️ WARNING: ${formData.institution} ACADEMIC YEAR PROMOTION\n\n` + - `This action will shift all academic structures for ${formData.institution} forward:\n` + - '• 1st Year → 2nd Year\n' + - '• 2nd Year → 3rd Year\n' + - '• 3rd Year → 4th Year\n' + - '• 4th Year mappings will be PERMANENTLY DELETED.\n\n' + - 'Do you wish to proceed with this irreversible operation?' - ); - - if (confirmed) { - try { - const response = await fetch(`${API_BASE_URL}/api/classes/promote?institution=${formData.institution}`, { - method: 'POST' - }); - if (response.ok) { - fetchClasses(); - alert(`${formData.institution} academic year promotion completed successfully!`); + showConfirm( + 'Academic Promotion', + `WARNING: This action will shift all academic structures for ${formData.institution} forward:\n• 1st Year → 2nd Year\n• 2nd Year → 3rd Year\n• 3rd Year → 4th Year\n• 4th Year mappings will be PERMANENTLY DELETED.\n\nDo you wish to proceed?`, + async () => { + try { + const response = await fetch(`${API_BASE_URL}/api/classes/promote?institution=${formData.institution}`, { + method: 'POST' + }); + if (response.ok) { + fetchClasses(); + showAlert('Success', `${formData.institution} academic year promotion completed successfully!`, 'success'); + } + } catch (error) { + console.error('Promotion failed:', error); + showAlert('Error', 'Promotion failed.', 'error'); } - } catch (error) { - console.error('Promotion failed:', error); } + ); + }; + + const handleCreateTempBatch = () => { + if (!newBatchName.trim()) { + showAlert('Required', 'Please enter a batch name', 'error'); + return; + } + const newBatch = { + id: Date.now() + Math.floor(Math.random() * 1000), + name: newBatchName.trim(), + department: selectedBatchDept, + classes: [] + }; + setTempBatches([...tempBatches, newBatch]); + setNewBatchName(''); + }; + + const handleDeleteTempBatch = (batchId: number) => { + setTempBatches(tempBatches.filter(b => b.id !== batchId)); + }; + + const handleToggleClassInTempBatch = (batchId: number, classStr: string) => { + setTempBatches(tempBatches.map(b => { + if (b.id !== batchId) return b; + const isAssigned = b.classes?.includes(classStr); + return { + ...b, + classes: isAssigned + ? b.classes.filter((c: string) => c !== classStr) + : [...(b.classes || []), classStr] + }; + })); + }; + + const handleSaveChanges = async () => { + try { + const originalDeptBatches = batches.filter(b => b.department === selectedBatchDept); + + // 1. Identify deleted batches (in original but not in temp) + const deletedBatches = originalDeptBatches.filter(ob => !tempBatches.some(tb => String(tb.id) === String(ob.id))); + + // 2. Perform deletes + for (const db of deletedBatches) { + await fetch(`${API_BASE_URL}/api/batches/${db.id}`, { method: 'DELETE' }); + } + + // 3. Save / Update current batches in tempBatches + for (const tb of tempBatches) { + await fetch(API_BASE_URL + '/api/batches', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + id: tb.id, + name: tb.name, + department: tb.department, + classes: tb.classes + }) + }); + } + + // 4. Refresh state + await fetchBatches(); + showAlert('Success', 'Batch configurations saved successfully.', 'success'); + } catch (error) { + console.error('Failed to save batch changes:', error); + showAlert('Error', 'Failed to save changes.', 'error'); } }; @@ -169,6 +264,14 @@ export const ClassManagement: React.FC = () => { currentPage * itemsPerPage ); + const selectedDeptClasses = classes.filter(c => c.institution === formData.institution && c.department === formData.department); + const totalClassesCount = selectedDeptClasses.reduce((sum, c) => sum + c.sections.length, 0); + const hasMoreThanFourClasses = totalClassesCount > 4; + const deptClassesList = selectedDeptClasses.flatMap(c => c.sections.map(sec => `${c.academicYear} - ${sec}`)); + + const selectedBatchDeptClasses = classes.filter(c => c.department === selectedBatchDept); + const batchDeptClassesList = selectedBatchDeptClasses.flatMap(c => c.sections.map(sec => `${c.academicYear} - ${sec}`)); + return (
{/* Header */} @@ -430,6 +533,111 @@ export const ClassManagement: React.FC = () => {
+ + {/* Batches Section - Always Visible */} +
+
+
+
+ +
+
+

Department Batches

+

Configure custom student batches for auditing & event planning.

+
+
+ +
+ {/* Select Department */} +
+ Department: + +
+ + {/* Create Batch Form */} +
+ setNewBatchName(e.target.value)} + className="bg-slate-50 border border-slate-200 rounded-xl py-2 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all text-slate-800" + /> + +
+
+
+ +
+ {tempBatches.map(batch => ( +
+
+ {batch.name} + +
+ +
+ Assign Classes to this Batch: + {batchDeptClassesList.length > 0 ? ( +
+ {batchDeptClassesList.map(cls => { + const isChecked = batch.classes?.includes(cls); + return ( + + ); + })} +
+ ) : ( + No classes configured for {selectedBatchDept}. + )} +
+
+ ))} + {tempBatches.length === 0 && ( +
+ No batches created yet for {selectedBatchDept}. Use the form above to add your first batch. +
+ )} +
+ + {tempBatches.length > 0 && ( +
+ +
+ )} +
); }; diff --git a/RIT-EMS-main/frontend/src/components/dashboard/EventDetailsModal.tsx b/RIT-EMS-main/frontend/src/components/dashboard/EventDetailsModal.tsx index 9f8b2e9..c69fc33 100644 --- a/RIT-EMS-main/frontend/src/components/dashboard/EventDetailsModal.tsx +++ b/RIT-EMS-main/frontend/src/components/dashboard/EventDetailsModal.tsx @@ -131,11 +131,17 @@ export const EventDetailsModal: React.FC = ({ isOpen, on Target Audience
- {event.academicYears.map((year: string) => ( - - {year} + {(event as any).targetedBatch ? ( + + {(event as any).targetedBatch} - ))} + ) : ( + event.academicYears.map((year: string) => ( + + {year} + + )) + )}
)} diff --git a/RIT-EMS-main/frontend/src/components/dashboard/EventProposalForm.tsx b/RIT-EMS-main/frontend/src/components/dashboard/EventProposalForm.tsx index fa21eb8..f44cae7 100644 --- a/RIT-EMS-main/frontend/src/components/dashboard/EventProposalForm.tsx +++ b/RIT-EMS-main/frontend/src/components/dashboard/EventProposalForm.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useRef } from 'react'; +import React, { useState, useEffect, useRef, useMemo } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { Calendar, @@ -100,6 +100,7 @@ interface EventProposalFormProps { budget?: string | number; hasRegistrationFee?: boolean; registrationFee?: string | number; + paymentLink?: string; targetDepartments?: string[]; description?: string; sponsors?: string[]; @@ -194,6 +195,7 @@ export const EventProposalForm: React.FC = ({ initialDat customEventType: '', hasRegistrationFee: initialData?.hasRegistrationFee || false, registrationFee: initialData?.registrationFee || '' as string | number, + paymentLink: initialData?.paymentLink || '', targetDepartments: initialData?.targetDepartments || (isFaculty ? (isPlacementCell ? [] : [user?.department || 'CSE']) : [] as string[]), allDepts: false, allBatches: false, @@ -203,7 +205,8 @@ export const EventProposalForm: React.FC = ({ initialDat centreName: initialData?.centreName || (eventScope === 'CENTRE' ? ((CENTRE_MAPPING[user?.department || ''] || CENTRE_MAPPING['Placement Department'] || [])[0] || '') : ''), isPublicEvent: initialData?.isPublicEvent || false, image: initialData?.image || '', - status: initialData?.status || 'REQUESTED' + status: initialData?.status || 'REQUESTED', + targetedBatch: (initialData as any)?.targetedBatch || '' }); const [newSponsor, setNewSponsor] = useState(''); @@ -230,12 +233,32 @@ export const EventProposalForm: React.FC = ({ initialDat })); }, [eventScope, user?.department]); + const [batches, setBatches] = useState([]); + useEffect(() => { fetchClasses(); fetchExistingEvents(); fetchClassStrengths(); + fetchBatches(); }, []); + const fetchBatches = async () => { + try { + const response = await fetch(API_BASE_URL + '/api/batches'); + if (response.ok) { + const data = await response.json(); + setBatches(data); + } + } catch (err) { + console.error('Failed to fetch batches:', err); + } + }; + + const deptBatches = useMemo(() => { + const dept = eventScope === 'DEPARTMENT' ? formData.department : (user?.department || 'CSE'); + return batches.filter(b => b.department === dept); + }, [batches, formData.department, eventScope, user?.department]); + const fetchClassStrengths = async () => { try { const response = await fetch(API_BASE_URL + '/api/admin/users'); @@ -853,17 +876,31 @@ export const EventProposalForm: React.FC = ({ initialDat exit={{ opacity: 0, height: 0 }} className="md:col-span-2 overflow-hidden" > -
- -
- - setFormData({...formData, registrationFee: e.target.value})} - className="w-full bg-white border border-brand-indigo/20 rounded-xl py-4 pl-12 pr-6 text-sm font-bold text-brand-indigo focus:ring-2 ring-brand-indigo/20 outline-none" - placeholder="Amount in INR" - /> +
+
+ +
+ + setFormData({...formData, registrationFee: e.target.value})} + className="w-full bg-white border border-brand-indigo/20 rounded-xl py-4 pl-12 pr-6 text-sm font-bold text-brand-indigo focus:ring-2 ring-brand-indigo/20 outline-none" + placeholder="Amount in INR" + /> +
+
+
+ +
+ setFormData({...formData, paymentLink: e.target.value})} + className="w-full bg-white border border-brand-indigo/20 rounded-xl py-4 px-6 text-sm font-bold text-brand-indigo focus:ring-2 ring-brand-indigo/20 outline-none" + placeholder="https://forms.gle/... or payment link" + /> +
@@ -1102,14 +1139,59 @@ export const EventProposalForm: React.FC = ({ initialDat ))}
) : ( - +
+ {deptBatches.length > 0 && ( +
+ +

Or select a standard academic year below:

+
+ )} + + +
)}
diff --git a/RIT-EMS-main/frontend/src/components/dashboard/InstitutionalChecklist.tsx b/RIT-EMS-main/frontend/src/components/dashboard/InstitutionalChecklist.tsx index fd61872..ed06dbb 100644 --- a/RIT-EMS-main/frontend/src/components/dashboard/InstitutionalChecklist.tsx +++ b/RIT-EMS-main/frontend/src/components/dashboard/InstitutionalChecklist.tsx @@ -33,6 +33,7 @@ interface Event { category: string; createdAt?: string; updatedAt?: string; + targetedBatch?: string; } interface ClassMapping { @@ -63,9 +64,19 @@ const DepartmentCard: React.FC<{ index: number; isApplicable: boolean; sections: string[]; + batches: any[]; onIncompleteClick?: (data: any) => void; isCompact?: boolean; -}> = ({ deptName, events, selectedBatch, index, isApplicable, sections, onIncompleteClick, isCompact }) => { +}> = ({ deptName, events, selectedBatch, index, isApplicable, sections, batches, onIncompleteClick, isCompact }) => { + const activeBatchesForYear = useMemo(() => { + return batches.filter(b => + b.department === deptName && + b.classes?.some((cls: string) => cls.startsWith(selectedBatch)) + ); + }, [batches, deptName, selectedBatch]); + + const hasCustomBatches = activeBatchesForYear.length > 0; + const departmentEvents = events.filter(e => { const status = e.status?.toUpperCase(); const isDeptMatch = e.department?.trim().toLowerCase() === deptName.trim().toLowerCase(); @@ -75,10 +86,9 @@ const DepartmentCard: React.FC<{ (status === 'APPROVED' || status === 'COMPLETED' || status === 'PENDING_PR'); }); - // Base requirement is 1 event per category per batch, or 2 if multiple sections exist + // Base requirement is 1 event per category per batch, or 1 if no custom batches exist const getRequiredCount = (catId: string) => { - if (catId === 'INSTITUTIONAL') return 1; - return sections.length > 1 ? 2 : 1; + return 1; }; return ( @@ -97,6 +107,11 @@ const DepartmentCard: React.FC<{
{!isApplicable ? ( Inert Batch + ) : hasCustomBatches ? ( + + + {activeBatchesForYear.length} Batches + ) : ( @@ -132,9 +147,17 @@ const DepartmentCard: React.FC<{ } }); - const eventsRequired = getRequiredCount(cat.id); + const eventsRequired = hasCustomBatches ? activeBatchesForYear.length : getRequiredCount(cat.id); const missingSections = sections.filter(s => !coveredSections.has(s)); - const isFullyCovered = (missingSections.length === 0 && currentCount > 0) || (currentCount >= eventsRequired); + + const coveredBatches = activeBatchesForYear.filter(b => + matchingEvents.some(e => e.category === 'INSTITUTIONAL' || !e.targetedBatch || e.targetedBatch === b.name) + ); + const missingBatches = activeBatchesForYear.filter(b => !coveredBatches.includes(b)); + + const isFullyCovered = hasCustomBatches + ? (missingBatches.length === 0 && matchingEvents.length > 0) + : (missingSections.length === 0 && currentCount > 0) || (currentCount >= eventsRequired); return (
- {missingSections.length > 0 && currentCount > 0 && ( + {hasCustomBatches && missingBatches.length > 0 && currentCount > 0 && ( + + +
+

Batches Left Out

+

{missingBatches.map(b => b.name).join(', ')}

+
+
+ )} + + {!hasCustomBatches && missingSections.length > 0 && currentCount > 0 && (
@@ -219,6 +252,7 @@ export const InstitutionalChecklist: React.FC<{ const [classes, setClasses] = useState([]); const [loading, setLoading] = useState(true); const [selectedBatch, setSelectedBatch] = useState(''); + const [batches, setBatches] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [itemsPerPage, setItemsPerPage] = useState(6); @@ -228,9 +262,10 @@ export const InstitutionalChecklist: React.FC<{ const fetchData = async () => { try { - const [eventsRes, classesRes] = await Promise.all([ + const [eventsRes, classesRes, batchesRes] = await Promise.all([ fetch(API_BASE_URL + '/api/events'), - fetch(API_BASE_URL + '/api/classes') + fetch(API_BASE_URL + '/api/classes'), + fetch(API_BASE_URL + '/api/batches') ]); if (eventsRes.ok && classesRes.ok) { @@ -238,12 +273,14 @@ export const InstitutionalChecklist: React.FC<{ eventsRes.json(), classesRes.json() ]); + const batchesData = batchesRes.ok ? await batchesRes.json() : []; setEvents(eventsData); setClasses(classesData); + setBatches(batchesData); - const batches = Array.from(new Set(classesData.map((c: any) => c.academicYear.trim()))).sort().reverse(); - if (batches.length > 0 && !selectedBatch) { - setSelectedBatch(batches[0] as string); + const yearBatches = Array.from(new Set(classesData.map((c: any) => c.academicYear.trim()))).sort().reverse(); + if (yearBatches.length > 0 && !selectedBatch) { + setSelectedBatch(yearBatches[0] as string); } } } catch (error) { @@ -347,6 +384,7 @@ export const InstitutionalChecklist: React.FC<{ index={idx} isApplicable={deptClasses.length > 0} sections={sections} + batches={batches} onIncompleteClick={onIncompleteClick} isCompact={isCompact} /> diff --git a/RIT-EMS-main/frontend/src/components/dashboard/ManageNoticesView.tsx b/RIT-EMS-main/frontend/src/components/dashboard/ManageNoticesView.tsx new file mode 100644 index 0000000..9f61834 --- /dev/null +++ b/RIT-EMS-main/frontend/src/components/dashboard/ManageNoticesView.tsx @@ -0,0 +1,798 @@ +import React, { useState, useEffect } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + Bell, + Trash2, + Plus, + Layers, + Settings, + Upload, + Calendar, + X, + CheckCircle2, + AlertTriangle, + Globe, + Loader2, + FileImage +} from 'lucide-react'; +import { cn } from '../../lib/utils'; +import { API_BASE_URL } from '../../lib/config'; +import { useDialog } from '../../context/DialogContext'; + +interface Notice { + id: string; + title: string; + message: string; + timestamp: string; + type: 'INFO' | 'URGENT' | 'DELAY'; + image?: string; + expiresAt?: string; +} + +interface SpecialEvent { + id: string; + title: string; + description: string; + link: string; + created_at: string; +} + +export const ManageNoticesView: React.FC = () => { + const { showAlert, showConfirm } = useDialog(); + + // Navigation Tabs + const [activeTab, setActiveTab] = useState<'notices' | 'scroll' | 'special-events'>('notices'); + + // Data States + const [notices, setNotices] = useState([]); + const [specialEvents, setSpecialEvents] = useState([]); + const [scrollConfig, setScrollConfig] = useState({ + isActive: false, + title: '', + message: '' + }); + + const [loading, setLoading] = useState(true); + + // Modal States + const [isNoticeModalOpen, setIsNoticeModalOpen] = useState(false); + const [isEventModalOpen, setIsEventModalOpen] = useState(false); + + // Form States (Notice) + const [noticeTitle, setNoticeTitle] = useState(''); + const [noticeMessage, setNoticeMessage] = useState(''); + const [noticeType, setNoticeType] = useState<'INFO' | 'URGENT' | 'DELAY'>('INFO'); + const [noticeImage, setNoticeImage] = useState(null); + + // Form States (Special Event) + const [eventTitle, setEventTitle] = useState(''); + const [eventDesc, setEventDesc] = useState(''); + const [eventLink, setEventLink] = useState(''); + const [eventImage, setEventImage] = useState(null); + + const handleEventImageUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + if (file.size > 1.5 * 1024 * 1024) { + showAlert('File Too Large', 'Please upload an image smaller than 1.5MB.', 'error'); + return; + } + const reader = new FileReader(); + reader.onloadend = () => { + setEventImage(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + + useEffect(() => { + fetchInitialData(); + }, []); + + const fetchInitialData = async () => { + try { + setLoading(true); + const [noticesRes, eventsRes, settingsRes] = await Promise.all([ + fetch(`${API_BASE_URL}/api/announcements`), + fetch(`${API_BASE_URL}/api/special-events`), + fetch(`${API_BASE_URL}/api/settings`) + ]); + + if (noticesRes.ok) { + const noticesData = await noticesRes.json(); + setNotices(noticesData); + } + if (eventsRes.ok) { + const eventsData = await eventsRes.json(); + setSpecialEvents(eventsData); + } + if (settingsRes.ok) { + const settingsData = await settingsRes.json(); + if (settingsData.scroll_notification) { + setScrollConfig(settingsData.scroll_notification); + } + } + } catch (err) { + console.error('Failed to load notice configuration data:', err); + } finally { + setLoading(false); + } + }; + + // Sticky notice limits check + const handleOpenNoticeModal = () => { + if (notices.length >= 3) { + showAlert( + 'Limit Reached', + 'A maximum of 3 notices can be active on the campus notice board at any time. Please delete an existing notice first.', + 'info' + ); + return; + } + setNoticeTitle(''); + setNoticeMessage(''); + setNoticeType('INFO'); + setNoticeImage(null); + setIsNoticeModalOpen(true); + }; + + const handleImageUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + if (file.size > 1.5 * 1024 * 1024) { + showAlert('File Too Large', 'Please upload an image smaller than 1.5MB.', 'error'); + return; + } + const reader = new FileReader(); + reader.onloadend = () => { + setNoticeImage(reader.result as string); + }; + reader.readAsDataURL(file); + } + }; + + const handleAddNotice = async (e: React.FormEvent) => { + e.preventDefault(); + if (!noticeTitle.trim() || !noticeMessage.trim()) { + showAlert('Required Fields', 'Please fill in both title and message.', 'error'); + return; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/announcements`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: noticeTitle.trim(), + message: noticeMessage.trim(), + type: noticeType, + image: noticeImage + }) + }); + + if (response.ok) { + setIsNoticeModalOpen(false); + fetchInitialData(); + showAlert('Success', 'Notice posted successfully to Campus Notice Board.', 'success'); + } + } catch (err) { + console.error('Failed to post notice:', err); + showAlert('Error', 'Failed to save notice. Please try again.', 'error'); + } + }; + + const handleDeleteNotice = async (id: string) => { + showConfirm( + 'Remove Notice', + 'Are you sure you want to remove this notice from the board?', + async () => { + try { + const response = await fetch(`${API_BASE_URL}/api/announcements/${id}`, { + method: 'DELETE' + }); + if (response.ok) { + fetchInitialData(); + showAlert('Success', 'Notice removed successfully.', 'success'); + } + } catch (err) { + console.error('Delete failed:', err); + showAlert('Error', 'Failed to delete notice.', 'error'); + } + } + ); + }; + + // Scroll Notification Updates + const handleSaveScrollConfig = async (e: React.FormEvent) => { + e.preventDefault(); + if (scrollConfig.isActive && (!scrollConfig.title.trim() || !scrollConfig.message.trim())) { + showAlert('Required fields', 'Please enter a title and message for the scroll notification.', 'error'); + return; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/settings`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + key: 'scroll_notification', + value: scrollConfig + }) + }); + + if (response.ok) { + fetchInitialData(); + showAlert('Success', 'Ancient scroll notification settings saved successfully.', 'success'); + } + } catch (err) { + console.error('Save failed:', err); + showAlert('Error', 'Failed to update scroll settings.', 'error'); + } + }; + + // Special Events Updates + const handleAddSpecialEvent = async (e: React.FormEvent) => { + e.preventDefault(); + if (!eventTitle.trim() || !eventDesc.trim() || !eventLink.trim()) { + showAlert('Required Fields', 'Please fill in all special event details.', 'error'); + return; + } + + try { + const response = await fetch(`${API_BASE_URL}/api/special-events`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: eventTitle.trim(), + description: eventDesc.trim(), + link: eventLink.trim(), + created_by: 'ADMIN', + image: eventImage + }) + }); + + if (response.ok) { + setIsEventModalOpen(false); + setEventTitle(''); + setEventDesc(''); + setEventLink(''); + setEventImage(null); + fetchInitialData(); + showAlert('Success', 'Special event added to the registry successfully.', 'success'); + } + } catch (err) { + console.error('Post failed:', err); + showAlert('Error', 'Failed to create special event.', 'error'); + } + }; + + const handleDeleteSpecialEvent = async (id: string) => { + showConfirm( + 'Delete Special Event', + 'Are you sure you want to remove this event from the registry?', + async () => { + try { + const response = await fetch(`${API_BASE_URL}/api/special-events/${id}`, { + method: 'DELETE' + }); + if (response.ok) { + fetchInitialData(); + showAlert('Success', 'Special event deleted successfully.', 'success'); + } + } catch (err) { + console.error('Delete failed:', err); + showAlert('Error', 'Failed to delete event.', 'error'); + } + } + ); + }; + + if (loading) { + return ( +
+ +
+ ); + } + + return ( +
+ {/* Title */} +
+
+

Notices & Banner Settings

+

Configure notice boards, scroll announcements, & special events

+
+
+ + {/* Tabs */} +
+ + + +
+ + {/* Notices Tab */} + {activeTab === 'notices' && ( +
+
+
+

Active Campus Notices

+

These cards are pinned to the student Campus Notice Board (Maximum 3 notices).

+
+ +
+ +
+ {notices.map(notice => ( +
+
+
+ + {notice.type} + + +
+ + {notice.image && ( + Notice Attachment + )} + +

{notice.title}

+

{notice.message}

+
+ +
+ {new Date(notice.timestamp).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' })} +
+
+ ))} + + {notices.length === 0 && ( +
+ No active announcements on the notice board. +
+ )} +
+
+ )} + + {/* Scroll Notifications Tab */} + {activeTab === 'scroll' && ( +
+
+

Ancient Scroll Notifications

+

Configure a custom styled scroll overlay alert visible on the student home page.

+
+ +
+ {/* Toggle Status */} +
+
+

Display Notification Scroll

+

Turns the scroll visibility on/off for students

+
+ +
+ + {/* Title */} +
+ + setScrollConfig({...scrollConfig, title: e.target.value})} + className="w-full bg-slate-50 border border-slate-200 rounded-xl py-3 px-4 text-xs font-bold focus:bg-white focus:border-brand-indigo outline-none transition-all text-slate-800" + required={scrollConfig.isActive} + /> +
+ + {/* Message */} +
+ +