Add Admin management for Centers of Excellence, Faculty Directory, and GPA Calculator course & credit assignments
This commit is contained in:
@@ -5,11 +5,14 @@ import {
|
||||
CheckCircle2, AlertCircle, RefreshCw, Bus, Send,
|
||||
MessageCircle, BookOpen, Bot, Edit, MapPin,
|
||||
Clock, Palette, KeyRound, ExternalLink, ArrowRight,
|
||||
Code2, Check, HelpCircle, Users, Activity
|
||||
Code2, Check, HelpCircle, Users, Activity,
|
||||
UserCheck, Building2, GraduationCap, Calculator, Search
|
||||
} from 'lucide-react';
|
||||
import { getBackendUrl } from '@/lib/utils';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import { FACULTY_DATA, CLUBS_DATA, DEPARTMENTS } from '@/constants';
|
||||
import { DEPARTMENT_CURRICULUM, DEPARTMENT_CODE_MAP } from '@/constants/departmentCurriculum';
|
||||
|
||||
|
||||
interface BusStopItem {
|
||||
@@ -49,6 +52,9 @@ const TELEGRAM_STORAGE_KEY = 'RIT_LOCAL_TELEGRAM_CONFIG';
|
||||
const NOTES_STORAGE_KEY = 'RIT_LOCAL_NOTES';
|
||||
const QUESTIONS_STORAGE_KEY = 'RIT_LOCAL_QUESTIONS';
|
||||
const RECIPIENTS_STORAGE_KEY = 'RIT_LOCAL_RECIPIENTS';
|
||||
const FACULTY_STORAGE_KEY = 'RIT_LOCAL_FACULTY';
|
||||
const CLUBS_STORAGE_KEY = 'RIT_LOCAL_CLUBS';
|
||||
const CURRICULUM_STORAGE_KEY = 'RIT_LOCAL_GPA_CURRICULUM';
|
||||
|
||||
interface NoteItem {
|
||||
id: number;
|
||||
@@ -162,6 +168,9 @@ export default function AdminDashboard() {
|
||||
if (activeTab === 'notes') fetchNotes();
|
||||
if (activeTab === 'community') fetchQuestions();
|
||||
if (activeTab === 'subscribers') fetchRecipients();
|
||||
if (activeTab === 'faculty') fetchFaculty();
|
||||
if (activeTab === 'clubs') fetchClubs();
|
||||
if (activeTab === 'curriculum') fetchCurriculum();
|
||||
}, [isLoggedIn, activeTab]);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
@@ -684,6 +693,257 @@ export default function AdminDashboard() {
|
||||
fetch(getBackendUrl(`/api/admin/recipients?email=${encodeURIComponent(email)}`), { method: 'DELETE' }).catch(() => {});
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// FACULTY MANAGEMENT HANDLERS
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const fetchFaculty = () => {
|
||||
setLoadingFaculty(true);
|
||||
const saved = localStorage.getItem(FACULTY_STORAGE_KEY);
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
setFacultyList(parsed);
|
||||
setLoadingFaculty(false);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
setFacultyList(FACULTY_DATA);
|
||||
localStorage.setItem(FACULTY_STORAGE_KEY, JSON.stringify(FACULTY_DATA));
|
||||
setLoadingFaculty(false);
|
||||
};
|
||||
|
||||
const openAddFacultyModal = () => {
|
||||
setEditingFaculty(null);
|
||||
setFacultyFormData({
|
||||
name: '',
|
||||
designation: 'Assistant Professor',
|
||||
department: 'Computer Science & Engineering',
|
||||
email: '',
|
||||
office: '',
|
||||
specialization: '',
|
||||
isClassIncharge: false,
|
||||
});
|
||||
setIsFacultyModalOpen(true);
|
||||
};
|
||||
|
||||
const openEditFacultyModal = (item: any) => {
|
||||
setEditingFaculty(item);
|
||||
setFacultyFormData({
|
||||
name: item.name || '',
|
||||
designation: item.designation || '',
|
||||
department: item.department || 'Computer Science & Engineering',
|
||||
email: item.email || '',
|
||||
office: item.office || item.cabin || '',
|
||||
specialization: item.specialization || '',
|
||||
isClassIncharge: item.isClassIncharge || false,
|
||||
});
|
||||
setIsFacultyModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveFaculty = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!facultyFormData.name.trim() || !facultyFormData.department.trim()) {
|
||||
showToast('error', 'Faculty Name and Department are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
let updated: any[];
|
||||
if (editingFaculty) {
|
||||
updated = facultyList.map((f) =>
|
||||
f.id === editingFaculty.id ? { ...f, ...facultyFormData } : f
|
||||
);
|
||||
showToast('success', `Faculty ${facultyFormData.name} updated!`);
|
||||
} else {
|
||||
const newId = `fac_${Date.now()}`;
|
||||
const newFaculty = { id: newId, ...facultyFormData };
|
||||
updated = [newFaculty, ...facultyList];
|
||||
showToast('success', `Faculty ${facultyFormData.name} added!`);
|
||||
}
|
||||
|
||||
setFacultyList(updated);
|
||||
localStorage.setItem(FACULTY_STORAGE_KEY, JSON.stringify(updated));
|
||||
setIsFacultyModalOpen(false);
|
||||
|
||||
fetch(getBackendUrl('/api/admin/faculty'), {
|
||||
method: editingFaculty ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(facultyFormData),
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const handleDeleteFaculty = (id: string, name: string) => {
|
||||
if (!confirm(`Are you sure you want to delete faculty member "${name}"?`)) return;
|
||||
const updated = facultyList.filter((f) => f.id !== id);
|
||||
setFacultyList(updated);
|
||||
localStorage.setItem(FACULTY_STORAGE_KEY, JSON.stringify(updated));
|
||||
showToast('success', `Faculty "${name}" deleted.`);
|
||||
|
||||
fetch(getBackendUrl(`/api/admin/faculty/${id}`), { method: 'DELETE' }).catch(() => {});
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// CLUBS & CENTERS MANAGEMENT HANDLERS
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const fetchClubs = () => {
|
||||
setLoadingClubs(true);
|
||||
const saved = localStorage.getItem(CLUBS_STORAGE_KEY);
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (Array.isArray(parsed) && parsed.length > 0) {
|
||||
setClubsList(parsed);
|
||||
setLoadingClubs(false);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
setClubsList(CLUBS_DATA);
|
||||
localStorage.setItem(CLUBS_STORAGE_KEY, JSON.stringify(CLUBS_DATA));
|
||||
setLoadingClubs(false);
|
||||
};
|
||||
|
||||
const openAddClubModal = () => {
|
||||
setEditingClub(null);
|
||||
setClubFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
category: 'Center of Excellence',
|
||||
type: 'Center',
|
||||
coordinatorName: '',
|
||||
contactEmail: '',
|
||||
details: '',
|
||||
});
|
||||
setIsClubModalOpen(true);
|
||||
};
|
||||
|
||||
const openEditClubModal = (item: any) => {
|
||||
setEditingClub(item);
|
||||
setClubFormData({
|
||||
name: item.name || '',
|
||||
description: item.description || '',
|
||||
category: item.category || 'Center of Excellence',
|
||||
type: item.type || (item.category === 'Center of Excellence' ? 'Center' : 'Club'),
|
||||
coordinatorName: item.coordinatorName || '',
|
||||
contactEmail: item.contactEmail || '',
|
||||
details: item.details || '',
|
||||
});
|
||||
setIsClubModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveClub = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!clubFormData.name.trim() || !clubFormData.description.trim()) {
|
||||
showToast('error', 'Name and Description are required.');
|
||||
return;
|
||||
}
|
||||
|
||||
let updated: any[];
|
||||
if (editingClub) {
|
||||
updated = clubsList.map((c) =>
|
||||
c.id === editingClub.id ? { ...c, ...clubFormData } : c
|
||||
);
|
||||
showToast('success', `${clubFormData.type} "${clubFormData.name}" updated!`);
|
||||
} else {
|
||||
const newId = `club_${Date.now()}`;
|
||||
const newClub = { id: newId, members: 50, ...clubFormData };
|
||||
updated = [newClub, ...clubsList];
|
||||
showToast('success', `${clubFormData.type} "${clubFormData.name}" added!`);
|
||||
}
|
||||
|
||||
setClubsList(updated);
|
||||
localStorage.setItem(CLUBS_STORAGE_KEY, JSON.stringify(updated));
|
||||
setIsClubModalOpen(false);
|
||||
|
||||
fetch(getBackendUrl('/api/admin/clubs'), {
|
||||
method: editingClub ? 'PUT' : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(clubFormData),
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const handleDeleteClub = (id: string, name: string) => {
|
||||
if (!confirm(`Delete "${name}" from directory?`)) return;
|
||||
const updated = clubsList.filter((c) => c.id !== id);
|
||||
setClubsList(updated);
|
||||
localStorage.setItem(CLUBS_STORAGE_KEY, JSON.stringify(updated));
|
||||
showToast('success', `"${name}" removed.`);
|
||||
|
||||
fetch(getBackendUrl(`/api/admin/clubs/${id}`), { method: 'DELETE' }).catch(() => {});
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// GPA CALCULATOR CURRICULUM MANAGEMENT HANDLERS
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
const fetchCurriculum = () => {
|
||||
const saved = localStorage.getItem(CURRICULUM_STORAGE_KEY);
|
||||
if (saved) {
|
||||
try {
|
||||
const parsed = JSON.parse(saved);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
setCurriculumMap(parsed);
|
||||
return;
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
setCurriculumMap(DEPARTMENT_CURRICULUM as any);
|
||||
localStorage.setItem(CURRICULUM_STORAGE_KEY, JSON.stringify(DEPARTMENT_CURRICULUM));
|
||||
};
|
||||
|
||||
const openAddCourseModal = () => {
|
||||
setEditingCourseIndex(null);
|
||||
setCourseFormData({ name: '', credits: 3, isElective: false });
|
||||
setIsCourseModalOpen(true);
|
||||
};
|
||||
|
||||
const openEditCourseModal = (idx: number, course: any) => {
|
||||
setEditingCourseIndex(idx);
|
||||
setCourseFormData({
|
||||
name: course.name || '',
|
||||
credits: course.credits || 3,
|
||||
isElective: !!course.isElective,
|
||||
});
|
||||
setIsCourseModalOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveCourse = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!courseFormData.name.trim()) {
|
||||
showToast('error', 'Course name/code is required.');
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedMap = { ...curriculumMap };
|
||||
if (!updatedMap[selectedGpaDept]) updatedMap[selectedGpaDept] = {};
|
||||
const coursesList = [...(updatedMap[selectedGpaDept][selectedGpaSem] || (DEPARTMENT_CURRICULUM[selectedGpaDept] && DEPARTMENT_CURRICULUM[selectedGpaDept][selectedGpaSem]) || [])];
|
||||
|
||||
if (editingCourseIndex !== null) {
|
||||
coursesList[editingCourseIndex] = { ...courseFormData };
|
||||
showToast('success', `Course "${courseFormData.name}" updated!`);
|
||||
} else {
|
||||
coursesList.push({ ...courseFormData });
|
||||
showToast('success', `Course "${courseFormData.name}" added to ${selectedGpaDept} Sem ${selectedGpaSem}!`);
|
||||
}
|
||||
|
||||
updatedMap[selectedGpaDept][selectedGpaSem] = coursesList;
|
||||
setCurriculumMap(updatedMap);
|
||||
localStorage.setItem(CURRICULUM_STORAGE_KEY, JSON.stringify(updatedMap));
|
||||
setIsCourseModalOpen(false);
|
||||
};
|
||||
|
||||
const handleDeleteCourse = (idx: number, name: string) => {
|
||||
if (!confirm(`Delete course "${name}" from ${selectedGpaDept} Semester ${selectedGpaSem}?`)) return;
|
||||
const updatedMap = { ...curriculumMap };
|
||||
if (!updatedMap[selectedGpaDept]) updatedMap[selectedGpaDept] = {};
|
||||
const coursesList = [...(updatedMap[selectedGpaDept][selectedGpaSem] || [])].filter((_, i) => i !== idx);
|
||||
updatedMap[selectedGpaDept][selectedGpaSem] = coursesList;
|
||||
|
||||
setCurriculumMap(updatedMap);
|
||||
localStorage.setItem(CURRICULUM_STORAGE_KEY, JSON.stringify(updatedMap));
|
||||
showToast('success', `Course "${name}" deleted.`);
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// RENDER: LOGIN VIEW
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
@@ -870,6 +1130,9 @@ export default function AdminDashboard() {
|
||||
{ id: 'notes', label: 'Notes & Study Materials', icon: BookOpen },
|
||||
{ id: 'community', label: 'Community Q&A Moderation', icon: MessageCircle },
|
||||
{ id: 'subscribers', label: 'Deployment Email Alerts', icon: Mail },
|
||||
{ id: 'faculty', label: 'Faculty Directory', icon: UserCheck },
|
||||
{ id: 'clubs', label: 'Clubs & Centers', icon: Building2 },
|
||||
{ id: 'curriculum', label: 'GPA Curriculum & Credits', icon: Calculator },
|
||||
].map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
const isActive = activeTab === tab.id;
|
||||
@@ -1389,6 +1652,304 @@ export default function AdminDashboard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
{/* TAB 6: FACULTY DIRECTORY MANAGEMENT */}
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
{activeTab === 'faculty' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-slate-900/60 p-5 rounded-3xl border border-white/10">
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<UserCheck className="w-5 h-5 text-orange-400" />
|
||||
Faculty Directory Management ({facultyList.length})
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mt-0.5">
|
||||
Add, update, or remove faculty profiles, cabin office hours, designations, and department assignments.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Search className="w-3.5 h-3.5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
value={facultySearch}
|
||||
onChange={(e) => setFacultySearch(e.target.value)}
|
||||
placeholder="Search faculty..."
|
||||
className="bg-slate-950 border border-white/10 rounded-xl pl-9 pr-3 py-2 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={openAddFacultyModal}
|
||||
className="px-4 py-2.5 rounded-xl bg-orange-500 hover:bg-orange-600 text-white font-bold text-xs shadow-md shadow-orange-500/20 transition-all flex items-center gap-1.5 shrink-0 cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Faculty
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{facultyList
|
||||
.filter((f) =>
|
||||
!facultySearch.trim() ||
|
||||
f.name.toLowerCase().includes(facultySearch.toLowerCase()) ||
|
||||
f.department.toLowerCase().includes(facultySearch.toLowerCase()) ||
|
||||
(f.designation && f.designation.toLowerCase().includes(facultySearch.toLowerCase()))
|
||||
)
|
||||
.map((fac) => (
|
||||
<div
|
||||
key={fac.id}
|
||||
className="bg-slate-900/80 border border-white/10 rounded-2xl p-4 space-y-3 hover:border-white/20 transition-all flex flex-col justify-between"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<h4 className="text-xs font-bold text-white">{fac.name}</h4>
|
||||
<span className="text-[11px] font-medium text-orange-400 block mt-0.5">{fac.designation}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => openEditFacultyModal(fac)}
|
||||
className="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white"
|
||||
>
|
||||
<Edit className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteFaculty(fac.id, fac.name)}
|
||||
className="p-1.5 rounded-lg bg-rose-500/10 hover:bg-rose-500 text-rose-400 hover:text-white"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-[11px] text-slate-400 pt-2 border-t border-white/10">
|
||||
<p>🎓 <strong className="text-slate-200">{fac.department}</strong></p>
|
||||
{fac.email && <p>✉️ <span className="text-slate-300">{fac.email}</span></p>}
|
||||
{(fac.office || fac.cabin) && <p>📍 <span className="text-slate-300">Cabin: {fac.office || fac.cabin}</span></p>}
|
||||
{fac.specialization && <p>⚡ <span className="text-slate-400">{fac.specialization}</span></p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
{/* TAB 7: CLUBS & CENTERS OF EXCELLENCE MANAGEMENT */}
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
{activeTab === 'clubs' && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4 bg-slate-900/60 p-5 rounded-3xl border border-white/10">
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<Building2 className="w-5 h-5 text-orange-400" />
|
||||
Clubs & Centers of Excellence ({clubsList.length})
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mt-0.5">
|
||||
Manage student clubs, 15 Future Tech Centers of Excellence (CoEs), faculty lead contacts, and descriptions.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Search className="w-3.5 h-3.5 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||
<input
|
||||
type="text"
|
||||
value={clubsSearch}
|
||||
onChange={(e) => setClubsSearch(e.target.value)}
|
||||
placeholder="Search clubs & centers..."
|
||||
className="bg-slate-950 border border-white/10 rounded-xl pl-9 pr-3 py-2 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={openAddClubModal}
|
||||
className="px-4 py-2.5 rounded-xl bg-orange-500 hover:bg-orange-600 text-white font-bold text-xs shadow-md shadow-orange-500/20 transition-all flex items-center gap-1.5 shrink-0 cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Club / Center
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{clubsList
|
||||
.filter((c) =>
|
||||
!clubsSearch.trim() ||
|
||||
c.name.toLowerCase().includes(clubsSearch.toLowerCase()) ||
|
||||
c.category.toLowerCase().includes(clubsSearch.toLowerCase()) ||
|
||||
(c.coordinatorName && c.coordinatorName.toLowerCase().includes(clubsSearch.toLowerCase()))
|
||||
)
|
||||
.map((club) => (
|
||||
<div
|
||||
key={club.id}
|
||||
className="bg-slate-900/80 border border-white/10 rounded-2xl p-4 space-y-3 hover:border-white/20 transition-all flex flex-col justify-between"
|
||||
>
|
||||
<div>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className={`px-2 py-0.5 rounded-full text-[9px] font-bold uppercase ${
|
||||
club.type === 'Center' || club.category === 'Center of Excellence'
|
||||
? 'bg-purple-500/20 text-purple-300 border border-purple-500/30'
|
||||
: 'bg-blue-500/20 text-blue-300 border border-blue-500/30'
|
||||
}`}>
|
||||
{club.type || (club.category === 'Center of Excellence' ? 'Center' : 'Club')}
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="text-xs font-bold text-white line-clamp-1">{club.name}</h4>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<button
|
||||
onClick={() => openEditClubModal(club)}
|
||||
className="p-1.5 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white"
|
||||
>
|
||||
<Edit className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteClub(club.id, club.name)}
|
||||
className="p-1.5 rounded-lg bg-rose-500/10 hover:bg-rose-500 text-rose-400 hover:text-white"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-[11px] text-slate-400 line-clamp-2 mt-2">{club.description}</p>
|
||||
</div>
|
||||
|
||||
<div className="pt-2 border-t border-white/10 text-[11px] text-slate-400 space-y-1">
|
||||
{club.coordinatorName && <p>👤 Lead: <strong className="text-slate-200">{club.coordinatorName}</strong></p>}
|
||||
{club.category && <p>🏷️ Category: <span className="text-orange-400">{club.category}</span></p>}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
{/* TAB 8: GPA CALCULATOR CURRICULUM MANAGEMENT */}
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
{activeTab === 'curriculum' && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-slate-900/80 border border-white/10 rounded-3xl p-6 shadow-xl space-y-5">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div>
|
||||
<h3 className="text-base font-bold text-white flex items-center gap-2">
|
||||
<Calculator className="w-5 h-5 text-orange-400" />
|
||||
Department Courses & Credits Management (GPA Calculator)
|
||||
</h3>
|
||||
<p className="text-xs text-slate-400 mt-1">
|
||||
Assign and customize course codes, titles, and credit weightages for individual departments & semesters used in GPA calculations.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={openAddCourseModal}
|
||||
className="px-4 py-2.5 rounded-xl bg-orange-500 hover:bg-orange-600 text-white font-bold text-xs shadow-md transition-all flex items-center gap-1.5 shrink-0 cursor-pointer"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Add Course to {selectedGpaDept} Sem {selectedGpaSem}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Department & Semester Selectors */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 bg-slate-950 p-4 rounded-2xl border border-white/10">
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">Select Department</label>
|
||||
<select
|
||||
value={selectedGpaDept}
|
||||
onChange={(e) => setSelectedGpaDept(e.target.value)}
|
||||
className="w-full bg-slate-900 border border-white/10 rounded-xl px-3.5 py-2.5 text-xs text-white font-semibold outline-none focus:border-orange-500"
|
||||
>
|
||||
{['CSE', 'CSBS', 'AIML', 'AIDS', 'ECE', 'MECH', 'CIVIL', 'EEE', 'CCE', 'ECL', 'VLSI'].map((dept) => (
|
||||
<option key={dept} value={dept}>{dept} - Department</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs font-bold text-slate-300 mb-1.5">Select Semester</label>
|
||||
<select
|
||||
value={selectedGpaSem}
|
||||
onChange={(e) => setSelectedGpaSem(Number(e.target.value))}
|
||||
className="w-full bg-slate-900 border border-white/10 rounded-xl px-3.5 py-2.5 text-xs text-white font-semibold outline-none focus:border-orange-500"
|
||||
>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8].map((sem) => (
|
||||
<option key={sem} value={sem}>Semester {sem}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Courses Table for Selected Department & Semester */}
|
||||
<div className="space-y-3 pt-2">
|
||||
{(() => {
|
||||
const currentSemCourses = (curriculumMap[selectedGpaDept] && curriculumMap[selectedGpaDept][selectedGpaSem]) || (DEPARTMENT_CURRICULUM[selectedGpaDept] && DEPARTMENT_CURRICULUM[selectedGpaDept][selectedGpaSem]) || [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className="text-xs font-bold text-slate-300 block">
|
||||
Configured Courses for {selectedGpaDept} - Semester {selectedGpaSem} ({currentSemCourses.length})
|
||||
</span>
|
||||
|
||||
<div className="space-y-2">
|
||||
{currentSemCourses.length > 0 ? (
|
||||
currentSemCourses.map((course: any, idx: number) => (
|
||||
<div
|
||||
key={idx}
|
||||
className="bg-slate-950/70 border border-white/10 rounded-2xl p-4 flex items-center justify-between gap-4"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-9 h-9 rounded-xl bg-orange-500/10 border border-orange-500/20 flex items-center justify-center text-orange-400 font-extrabold text-xs shrink-0">
|
||||
{course.credits} Cr
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-bold text-white flex items-center gap-2">
|
||||
{course.name}
|
||||
{course.isElective && (
|
||||
<span className="px-2 py-0.5 rounded-full text-[9px] font-bold bg-amber-500/20 text-amber-300 border border-amber-500/30">
|
||||
Elective Slot
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
<p className="text-[10px] text-slate-400">Credit Weightage: {course.credits} Units</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => openEditCourseModal(idx, course)}
|
||||
className="p-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white"
|
||||
>
|
||||
<Edit className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteCourse(idx, course.name)}
|
||||
className="p-2 rounded-xl bg-rose-500/10 hover:bg-rose-500 text-rose-400 hover:text-white"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-xs text-slate-500 italic p-6 text-center bg-slate-950/40 rounded-2xl border border-white/5">
|
||||
No custom courses configured for {selectedGpaDept} Semester {selectedGpaSem} yet. Click "Add Course" above!
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
@@ -1576,6 +2137,283 @@ export default function AdminDashboard() {
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
{/* MODAL: CREATE / EDIT FACULTY */}
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
<AnimatePresence>
|
||||
{isFacultyModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md overflow-y-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="bg-slate-900 border border-white/10 rounded-3xl p-6 max-w-lg w-full shadow-2xl space-y-4"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-white/10 pb-3">
|
||||
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<UserCheck className="w-4 h-4 text-orange-400" />
|
||||
{editingFaculty ? `Edit Faculty: ${editingFaculty.name}` : 'Add New Faculty Member'}
|
||||
</h3>
|
||||
<button onClick={() => setIsFacultyModalOpen(false)} className="text-slate-400 hover:text-white text-sm">✕</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSaveFaculty} className="space-y-3 text-xs">
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Full Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={facultyFormData.name}
|
||||
onChange={(e) => setFacultyFormData({ ...facultyFormData, name: e.target.value })}
|
||||
placeholder="e.g. Dr. ARTHI A."
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Designation</label>
|
||||
<input
|
||||
type="text"
|
||||
value={facultyFormData.designation}
|
||||
onChange={(e) => setFacultyFormData({ ...facultyFormData, designation: e.target.value })}
|
||||
placeholder="e.g. Associate Professor & HOD"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Department</label>
|
||||
<input
|
||||
type="text"
|
||||
value={facultyFormData.department}
|
||||
onChange={(e) => setFacultyFormData({ ...facultyFormData, department: e.target.value })}
|
||||
placeholder="Computer Science & Engineering"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={facultyFormData.email}
|
||||
onChange={(e) => setFacultyFormData({ ...facultyFormData, email: e.target.value })}
|
||||
placeholder="arthi.a@ritchennai.edu.in"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Cabin / Office Room</label>
|
||||
<input
|
||||
type="text"
|
||||
value={facultyFormData.office}
|
||||
onChange={(e) => setFacultyFormData({ ...facultyFormData, office: e.target.value })}
|
||||
placeholder="CSE HOD Cabin, 2nd Floor"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Specialization / Domain</label>
|
||||
<input
|
||||
type="text"
|
||||
value={facultyFormData.specialization}
|
||||
onChange={(e) => setFacultyFormData({ ...facultyFormData, specialization: e.target.value })}
|
||||
placeholder="Machine Learning, Deep Learning, Cloud Computing"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-3 border-t border-white/10">
|
||||
<button type="button" onClick={() => setIsFacultyModalOpen(false)} className="px-4 py-2 rounded-xl bg-slate-800 text-slate-300">Cancel</button>
|
||||
<button type="submit" className="px-5 py-2 rounded-xl bg-orange-500 font-bold text-white">Save Faculty</button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
{/* MODAL: CREATE / EDIT CLUB OR CENTER */}
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
<AnimatePresence>
|
||||
{isClubModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md overflow-y-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="bg-slate-900 border border-white/10 rounded-3xl p-6 max-w-lg w-full shadow-2xl space-y-4"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-white/10 pb-3">
|
||||
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<Building2 className="w-4 h-4 text-orange-400" />
|
||||
{editingClub ? `Edit: ${editingClub.name}` : 'Add New Club / Center of Excellence'}
|
||||
</h3>
|
||||
<button onClick={() => setIsClubModalOpen(false)} className="text-slate-400 hover:text-white text-sm">✕</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSaveClub} className="space-y-3 text-xs">
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={clubFormData.name}
|
||||
onChange={(e) => setClubFormData({ ...clubFormData, name: e.target.value })}
|
||||
placeholder="e.g. Center for Artificial Intelligence & Deep Learning"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Type</label>
|
||||
<select
|
||||
value={clubFormData.type}
|
||||
onChange={(e) => setClubFormData({ ...clubFormData, type: e.target.value as any })}
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3 py-2 text-white outline-none focus:border-orange-500"
|
||||
>
|
||||
<option value="Center">Center of Excellence (CoE)</option>
|
||||
<option value="Club">Student Club</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Category</label>
|
||||
<input
|
||||
type="text"
|
||||
value={clubFormData.category}
|
||||
onChange={(e) => setClubFormData({ ...clubFormData, category: e.target.value })}
|
||||
placeholder="Center of Excellence / Technical / Cultural"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Description *</label>
|
||||
<textarea
|
||||
rows={2}
|
||||
value={clubFormData.description}
|
||||
onChange={(e) => setClubFormData({ ...clubFormData, description: e.target.value })}
|
||||
placeholder="Short description of research domain or club activities..."
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Faculty Lead / Coordinator</label>
|
||||
<input
|
||||
type="text"
|
||||
value={clubFormData.coordinatorName}
|
||||
onChange={(e) => setClubFormData({ ...clubFormData, coordinatorName: e.target.value })}
|
||||
placeholder="e.g. Dr. ARTHI A."
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Contact Email</label>
|
||||
<input
|
||||
type="email"
|
||||
value={clubFormData.contactEmail}
|
||||
onChange={(e) => setClubFormData({ ...clubFormData, contactEmail: e.target.value })}
|
||||
placeholder="coe.ai@ritchennai.edu.in"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-3 border-t border-white/10">
|
||||
<button type="button" onClick={() => setIsClubModalOpen(false)} className="px-4 py-2 rounded-xl bg-slate-800 text-slate-300">Cancel</button>
|
||||
<button type="submit" className="px-5 py-2 rounded-xl bg-orange-500 font-bold text-white">Save Entry</button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
{/* MODAL: CREATE / EDIT GPA COURSE */}
|
||||
{/* ───────────────────────────────────────────────────────────── */}
|
||||
<AnimatePresence>
|
||||
{isCourseModalOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-md overflow-y-auto">
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
className="bg-slate-900 border border-white/10 rounded-3xl p-6 max-w-md w-full shadow-2xl space-y-4"
|
||||
>
|
||||
<div className="flex items-center justify-between border-b border-white/10 pb-3">
|
||||
<h3 className="text-sm font-bold text-white flex items-center gap-2">
|
||||
<Calculator className="w-4 h-4 text-orange-400" />
|
||||
{editingCourseIndex !== null ? 'Edit Course' : `Add Course to ${selectedGpaDept} Sem ${selectedGpaSem}`}
|
||||
</h3>
|
||||
<button onClick={() => setIsCourseModalOpen(false)} className="text-slate-400 hover:text-white text-sm">✕</button>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSaveCourse} className="space-y-3 text-xs">
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Course Code & Title *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={courseFormData.name}
|
||||
onChange={(e) => setCourseFormData({ ...courseFormData, name: e.target.value })}
|
||||
placeholder="e.g. CS3301 - Data Structures"
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label className="block text-slate-300 font-semibold mb-1">Credit Weightage (1-6)</label>
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={6}
|
||||
value={courseFormData.credits}
|
||||
onChange={(e) => setCourseFormData({ ...courseFormData, credits: Number(e.target.value) })}
|
||||
className="w-full bg-slate-950 border border-white/10 rounded-xl px-3.5 py-2 text-white outline-none focus:border-orange-500 font-mono"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center pt-5">
|
||||
<label className="flex items-center gap-2 text-slate-300 font-semibold cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={courseFormData.isElective}
|
||||
onChange={(e) => setCourseFormData({ ...courseFormData, isElective: e.target.checked })}
|
||||
className="rounded border-white/10 bg-slate-950 text-orange-500 focus:ring-0"
|
||||
/>
|
||||
<span>Is Professional Elective</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-3 border-t border-white/10">
|
||||
<button type="button" onClick={() => setIsCourseModalOpen(false)} className="px-4 py-2 rounded-xl bg-slate-800 text-slate-300">Cancel</button>
|
||||
<button type="submit" className="px-5 py-2 rounded-xl bg-orange-500 font-bold text-white">Save Course</button>
|
||||
</div>
|
||||
</form>
|
||||
</motion.div>
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user