Save local admin console and unified auth changes before pulling remote UI updates

This commit is contained in:
Shanmuga Krishnan S M
2026-08-19 08:34:31 +05:30
parent 1eebd10f3a
commit bba33ef05b
6 changed files with 857 additions and 423 deletions

View File

@@ -9,6 +9,8 @@ import {
} from 'lucide-react';
import { getBackendUrl } from '@/lib/utils';
import { Link } from 'react-router-dom';
import { useAuth } from '@/context/AuthContext';
interface BusStopItem {
id?: number;
@@ -57,6 +59,7 @@ interface QuestionItem {
}
export default function AdminDashboard() {
const { user, loginWithCredentials, logout: authLogout } = useAuth();
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [userRole, setUserRole] = useState<'ROLE_TRANSPORT' | 'ROLE_SUPER_ADMIN' | null>(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' && (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-4 bg-slate-900/60 p-5 rounded-3xl border border-white/10">
<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">
<Bus className="w-5 h-5 text-orange-400" />
College Bus Routes ({routes.length} Active)
</h3>
<p className="text-xs text-slate-400">
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.
</p>
</div>
@@ -692,13 +870,13 @@ export default function AdminDashboard() {
<button
onClick={fetchRoutes}
title="Refresh routes"
className="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 border border-white/10"
className="p-2.5 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 border border-white/10 cursor-pointer"
>
<RefreshCw className={`w-4 h-4 ${loadingRoutes ? 'animate-spin' : ''}`} />
</button>
<button
onClick={openCreateRouteModal}
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white text-xs font-bold shadow-md shadow-orange-500/20 hover:opacity-95 transition-all cursor-pointer"
className="flex items-center gap-2 px-4 py-2.5 rounded-xl bg-gradient-to-r from-[#FF6B00] to-[#F97316] text-white text-xs font-bold shadow-md shadow-orange-500/20 hover:opacity-95 transition-all cursor-pointer whitespace-nowrap"
>
<Plus className="w-4 h-4" />
Add New Route
@@ -706,15 +884,34 @@ export default function AdminDashboard() {
</div>
</div>
{/* Route Search Filter */}
<div className="relative">
<input
type="text"
value={busSearchQuery}
onChange={(e) => 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"
/>
</div>
{loadingRoutes ? (
<div className="py-16 text-center text-xs text-slate-400">Loading bus routes from database...</div>
<div className="py-16 text-center text-xs text-slate-400">Loading bus routes...</div>
) : routes.length === 0 ? (
<div className="py-16 text-center text-xs text-slate-400 bg-slate-900/40 rounded-3xl border border-white/10">
No routes configured yet. Click "Add New Route" to create your first bus route!
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5">
{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) => (
<motion.div
key={route.id}
initial={{ opacity: 0, y: 10 }}

View File

@@ -15,13 +15,28 @@ export default function BusRoutes() {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
useEffect(() => {
// 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))

View File

@@ -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<StudentDashboardData | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(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<string | null>(null);
const [mockUsers, setMockUsers] = useState<MockUserCredential[]>([]);
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 (
<div className="min-h-[85vh] bg-[#0a0f1d] text-slate-100 flex items-center justify-center py-12 px-4 sm:px-6">
<div className="w-full max-w-md space-y-6">
<div className="min-h-[80vh] bg-[#0a0f1d] text-slate-100 flex items-center justify-center py-12 px-4 sm:px-6">
<div className="w-full max-w-md space-y-6 text-center">
<div className="text-center space-y-2">
<div className="w-16 h-16 rounded-2xl bg-gradient-to-br from-[#FF6B00] to-[#F97316] p-0.5 shadow-xl shadow-orange-500/20 mx-auto">
<div className="w-full h-full bg-slate-950/60 rounded-[14px] flex items-center justify-center backdrop-blur-sm">
<GraduationCap className="w-8 h-8 text-white" />
</div>
<div className="w-20 h-20 rounded-3xl bg-gradient-to-br from-[#FF6B00] to-[#F97316] p-0.5 shadow-2xl shadow-orange-500/25 mx-auto">
<div className="w-full h-full bg-slate-950/70 rounded-[22px] flex items-center justify-center backdrop-blur-sm">
<GraduationCap className="w-10 h-10 text-white" />
</div>
<h1 className="text-2xl font-extrabold text-white tracking-tight">
IMS Student Portal Sign In
</div>
<div className="space-y-2">
<h1 className="text-2xl sm:text-3xl font-extrabold text-white tracking-tight">
Student Dashboard
</h1>
<p className="text-xs text-slate-400 max-w-sm mx-auto">
Enter your official college Register Number and Password to access your timetable and assigned classroom venue.
<p className="text-xs sm:text-sm text-slate-400 max-w-sm mx-auto leading-relaxed">
Sign in with your student credentials to view your live daily timetable, assigned classroom, and department info.
</p>
</div>
{/* Login Form Card */}
<motion.div
initial={{ opacity: 0, y: 15 }}
animate={{ opacity: 1, y: 0 }}
className="bg-slate-900/80 backdrop-blur-xl border border-white/10 rounded-3xl p-6 shadow-2xl space-y-5"
className="bg-slate-900/80 backdrop-blur-xl border border-white/10 rounded-3xl p-6 shadow-2xl space-y-4"
>
{loginError && (
<div className="p-3.5 rounded-2xl bg-rose-500/10 border border-rose-500/25 flex items-start gap-2.5 text-xs text-rose-300">
<AlertCircle className="w-4 h-4 text-rose-400 shrink-0 mt-0.5" />
<span>{loginError}</span>
</div>
)}
<button
type="button"
onClick={openAuthModal}
className="w-full py-3.5 px-4 rounded-xl text-white font-bold text-sm bg-gradient-to-r from-[#FF6B00] to-[#F97316] hover:opacity-95 shadow-lg shadow-orange-500/20 transition-all flex items-center justify-center gap-2 cursor-pointer"
>
<LogIn className="w-4 h-4" />
<span>Sign In to Access Dashboard</span>
<ArrowRight className="w-4 h-4" />
</button>
<form onSubmit={handleImsLogin} className="space-y-4">
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5 flex items-center gap-1.5">
<User className="w-3.5 h-3.5 text-orange-400" />
Register Number / Roll No
</label>
<input
type="text"
value={regNumberInput}
onChange={(e) => 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
/>
</div>
<div>
<label className="block text-xs font-semibold text-slate-300 mb-1.5 flex items-center gap-1.5">
<KeyRound className="w-3.5 h-3.5 text-orange-400" />
IMS Password
</label>
<input
type="password"
value={passwordInput}
onChange={(e) => 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
/>
</div>
<button
type="submit"
disabled={isLoggingIn}
className="w-full py-3.5 px-4 rounded-xl text-white font-bold text-sm bg-gradient-to-r from-[#FF6B00] to-[#F97316] hover:opacity-95 shadow-lg shadow-orange-500/20 transition-all flex items-center justify-center gap-2 cursor-pointer"
>
{isLoggingIn ? (
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
) : (
<>
<span>Sign In to Student Portal</span>
<ArrowRight className="w-4 h-4" />
</>
)}
</button>
</form>
{/* Expandable Demo Helper */}
<div className="pt-3 border-t border-white/10">
<button
type="button"
onClick={() => setShowDemoBox(!showDemoBox)}
className="w-full text-center text-xs font-semibold text-orange-400 hover:underline flex items-center justify-center gap-1 cursor-pointer"
>
<Sparkles className="w-3.5 h-3.5" />
{showDemoBox ? 'Hide Demo Accounts' : 'Testing / Demo Accounts'}
</button>
<AnimatePresence>
{showDemoBox && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
className="overflow-hidden mt-3 space-y-2"
>
<p className="text-[11px] text-slate-400 text-center">
Click any sample student below to auto-fill the login form:
</p>
<div className="space-y-1.5">
{mockUsers.map((mock) => (
<button
key={mock.regNumber}
type="button"
onClick={() => handleQuickFillMock(mock)}
className="w-full flex items-center justify-between p-2.5 rounded-xl bg-slate-950/80 hover:bg-slate-950 border border-white/5 text-left text-xs transition-colors cursor-pointer"
>
<div>
<p className="font-bold text-white">{mock.studentName}</p>
<p className="text-[10px] text-slate-400">Reg: {mock.regNumber} ({mock.department})</p>
</div>
<span className="text-[10px] text-orange-400 font-mono">Fill Form</span>
</button>
))}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
<p className="text-[11px] text-slate-500">
Works with Register Number (e.g. <code>2114251001</code> / <code>rit@2026</code>)
</p>
</motion.div>
</div>
</div>
@@ -257,7 +124,7 @@ export default function StudentDashboard() {
<div>
<div className="flex flex-wrap items-center gap-2 mb-1">
<h1 className="text-xl sm:text-2xl font-extrabold text-white tracking-tight">
{data?.student.name || imsSession.student.name}
{data?.student.name || user?.name || imsSession?.student?.name}
</h1>
<span className="px-2.5 py-0.5 rounded-full text-[10px] font-bold tracking-wider bg-emerald-500/20 text-emerald-400 border border-emerald-500/30 uppercase">
Authenticated Student
@@ -265,11 +132,15 @@ export default function StudentDashboard() {
</div>
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-slate-400 font-medium">
<span>Reg No: <strong className="text-white">{data?.student.regNumber || imsSession.student.regNumber}</strong></span>
<span>Reg No: <strong className="text-white">{data?.student.regNumber || effectiveRegNumber}</strong></span>
<span>•</span>
<span>{data?.student.degree} {data?.student.department}</span>
<span>•</span>
<span>Year {data?.student.year}, Sem {data?.student.semester} ({data?.student.section})</span>
<span>{data?.student.degree || 'B.E.'} {data?.student.department || user?.department || 'Engineering'}</span>
{data?.student.year && (
<>
<span>•</span>
<span>Year {data.student.year}, Sem {data.student.semester} ({data.student.section})</span>
</>
)}
</div>
</div>
</div>
@@ -283,7 +154,7 @@ export default function StudentDashboard() {
<ExternalLink className="w-3.5 h-3.5" />
</Link>
<button
onClick={handleLogoutIms}
onClick={handleLogout}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-slate-800 hover:bg-rose-500/15 text-slate-300 hover:text-rose-400 border border-white/10 hover:border-rose-500/30 text-xs font-semibold transition-all cursor-pointer"
>
<LogOut className="w-4 h-4" />
@@ -308,7 +179,7 @@ export default function StudentDashboard() {
<div className="bg-slate-900/40 border border-white/10 rounded-2xl p-4 flex items-center gap-3.5">
<div className="w-10 h-10 rounded-xl bg-purple-500/10 border border-purple-500/20 flex items-center justify-center text-purple-400 shrink-0">
<Users className="w-5 h-5" />
<Building className="w-5 h-5" />
</div>
<div className="min-w-0">
<p className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Class Incharge</p>
@@ -376,7 +247,6 @@ export default function StudentDashboard() {
to={`/faculty?dept=${encodeURIComponent(data.student.department)}`}
className="flex items-center gap-1.5 px-4 py-2 rounded-xl bg-slate-900 hover:bg-slate-800 border border-white/10 text-xs font-bold text-slate-200 hover:text-white transition-all cursor-pointer whitespace-nowrap shrink-0"
>
<Users className="w-4 h-4 text-orange-400" />
<span>View {data.student.departmentCode} Faculty Directory</span>
<ArrowRight className="w-3.5 h-3.5 text-slate-400" />
</Link>
@@ -395,7 +265,7 @@ export default function StudentDashboard() {
<p className="text-slate-400 text-xs max-w-md mx-auto">{error}</p>
<button
onClick={() => loadDashboard()}
className="px-4 py-2 bg-rose-500 text-white rounded-xl text-xs font-bold hover:bg-rose-600 transition-colors"
className="px-4 py-2 bg-rose-500 text-white rounded-xl text-xs font-bold hover:bg-rose-600 transition-colors cursor-pointer"
>
Retry Connection
</button>