From bba33ef05bab19609000cf46cd6813c85a2c28a7 Mon Sep 17 00:00:00 2001 From: Shanmuga Krishnan S M Date: Wed, 19 Aug 2026 08:34:31 +0530 Subject: [PATCH] Save local admin console and unified auth changes before pulling remote UI updates --- src/components/Auth/GoogleAuthModal.tsx | 256 ++++++++++--- src/components/Navbar/Navbar.tsx | 133 ++++--- src/context/AuthContext.tsx | 279 ++++++++++---- src/pages/Admin/AdminDashboard.tsx | 357 ++++++++++++++---- src/pages/BusRoutes/BusRoutes.tsx | 19 +- .../StudentDashboard/StudentDashboard.tsx | 236 +++--------- 6 files changed, 857 insertions(+), 423 deletions(-) diff --git a/src/components/Auth/GoogleAuthModal.tsx b/src/components/Auth/GoogleAuthModal.tsx index 5d1aa2d..d9c9ec9 100644 --- a/src/components/Auth/GoogleAuthModal.tsx +++ b/src/components/Auth/GoogleAuthModal.tsx @@ -1,7 +1,11 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; -import { X, ShieldCheck, Sparkles } from 'lucide-react'; +import { + X, ShieldCheck, Sparkles, User, KeyRound, + ArrowRight, AlertCircle, GraduationCap, Bus, Shield +} from 'lucide-react'; import { useAuth } from '@/context/AuthContext'; +import { useNavigate } from 'react-router-dom'; interface GoogleAuthModalProps { isOpen: boolean; @@ -10,7 +14,14 @@ interface GoogleAuthModalProps { export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProps) { const buttonRef = useRef(null); - const { isAuthenticated } = useAuth(); + const navigate = useNavigate(); + const { isAuthenticated, loginWithCredentials } = useAuth(); + + const [usernameInput, setUsernameInput] = useState(''); + const [passwordInput, setPasswordInput] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + const [activeMode, setActiveMode] = useState<'credentials' | 'google'>('credentials'); // Close modal automatically if user gets authenticated useEffect(() => { @@ -21,7 +32,7 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp // Render Google Button inside modal when open useEffect(() => { - if (!isOpen) return; + if (!isOpen || activeMode !== 'google') return; const renderGoogleBtn = () => { if (window.google && buttonRef.current) { @@ -38,10 +49,7 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp } }; - // Render immediately if script is loaded renderGoogleBtn(); - - // Or poll briefly if script is loading const interval = setInterval(() => { if (window.google) { renderGoogleBtn(); @@ -50,7 +58,40 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp }, 200); return () => clearInterval(interval); - }, [isOpen]); + }, [isOpen, activeMode]); + + const handleSubmitCredentials = async (e: React.FormEvent) => { + e.preventDefault(); + if (!usernameInput.trim() || !passwordInput.trim()) { + setErrorMessage('Please enter both username/register number and password.'); + return; + } + + setIsLoading(true); + setErrorMessage(null); + + try { + const result = await loginWithCredentials(usernameInput.trim(), passwordInput.trim()); + if (result.success) { + onClose(); + if (result.redirectTo) { + navigate(result.redirectTo); + } + } else { + setErrorMessage(result.message || 'Invalid credentials.'); + } + } catch (err: any) { + setErrorMessage(err?.message || 'Authentication error.'); + } finally { + setIsLoading(false); + } + }; + + const handleQuickFill = (user: string, pass: string) => { + setUsernameInput(user); + setPasswordInput(pass); + setErrorMessage(null); + }; return ( @@ -62,20 +103,20 @@ export default function GoogleAuthModal({ isOpen, onClose }: GoogleAuthModalProp animate={{ opacity: 1 }} exit={{ opacity: 0 }} onClick={onClose} - className="absolute inset-0 bg-slate-950/70 backdrop-blur-md" + className="absolute inset-0 bg-slate-950/80 backdrop-blur-md" /> {/* Modal Container */} - {/* Background Orbs */} -
-
+ {/* Background Glow */} +
+
{/* Close Button */} - {/* Icon Header */} -
- + {/* Header */} +
+
+
+ +
+
+ +

+ Sign In to Freshers Hub +

+

+ Enter your student register number or admin credentials. +

- {/* Title & Description */} -

- Sign In to Freshers Hub -

- -

- Sign in with your Google account. Use your{' '} - @ritchennai.edu.in college email to unlock the{' '} - Verified Student badge and post collaboration requests! -

- - {/* Google Sign-In Button Container */} -
-
+ {/* Tab Selector */} +
+ +
- {/* Verified Student Callout */} -
- - Official RIT Student Verification via Google -
+ {/* ERROR MESSAGE */} + {errorMessage && ( +
+ + {errorMessage} +
+ )} + + {/* 1. CREDENTIALS SIGN IN FORM */} + {activeMode === 'credentials' && ( +
+
+ + setUsernameInput(e.target.value)} + placeholder="e.g. 2114251001 or Transport or Admin" + className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-2.5 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors font-mono" + required + /> +
+ +
+ + setPasswordInput(e.target.value)} + placeholder="••••••••" + className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-2.5 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors font-mono" + required + /> +
+ + + + {/* Quick Demo Fill Buttons */} +
+ + Quick Demo Credentials: + +
+ + + + +
+
+
+ )} + + {/* 2. GOOGLE SIGN IN */} + {activeMode === 'google' && ( +
+

+ Sign in with your Google account. Use your{' '} + @ritchennai.edu.in email for automatic student verification. +

+ +
+
+
+ +
+ + Google OAuth 2.0 Single Sign-On +
+
+ )}
)} diff --git a/src/components/Navbar/Navbar.tsx b/src/components/Navbar/Navbar.tsx index d31262f..d25d706 100644 --- a/src/components/Navbar/Navbar.tsx +++ b/src/components/Navbar/Navbar.tsx @@ -22,6 +22,7 @@ import { GraduationCap, LayoutDashboard, Home as HomeIcon, + ShieldAlert, } from 'lucide-react'; import { useAuth } from '@/context/AuthContext'; @@ -57,7 +58,7 @@ export default function Navbar() { const [isProfileOpen, setIsProfileOpen] = useState(false); const [isMoreOpen, setIsMoreOpen] = useState(false); const location = useLocation(); - const { user, isAuthenticated, isVerifiedStudent, loginWithGoogle, logout } = useAuth(); + const { user, isAuthenticated, isVerifiedStudent, isAdmin, isTransportAdmin, openAuthModal, logout } = useAuth(); const profileRef = useRef(null); const moreRef = useRef(null); @@ -209,7 +210,7 @@ export default function Navbar() {
- {/* Right: Auth Profile / Sign In */} + {/* Right: Auth Profile / Unified Sign In */}
{isAuthenticated && user ? (
@@ -225,12 +226,14 @@ export default function Navbar() { referrerPolicy="no-referrer" /> ) : ( -
- {user.name.charAt(0).toUpperCase()} +
+ {isAdmin ? 'A' : user.name.charAt(0).toUpperCase()}
)} {user.name.split(' ')[0]} @@ -258,8 +261,10 @@ export default function Navbar() { referrerPolicy="no-referrer" /> ) : ( -
- {user.name.charAt(0).toUpperCase()} +
+ {isAdmin ? 'A' : user.name.charAt(0).toUpperCase()}
)}
@@ -267,28 +272,45 @@ export default function Navbar() {

{user.email}

- {isVerifiedStudent ? ( -
- - Verified RIT Student -
- ) : ( -
- - Sign in with @ritchennai.edu.in + + {/* Role Badges */} + {isAdmin ? ( +
+ + + {isTransportAdmin ? 'Transport Fleet Manager' : 'System Super Admin'}
- )} + ) : isVerifiedStudent ? ( +
+ + + Verified Student {user.regNumber ? `(${user.regNumber})` : ''} + +
+ ) : null}
+
- setIsProfileOpen(false)} - className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-slate-300 hover:text-white hover:bg-orange-500/15 hover:border-orange-500/20 text-xs font-semibold transition-all cursor-pointer" - > - - My Student Dashboard - + {isAdmin ? ( + setIsProfileOpen(false)} + className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-slate-300 hover:text-white hover:bg-indigo-500/15 text-xs font-semibold transition-all cursor-pointer" + > + + Admin Console + + ) : ( + setIsProfileOpen(false)} + className="w-full flex items-center gap-2.5 px-3 py-2 rounded-xl text-slate-300 hover:text-white hover:bg-orange-500/15 text-xs font-semibold transition-all cursor-pointer" + > + + My Student Dashboard + + )} ) : ( - +
+
+ {user?.name} + + {isAdmin ? 'Admin' : (user?.regNumber || 'Student')} + +
+ {isAdmin ? ( + setIsMobileOpen(false)} + className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-xl bg-indigo-600 text-white text-xs font-bold" + > + Go to Admin Console + + ) : ( + setIsMobileOpen(false)} + className="w-full flex items-center justify-center gap-2 px-4 py-2 rounded-xl bg-orange-500 text-white text-xs font-bold" + > + Go to Student Dashboard + + )} + +
)}
diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index dda1e1f..5fc0ce8 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -2,6 +2,7 @@ import { createContext, useContext, useState, useEffect, useCallback } from 'rea import type { ReactNode } from 'react'; import { getBackendUrl } from '@/lib/utils'; import GoogleAuthModal from '@/components/Auth/GoogleAuthModal'; +import { getStoredImsSession, clearImsSession, saveImsSession, loginWithIms, type StudentInfo } from '@/services/imsService'; // ────────────────────────────────────────────────── // Types @@ -13,15 +14,28 @@ export interface AuthUser { name: string; pictureUrl?: string; verifiedStudent: boolean; + role?: 'ROLE_STUDENT' | 'ROLE_TRANSPORT' | 'ROLE_SUPER_ADMIN' | 'ROLE_USER'; + regNumber?: string; + department?: string; +} + +export interface LoginCredentialsResult { + success: boolean; + message?: string; + redirectTo?: string; + role?: string; } interface AuthContextType { user: AuthUser | null; isAuthenticated: boolean; isVerifiedStudent: boolean; + isAdmin: boolean; + isTransportAdmin: boolean; isLoading: boolean; isAuthModalOpen: boolean; loginWithGoogle: () => void; + loginWithCredentials: (username: string, password: string) => Promise; openAuthModal: () => void; closeAuthModal: () => void; logout: () => void; @@ -43,9 +57,12 @@ const AuthContext = createContext({ user: null, isAuthenticated: false, isVerifiedStudent: false, + isAdmin: false, + isTransportAdmin: false, isLoading: true, isAuthModalOpen: false, loginWithGoogle: () => {}, + loginWithCredentials: async () => ({ success: false }), openAuthModal: () => {}, closeAuthModal: () => {}, logout: () => {}, @@ -54,29 +71,13 @@ const AuthContext = createContext({ export const useAuth = () => useContext(AuthContext); // ────────────────────────────────────────────────── -// Google GSI global type +// Storage Keys // ────────────────────────────────────────────────── -declare global { - interface Window { - google?: { - accounts: { - id: { - initialize: (config: Record) => void; - prompt: (callback?: (notification: { isNotDisplayed: () => boolean; isSkippedMoment: () => boolean }) => void) => void; - renderButton: (element: HTMLElement, config: Record) => void; - revoke: (email: string, callback: () => void) => void; - }; - }; - }; - } -} - -// ────────────────────────────────────────────────── -// Provider -// ────────────────────────────────────────────────── - -const STORAGE_KEY = 'rit_auth_user'; +const GOOGLE_STORAGE_KEY = 'rit_auth_user'; +const ADMIN_TOKEN_KEY = 'RIT_ADMIN_TOKEN'; +const ADMIN_ROLE_KEY = 'RIT_ADMIN_ROLE'; +const ADMIN_USER_KEY = 'RIT_ADMIN_USER'; export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); @@ -86,7 +87,54 @@ export function AuthProvider({ children }: { children: ReactNode }) { const openAuthModal = useCallback(() => setIsAuthModalOpen(true), []); const closeAuthModal = useCallback(() => setIsAuthModalOpen(false), []); - // Handle the credential response from Google GSI + // Restore any active session on app load + useEffect(() => { + // 1. Check Admin Session + const adminToken = localStorage.getItem(ADMIN_TOKEN_KEY); + const adminRole = localStorage.getItem(ADMIN_ROLE_KEY); + const adminUser = localStorage.getItem(ADMIN_USER_KEY); + if (adminToken && adminRole) { + setUser({ + id: 999999, + name: adminUser || (adminRole === 'ROLE_TRANSPORT' ? 'Transport Admin' : 'Super Admin'), + email: adminRole === 'ROLE_TRANSPORT' ? 'transport@ritchennai.edu.in' : 'admin@ritchennai.edu.in', + verifiedStudent: false, + role: adminRole as any, + }); + setIsLoading(false); + return; + } + + // 2. Check IMS Student Session + const imsSession = getStoredImsSession(); + if (imsSession?.student) { + setUser({ + id: Number(imsSession.student.regNumber) || 1, + name: imsSession.student.name, + email: imsSession.student.email, + regNumber: imsSession.student.regNumber, + department: imsSession.student.department, + verifiedStudent: true, + role: 'ROLE_STUDENT', + }); + setIsLoading(false); + return; + } + + // 3. Check Google Session + const googleStored = localStorage.getItem(GOOGLE_STORAGE_KEY); + if (googleStored) { + try { + const parsed = JSON.parse(googleStored) as AuthUser; + setUser({ ...parsed, role: 'ROLE_USER' }); + } catch { + localStorage.removeItem(GOOGLE_STORAGE_KEY); + } + } + setIsLoading(false); + }, []); + + // Handle Google OAuth GSI const handleCredentialResponse = useCallback(async (response: { credential: string }) => { try { const res = await fetch(getBackendUrl('/api/auth/google'), { @@ -95,15 +143,13 @@ export function AuthProvider({ children }: { children: ReactNode }) { body: JSON.stringify({ credential: response.credential }), }); - if (!res.ok) { - console.error('Auth failed:', res.status); - return; + if (res.ok) { + const userData: AuthUser = await res.json(); + const userObj: AuthUser = { ...userData, role: 'ROLE_USER' }; + setUser(userObj); + localStorage.setItem(GOOGLE_STORAGE_KEY, JSON.stringify(userObj)); + setIsAuthModalOpen(false); } - - const userData: AuthUser = await res.json(); - setUser(userData); - localStorage.setItem(STORAGE_KEY, JSON.stringify(userData)); - setIsAuthModalOpen(false); } catch (err) { console.error('Google auth error:', err); } @@ -111,42 +157,8 @@ export function AuthProvider({ children }: { children: ReactNode }) { // Initialize Google GSI useEffect(() => { - // 1. Restore session from localStorage - const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - try { - const parsed = JSON.parse(stored) as AuthUser; - setUser(parsed); - - // Validate session is still valid against backend - fetch(getBackendUrl(`/api/auth/me?email=${encodeURIComponent(parsed.email)}`)) - .then((res) => { - if (!res.ok) { - localStorage.removeItem(STORAGE_KEY); - setUser(null); - } else { - return res.json(); - } - }) - .then((freshData) => { - if (freshData) { - setUser(freshData); - localStorage.setItem(STORAGE_KEY, JSON.stringify(freshData)); - } - }) - .catch(() => { - // Backend offline, keep cached session - }); - } catch { - localStorage.removeItem(STORAGE_KEY); - } - } - setIsLoading(false); - - // 2. Initialize Google GSI when the script loads const initGSI = () => { if (!window.google || !GOOGLE_CLIENT_ID) return; - window.google.accounts.id.initialize({ client_id: GOOGLE_CLIENT_ID, callback: handleCredentialResponse, @@ -169,19 +181,134 @@ export function AuthProvider({ children }: { children: ReactNode }) { }, [handleCredentialResponse]); const loginWithGoogle = useCallback(() => { - // Open the clean Google Sign-In modal directly setIsAuthModalOpen(true); - - // Also trigger One Tap prompt as a secondary background hint if supported if (window.google) { try { window.google.accounts.id.prompt(); - } catch { - // Silently ignore if browser blocks One Tap - } + } catch {} } }, []); + // ───────────────────────────────────────────────────────────── + // UNIFIED CREDENTIALS SIGN-IN (Students & Admins) + // ───────────────────────────────────────────────────────────── + const loginWithCredentials = useCallback(async (username: string, password: string): Promise => { + const cleanedUser = username.trim(); + const cleanedPass = password.trim(); + + if (!cleanedUser || !cleanedPass) { + return { success: false, message: 'Please enter both username/register number and password.' }; + } + + // 1. First, check if credentials match Admin / Transport + try { + const adminRes = await fetch(getBackendUrl('/api/admin/login'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: cleanedUser, password: cleanedPass }), + }); + + if (adminRes.ok) { + const data = await adminRes.json(); + if (data.success && data.token && data.role) { + localStorage.setItem(ADMIN_TOKEN_KEY, data.token); + localStorage.setItem(ADMIN_ROLE_KEY, data.role); + localStorage.setItem(ADMIN_USER_KEY, data.username); + + const adminUserObj: AuthUser = { + id: 999999, + name: data.username, + email: data.role === 'ROLE_TRANSPORT' ? 'transport@ritchennai.edu.in' : 'admin@ritchennai.edu.in', + verifiedStudent: false, + role: data.role, + }; + setUser(adminUserObj); + setIsAuthModalOpen(false); + + return { + success: true, + role: data.role, + redirectTo: '/admin', + message: `${data.username} logged in successfully`, + }; + } + } + } catch { + // If admin endpoint fails or is offline, check fallback + } + + // Direct local check for Admin credentials fallback + if ( + (cleanedUser.equalsIgnoreCase?.('Transport') || cleanedUser.toLowerCase() === 'transport') && + cleanedPass === 'RIT@2026' + ) { + const role = 'ROLE_TRANSPORT'; + localStorage.setItem(ADMIN_TOKEN_KEY, 'TRANSPORT_SESSION_TOKEN_RIT_2026'); + localStorage.setItem(ADMIN_ROLE_KEY, role); + localStorage.setItem(ADMIN_USER_KEY, 'Transport Admin'); + setUser({ + id: 999999, + name: 'Transport Admin', + email: 'transport@ritchennai.edu.in', + verifiedStudent: false, + role, + }); + setIsAuthModalOpen(false); + return { success: true, role, redirectTo: '/admin' }; + } + + if ( + (cleanedUser.toLowerCase() === 'admin' && (cleanedPass === 'RIT@2026' || cleanedPass === 'rit@2026')) || + (cleanedUser.toLowerCase() === 'ritadmin' && cleanedPass === 'ritadmin2026') + ) { + const role = 'ROLE_SUPER_ADMIN'; + localStorage.setItem(ADMIN_TOKEN_KEY, 'ADMIN_SESSION_TOKEN_RIT_2026'); + localStorage.setItem(ADMIN_ROLE_KEY, role); + localStorage.setItem(ADMIN_USER_KEY, 'Super Admin'); + setUser({ + id: 999999, + name: 'Super Admin', + email: 'admin@ritchennai.edu.in', + verifiedStudent: false, + role, + }); + setIsAuthModalOpen(false); + return { success: true, role, redirectTo: '/admin' }; + } + + // 2. Next, check IMS Student Login + try { + const imsResult = await loginWithIms(cleanedUser, cleanedPass); + if (imsResult.success && imsResult.student) { + const studentObj: AuthUser = { + id: Number(imsResult.student.regNumber) || 1, + name: imsResult.student.name, + email: imsResult.student.email, + regNumber: imsResult.student.regNumber, + department: imsResult.student.department, + verifiedStudent: true, + role: 'ROLE_STUDENT', + }; + setUser(studentObj); + setIsAuthModalOpen(false); + + return { + success: true, + role: 'ROLE_STUDENT', + redirectTo: '/dashboard', + message: 'Student login successful', + }; + } + } catch { + // Fallback + } + + return { + success: false, + message: 'Invalid credentials. Enter your Register Number or Admin credentials (Password: rit@2026 / RIT@2026).', + }; + }, []); + const logout = useCallback(() => { if (user?.email && window.google) { try { @@ -189,19 +316,29 @@ export function AuthProvider({ children }: { children: ReactNode }) { } catch {} } setUser(null); - localStorage.removeItem(STORAGE_KEY); + localStorage.removeItem(GOOGLE_STORAGE_KEY); + localStorage.removeItem(ADMIN_TOKEN_KEY); + localStorage.removeItem(ADMIN_ROLE_KEY); + localStorage.removeItem(ADMIN_USER_KEY); + clearImsSession(); setIsAuthModalOpen(false); }, [user]); + const isAdmin = user?.role === 'ROLE_SUPER_ADMIN' || user?.role === 'ROLE_TRANSPORT'; + const isTransportAdmin = user?.role === 'ROLE_TRANSPORT'; + return ( (null); const [usernameDisplay, setUsernameDisplay] = useState('Admin'); @@ -115,8 +118,18 @@ export default function AdminDashboard() { setTimeout(() => setToastMessage(null), 4000); }; - // Restore session + // Restore session from AuthContext or localStorage useEffect(() => { + if (user?.role === 'ROLE_TRANSPORT' || user?.role === 'ROLE_SUPER_ADMIN') { + setIsLoggedIn(true); + setUserRole(user.role); + setUsernameDisplay(user.name); + if (user.role === 'ROLE_TRANSPORT') { + setActiveTab('transport'); + } + return; + } + const savedRole = localStorage.getItem('RIT_ADMIN_ROLE') as any; const savedToken = localStorage.getItem('RIT_ADMIN_TOKEN'); if (savedToken && (savedRole === 'ROLE_TRANSPORT' || savedRole === 'ROLE_SUPER_ADMIN')) { @@ -127,7 +140,7 @@ export default function AdminDashboard() { setActiveTab('transport'); } } - }, []); + }, [user]); // Fetch data when activeTab changes useEffect(() => { @@ -142,38 +155,30 @@ export default function AdminDashboard() { // ───────────────────────────────────────────────────────────── // AUTHENTICATION // ───────────────────────────────────────────────────────────── - const handleLogin = (e: React.FormEvent) => { + const handleLogin = async (e: React.FormEvent) => { e.preventDefault(); - if (!usernameInput || !passwordInput) { + if (!usernameInput.trim() || !passwordInput.trim()) { setLoginError('Please enter both username and password.'); return; } setLoadingLogin(true); setLoginError(null); - fetch(getBackendUrl('/api/admin/login'), { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ username: usernameInput, password: passwordInput }), - }) - .then(async (res) => { - const data = await res.json(); - if (res.ok && data.success) { - localStorage.setItem('RIT_ADMIN_TOKEN', data.token); - localStorage.setItem('RIT_ADMIN_ROLE', data.role); - localStorage.setItem('RIT_ADMIN_USER', data.username); - setIsLoggedIn(true); - setUserRole(data.role); - setUsernameDisplay(data.username); - setActiveTab(data.role === 'ROLE_TRANSPORT' ? 'transport' : 'transport'); - } else { - setLoginError(data.message || 'Invalid username or password.'); - } - }) - .catch((err) => { - setLoginError(err.message || 'Server connection failed.'); - }) - .finally(() => setLoadingLogin(false)); + try { + const result = await loginWithCredentials(usernameInput.trim(), passwordInput.trim()); + if (result.success && (result.role === 'ROLE_TRANSPORT' || result.role === 'ROLE_SUPER_ADMIN')) { + setIsLoggedIn(true); + setUserRole(result.role as any); + setUsernameDisplay(result.role === 'ROLE_TRANSPORT' ? 'Transport Admin' : 'Super Admin'); + setActiveTab('transport'); + } else { + setLoginError(result.message || 'Invalid admin credentials. Use Transport or Admin with password RIT@2026.'); + } + } catch (err: any) { + setLoginError(err?.message || 'Authentication error.'); + } finally { + setLoadingLogin(false); + } }; const handleLogout = () => { @@ -184,19 +189,92 @@ export default function AdminDashboard() { setUserRole(null); setUsernameInput(''); setPasswordInput(''); + authLogout(); }; + const [busSearchQuery, setBusSearchQuery] = useState(''); + const ROUTE_STORAGE_KEY = 'RIT_LOCAL_BUS_ROUTES'; + // ───────────────────────────────────────────────────────────── // TRANSPORT ROUTE HANDLERS // ───────────────────────────────────────────────────────────── const fetchRoutes = () => { setLoadingRoutes(true); + + // 1. Try local storage first if previously edited + const saved = localStorage.getItem(ROUTE_STORAGE_KEY); + if (saved) { + try { + const parsed = JSON.parse(saved); + if (Array.isArray(parsed) && parsed.length > 0) { + setRoutes(parsed); + setLoadingRoutes(false); + } + } catch {} + } + + // 2. Fetch from backend or public/bus_routes.json fetch(getBackendUrl('/api/admin/transport/routes')) .then((res) => res.json()) .then((data) => { - if (Array.isArray(data)) setRoutes(data); + if (Array.isArray(data) && data.length > 0) { + setRoutes(data); + localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(data)); + } else { + // Fallback to static bus_routes.json if database was empty + fetch('/bus_routes.json') + .then((r) => r.json()) + .then((staticData) => { + if (Array.isArray(staticData) && staticData.length > 0) { + const formatted: BusRouteItem[] = staticData.map((s: any, idx: number) => ({ + id: idx + 1, + number: s.number, + name: s.name, + from: s.from, + to: s.to, + departureTime: s.departureTime, + arrivalTime: s.arrivalTime, + color: s.color || '#FF6B00', + stops: (s.stops || []).map((st: any, sIdx: number) => ({ + id: sIdx + 1, + name: st.name, + time: st.time, + stopOrder: sIdx + 1, + })), + })); + setRoutes(formatted); + localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(formatted)); + } + }); + } + }) + .catch(() => { + // Backend offline fallback + fetch('/bus_routes.json') + .then((r) => r.json()) + .then((staticData) => { + if (Array.isArray(staticData) && staticData.length > 0) { + const formatted: BusRouteItem[] = staticData.map((s: any, idx: number) => ({ + id: idx + 1, + number: s.number, + name: s.name, + from: s.from, + to: s.to, + departureTime: s.departureTime, + arrivalTime: s.arrivalTime, + color: s.color || '#FF6B00', + stops: (s.stops || []).map((st: any, sIdx: number) => ({ + id: sIdx + 1, + name: st.name, + time: st.time, + stopOrder: sIdx + 1, + })), + })); + setRoutes(formatted); + localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(formatted)); + } + }); }) - .catch(() => showToast('error', 'Failed to load bus routes')) .finally(() => setLoadingRoutes(false)); }; @@ -242,47 +320,62 @@ export default function AdminDashboard() { return; } + const cleanedStops = routeFormData.stops + .filter(s => s.name.trim().length > 0) + .map((s, idx) => ({ ...s, stopOrder: idx + 1, id: idx + 1 })); + const payload = { ...routeFormData, - stops: routeFormData.stops.filter(s => s.name.trim().length > 0), + stops: cleanedStops, }; - const url = editingRoute - ? getBackendUrl(`/api/admin/transport/routes/${editingRoute.id}`) - : getBackendUrl('/api/admin/transport/routes'); - - const method = editingRoute ? 'PUT' : 'POST'; + if (editingRoute) { + // Update locally + const updatedRoutes = routes.map((r) => + r.id === editingRoute.id ? { ...r, ...payload, id: editingRoute.id } : r + ); + setRoutes(updatedRoutes); + localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(updatedRoutes)); + showToast('success', `Route ${routeFormData.number} updated successfully!`); + setIsRouteModalOpen(false); - fetch(url, { - method, - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }) - .then(async (res) => { - if (res.ok) { - showToast('success', editingRoute ? 'Route updated successfully!' : 'New route created!'); - setIsRouteModalOpen(false); - fetchRoutes(); - } else { - const err = await res.json(); - showToast('error', err.error || 'Failed to save route'); - } - }) - .catch(() => showToast('error', 'Error connecting to backend')); + // Also sync to backend if online + fetch(getBackendUrl(`/api/admin/transport/routes/${editingRoute.id}`), { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }).catch(() => {}); + } else { + // Create new route + const newId = routes.length > 0 ? Math.max(...routes.map(r => r.id || 0)) + 1 : 1; + const newRoute: BusRouteItem = { + id: newId, + ...payload, + }; + const updatedRoutes = [newRoute, ...routes]; + setRoutes(updatedRoutes); + localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(updatedRoutes)); + showToast('success', `Route ${routeFormData.number} created successfully!`); + setIsRouteModalOpen(false); + + // Also sync to backend if online + fetch(getBackendUrl('/api/admin/transport/routes'), { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }).catch(() => {}); + } }; const handleDeleteRoute = (id: number, number: string) => { if (!confirm(`Are you sure you want to delete Route ${number}?`)) return; - fetch(getBackendUrl(`/api/admin/transport/routes/${id}`), { method: 'DELETE' }) - .then(async (res) => { - if (res.ok) { - showToast('success', `Route ${number} deleted.`); - fetchRoutes(); - } else { - showToast('error', 'Failed to delete route'); - } - }) - .catch(() => showToast('error', 'Failed to delete route')); + const updatedRoutes = routes.filter((r) => r.id !== id); + setRoutes(updatedRoutes); + localStorage.setItem(ROUTE_STORAGE_KEY, JSON.stringify(updatedRoutes)); + showToast('success', `Route ${number} deleted.`); + + // Sync to backend + fetch(getBackendUrl(`/api/admin/transport/routes/${id}`), { method: 'DELETE' }).catch(() => {}); }; const addStopField = () => { @@ -303,9 +396,21 @@ export default function AdminDashboard() { const fetchTelegramConfig = () => { setLoadingTelegram(true); fetch(getBackendUrl('/api/admin/telegram/config')) - .then((res) => res.json()) + .then((res) => { + if (!res.ok) throw new Error('Failed to fetch from backend'); + return res.json(); + }) .then((data) => setTelegramConfig(data)) - .catch(() => showToast('error', 'Failed to load Telegram bot configuration')) + .catch(() => { + // Offline Fallback Mock + setTelegramConfig({ + botToken: '782914X:AAH_mock_token_for_demo_environment', + seniorHelpers: [ + { chatId: 123456789, name: 'Rahul (CSE 4th Yr)' }, + { chatId: 987654321, name: 'Sneha (ECE 3rd Yr)' } + ] + }); + }) .finally(() => setLoadingTelegram(false)); }; @@ -332,7 +437,21 @@ export default function AdminDashboard() { showToast('error', err.error || 'Failed to add helper'); } }) - .catch(() => showToast('error', 'Connection error')); + .catch(() => { + // Offline Fallback + if (telegramConfig) { + setTelegramConfig({ + ...telegramConfig, + seniorHelpers: [ + ...telegramConfig.seniorHelpers, + { chatId: parseInt(newHelperChatId), name: newHelperName.trim() || 'New Helper' } + ] + }); + setNewHelperChatId(''); + setNewHelperName(''); + showToast('success', 'Senior responder added (Local Mode)!'); + } + }); }; const handleRemoveSeniorHelper = (chatId: number) => { @@ -346,7 +465,16 @@ export default function AdminDashboard() { showToast('error', 'Failed to remove helper'); } }) - .catch(() => showToast('error', 'Connection error')); + .catch(() => { + // Offline Fallback + if (telegramConfig) { + setTelegramConfig({ + ...telegramConfig, + seniorHelpers: telegramConfig.seniorHelpers.filter(h => h.chatId !== chatId) + }); + showToast('success', 'Senior helper removed (Local Mode).'); + } + }); }; const handleSaveTelegramTokens = (e: React.FormEvent) => { @@ -366,7 +494,7 @@ export default function AdminDashboard() { showToast('error', 'Failed to update bot config'); } }) - .catch(() => showToast('error', 'Connection error')) + .catch(() => showToast('success', 'Telegram config updated (Local Mode)!')) .finally(() => setSavingBotConfig(false)); }; @@ -376,9 +504,18 @@ export default function AdminDashboard() { const fetchNotes = () => { setLoadingNotes(true); fetch(getBackendUrl('/api/admin/notes')) - .then((res) => res.json()) + .then((res) => { + if (!res.ok) throw new Error(); + return res.json(); + }) .then((data) => { if (Array.isArray(data)) setNotes(data); }) - .catch(() => showToast('error', 'Failed to fetch study materials')) + .catch(() => { + // Offline Fallback Mock + setNotes([ + { id: 1, title: 'Data Structures Unit 1 Notes', subject: 'CS3301', authorName: 'Dr. ARTHI A.', department: 'AI&DS', fileUrl: '#', semester: 3, uploadDate: new Date().toISOString() }, + { id: 2, title: 'Matrices PYQ 2023', subject: 'MA3151', authorName: 'Prof. Senthil', department: 'S&H', fileUrl: '#', semester: 1, uploadDate: new Date(Date.now() - 86400000).toISOString() } + ]); + }) .finally(() => setLoadingNotes(false)); }; @@ -390,6 +527,10 @@ export default function AdminDashboard() { showToast('success', 'Note deleted.'); fetchNotes(); } + }) + .catch(() => { + setNotes(notes.filter(n => n.id !== id)); + showToast('success', 'Note deleted (Local Mode).'); }); }; @@ -399,9 +540,18 @@ export default function AdminDashboard() { const fetchQuestions = () => { setLoadingQuestions(true); fetch(getBackendUrl('/api/admin/questions')) - .then((res) => res.json()) + .then((res) => { + if (!res.ok) throw new Error(); + return res.json(); + }) .then((data) => { if (Array.isArray(data)) setQuestions(data); }) - .catch(() => showToast('error', 'Failed to fetch community questions')) + .catch(() => { + // Offline Fallback Mock + setQuestions([ + { id: 1, title: "When is the fresher's orientation?", content: "I couldn't find the exact date for the CSE orientation.", authorName: "Karthik", authorEmail: "karthik.2024@ritchennai.edu.in", createdAt: new Date().toISOString(), status: "PENDING", tags: ["orientation"], votes: 0 }, + { id: 2, title: "Are laptops mandatory in first year?", content: "Just wondering if we need to bring laptops to college every day.", authorName: "Sneha", authorEmail: "sneha.2024@ritchennai.edu.in", createdAt: new Date(Date.now() - 86400000).toISOString(), status: "APPROVED", tags: ["academics"], votes: 5 } + ]); + }) .finally(() => setLoadingQuestions(false)); }; @@ -413,6 +563,10 @@ export default function AdminDashboard() { showToast('success', 'Question deleted.'); fetchQuestions(); } + }) + .catch(() => { + setQuestions(questions.filter(q => q.id !== id)); + showToast('success', 'Question deleted (Local Mode).'); }); }; @@ -422,9 +576,18 @@ export default function AdminDashboard() { const fetchRecipients = () => { setLoadingRecipients(true); fetch(getBackendUrl('/api/admin/recipients')) - .then((res) => res.json()) + .then((res) => { + if (!res.ok) throw new Error(); + return res.json(); + }) .then((data) => { if (Array.isArray(data)) setRecipients(data); }) - .catch(() => showToast('error', 'Failed to fetch email subscribers')) + .catch(() => { + // Offline Fallback Mock + setRecipients([ + 'devops@ritchennai.edu.in', + 'admin@ritchennai.edu.in' + ]); + }) .finally(() => setLoadingRecipients(false)); }; @@ -440,7 +603,10 @@ export default function AdminDashboard() { headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: newEmail }), }) - .then((res) => res.json()) + .then((res) => { + if (!res.ok) throw new Error(); + return res.json(); + }) .then((data) => { if (data.success) { showToast('success', data.message || 'Email added successfully'); @@ -450,18 +616,30 @@ export default function AdminDashboard() { showToast('error', data.message || 'Could not add email'); } }) + .catch(() => { + setRecipients([...recipients, newEmail]); + setNewEmail(''); + showToast('success', 'Email added (Local Mode)'); + }) .finally(() => setSubmittingEmail(false)); }; const handleRemoveRecipient = (email: string) => { if (!confirm(`Remove ${email} from deployment notifications?`)) return; fetch(getBackendUrl(`/api/admin/recipients?email=${encodeURIComponent(email)}`), { method: 'DELETE' }) - .then((res) => res.json()) + .then((res) => { + if (!res.ok) throw new Error(); + return res.json(); + }) .then((data) => { if (data.success) { showToast('success', 'Removed email recipient'); fetchRecipients(); } + }) + .catch(() => { + setRecipients(recipients.filter((r) => r !== email)); + showToast('success', 'Removed email recipient (Local Mode)'); }); }; @@ -677,14 +855,14 @@ export default function AdminDashboard() { {/* ───────────────────────────────────────────────────────────── */} {activeTab === 'transport' && (
-
+

College Bus Routes ({routes.length} Active)

- Changes made here update the student Bus Routes directory and driver tracking app instantly. + Edit departure times, intermediate stops, colors, or create and delete routes instantly.

@@ -692,13 +870,13 @@ export default function AdminDashboard() {
+ {/* Route Search Filter */} +
+ setBusSearchQuery(e.target.value)} + placeholder="Search routes by number (e.g. R01, RIT-01), name, or origin..." + className="w-full bg-slate-900/80 border border-white/10 rounded-2xl px-4 py-3 text-xs text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors" + /> +
+ {loadingRoutes ? ( -
Loading bus routes from database...
+
Loading bus routes...
) : routes.length === 0 ? (
No routes configured yet. Click "Add New Route" to create your first bus route!
) : (
- {routes.map((route) => ( + {routes + .filter( + (r) => + !busSearchQuery.trim() || + r.name.toLowerCase().includes(busSearchQuery.toLowerCase()) || + r.number.toLowerCase().includes(busSearchQuery.toLowerCase()) || + r.from.toLowerCase().includes(busSearchQuery.toLowerCase()) + ) + .map((route) => ( { + // 0. Check local storage if edited by Transport Admin + const localAdminRoutes = localStorage.getItem('RIT_LOCAL_BUS_ROUTES'); + if (localAdminRoutes) { + try { + const parsed = JSON.parse(localAdminRoutes); + if (Array.isArray(parsed) && parsed.length > 0) { + setBusRoutes(parsed); + setSelectedBusRoute(parsed[0]); + setLoadingBus(false); + } + } catch {} + } + // 1. Instantly load static bus routes (0ms loading delay) fetch('/bus_routes.json') .then((res) => res.json()) .then((data) => { if (Array.isArray(data) && data.length > 0) { - setBusRoutes(data); - setSelectedBusRoute(data[0]); + if (!localStorage.getItem('RIT_LOCAL_BUS_ROUTES')) { + setBusRoutes(data); + setSelectedBusRoute(data[0]); + } } }) .catch((err) => console.error('Local JSON load error:', err)) diff --git a/src/pages/StudentDashboard/StudentDashboard.tsx b/src/pages/StudentDashboard/StudentDashboard.tsx index 451b639..fc85df5 100644 --- a/src/pages/StudentDashboard/StudentDashboard.tsx +++ b/src/pages/StudentDashboard/StudentDashboard.tsx @@ -1,56 +1,35 @@ import { useState, useEffect } from 'react'; -import { motion, AnimatePresence } from 'framer-motion'; +import { motion } from 'framer-motion'; import { - GraduationCap, Clock, MapPin, Users, - Calendar, Building, ShieldCheck, - Sparkles, KeyRound, ArrowRight, LogOut, - AlertCircle, User, HelpCircle, Compass, ExternalLink + GraduationCap, Clock, MapPin, + Calendar, Building, ArrowRight, LogOut, + HelpCircle, Compass, ExternalLink, LogIn } from 'lucide-react'; import { Link } from 'react-router-dom'; import { useAuth } from '@/context/AuthContext'; import { fetchStudentDashboard, - loginWithIms, - fetchMockImsUsers, getStoredImsSession, clearImsSession, - type StudentDashboardData, - type MockUserCredential + type StudentDashboardData } from '@/services/imsService'; export default function StudentDashboard() { - const { user } = useAuth(); + const { user, openAuthModal, logout } = useAuth(); const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState<'timetable' | 'location'>('timetable'); - // IMS Authentication Session State - const [imsSession, setImsSession] = useState<{ token: string; student: any } | null>(getStoredImsSession()); - const [regNumberInput, setRegNumberInput] = useState(''); - const [passwordInput, setPasswordInput] = useState(''); - const [isLoggingIn, setIsLoggingIn] = useState(false); - const [loginError, setLoginError] = useState(null); - const [mockUsers, setMockUsers] = useState([]); - const [showDemoBox, setShowDemoBox] = useState(false); - - // Load available mock accounts for demo helper - useEffect(() => { - fetchMockImsUsers().then((users) => { - setMockUsers(users); - if (users.length > 0 && !regNumberInput) { - setRegNumberInput(users[0].regNumber); - setPasswordInput(users[0].defaultPassword); - } - }); - }, []); + const imsSession = getStoredImsSession(); + const effectiveRegNumber = user?.regNumber || imsSession?.student?.regNumber; const loadDashboard = async () => { - if (!imsSession?.student?.regNumber) return; + if (!effectiveRegNumber) return; setIsLoading(true); setError(null); try { - const result = await fetchStudentDashboard(imsSession.student.regNumber, user?.email); + const result = await fetchStudentDashboard(effectiveRegNumber, user?.email); setData(result); } catch (err: any) { console.error('Error loading student dashboard:', err); @@ -61,172 +40,60 @@ export default function StudentDashboard() { }; useEffect(() => { - if (imsSession) { + if (effectiveRegNumber) { loadDashboard(); } else { setData(null); } - }, [imsSession]); + }, [effectiveRegNumber, user]); - const handleImsLogin = async (e?: React.FormEvent) => { - if (e) e.preventDefault(); - if (!regNumberInput.trim() || !passwordInput.trim()) { - setLoginError('Please enter both Register Number and Password.'); - return; - } - - setIsLoggingIn(true); - setLoginError(null); - - try { - const res = await loginWithIms(regNumberInput.trim(), passwordInput.trim()); - if (res.success && res.token && res.student) { - setImsSession({ token: res.token, student: res.student }); - } else { - setLoginError(res.message || 'Invalid Register Number or Password.'); - } - } catch (err: any) { - setLoginError(err?.message || 'Failed to authenticate with IMS server.'); - } finally { - setIsLoggingIn(false); - } - }; - - const handleQuickFillMock = (mock: MockUserCredential) => { - setRegNumberInput(mock.regNumber); - setPasswordInput(mock.defaultPassword); - }; - - const handleLogoutIms = () => { + const handleLogout = () => { clearImsSession(); - setImsSession(null); + logout(); setData(null); }; // ───────────────────────────────────────────────────────────── - // VIEW 1: IMS STUDENT LOGIN PORTAL (When not signed in to IMS) + // VIEW 1: UNAUTHENTICATED INVITATION SCREEN // ───────────────────────────────────────────────────────────── - if (!imsSession) { + if (!effectiveRegNumber) { return ( -
-
+
+
-
-
-
- -
+
+
+
-

- IMS Student Portal Sign In +

+ +
+

+ Student Dashboard

-

- Enter your official college Register Number and Password to access your timetable and assigned classroom venue. +

+ Sign in with your student credentials to view your live daily timetable, assigned classroom, and department info.

- {/* Login Form Card */} - {loginError && ( -
- - {loginError} -
- )} + -
-
- - setRegNumberInput(e.target.value)} - placeholder="e.g. 2114251001" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors font-mono" - required - /> -
- -
- - setPasswordInput(e.target.value)} - placeholder="••••••••" - className="w-full bg-slate-950 border border-white/10 rounded-xl px-4 py-3 text-sm text-white placeholder-slate-500 outline-none focus:border-orange-500 transition-colors font-mono" - required - /> -
- - -
- - {/* Expandable Demo Helper */} -
- - - - {showDemoBox && ( - -

- Click any sample student below to auto-fill the login form: -

-
- {mockUsers.map((mock) => ( - - ))} -
-
- )} -
-
+

+ Works with Register Number (e.g. 2114251001 / rit@2026) +

@@ -257,7 +124,7 @@ export default function StudentDashboard() {

- {data?.student.name || imsSession.student.name} + {data?.student.name || user?.name || imsSession?.student?.name}

Authenticated Student @@ -265,11 +132,15 @@ export default function StudentDashboard() {
- Reg No: {data?.student.regNumber || imsSession.student.regNumber} + Reg No: {data?.student.regNumber || effectiveRegNumber} • - {data?.student.degree} {data?.student.department} - • - Year {data?.student.year}, Sem {data?.student.semester} ({data?.student.section}) + {data?.student.degree || 'B.E.'} {data?.student.department || user?.department || 'Engineering'} + {data?.student.year && ( + <> + • + Year {data.student.year}, Sem {data.student.semester} ({data.student.section}) + + )}
@@ -283,7 +154,7 @@ export default function StudentDashboard() {