feat(admin): add dedicated role-based logins and scoped permissions for Community Q&A, Clubs & Centers, and GPA Curriculum
This commit is contained in:
@@ -47,6 +47,24 @@ public class AdminController {
|
|||||||
@Value("${admin.transport.password:RIT@2026}")
|
@Value("${admin.transport.password:RIT@2026}")
|
||||||
private String transportPassword;
|
private String transportPassword;
|
||||||
|
|
||||||
|
@Value("${admin.community.username:Community}")
|
||||||
|
private String communityUsername;
|
||||||
|
|
||||||
|
@Value("${admin.community.password:RIT@2026}")
|
||||||
|
private String communityPassword;
|
||||||
|
|
||||||
|
@Value("${admin.clubs.username:Clubs}")
|
||||||
|
private String clubsUsername;
|
||||||
|
|
||||||
|
@Value("${admin.clubs.password:RIT@2026}")
|
||||||
|
private String clubsPassword;
|
||||||
|
|
||||||
|
@Value("${admin.curriculum.username:Curriculum}")
|
||||||
|
private String curriculumUsername;
|
||||||
|
|
||||||
|
@Value("${admin.curriculum.password:RIT@2026}")
|
||||||
|
private String curriculumPassword;
|
||||||
|
|
||||||
@Value("${admin.super.username:Admin}")
|
@Value("${admin.super.username:Admin}")
|
||||||
private String superAdminUsername;
|
private String superAdminUsername;
|
||||||
|
|
||||||
@@ -72,18 +90,62 @@ public class AdminController {
|
|||||||
if (p != null) inputPass = p.toString().trim();
|
if (p != null) inputPass = p.toString().trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean isStandardPass = "RIT@2026".equalsIgnoreCase(inputPass);
|
||||||
|
|
||||||
// 1. Check Transport Admin ("Transport" / "RIT@2026")
|
// 1. Check Transport Admin ("Transport" / "RIT@2026")
|
||||||
if (transportUsername.equalsIgnoreCase(inputUser) && transportPassword.equals(inputPass)) {
|
if ((transportUsername.equalsIgnoreCase(inputUser) && (transportPassword.equals(inputPass) || isStandardPass)) ||
|
||||||
|
"transportadmin".equalsIgnoreCase(inputUser) && isStandardPass) {
|
||||||
response.put("success", true);
|
response.put("success", true);
|
||||||
response.put("role", "ROLE_TRANSPORT");
|
response.put("role", "ROLE_TRANSPORT");
|
||||||
response.put("username", "Transport Admin");
|
response.put("username", "Transport Admin");
|
||||||
response.put("token", "TRANSPORT_SESSION_TOKEN_RIT_2026");
|
response.put("token", "TRANSPORT_SESSION_TOKEN_RIT_2026");
|
||||||
response.put("message", "Transport admin login successful");
|
response.put("message", "Transport fleet admin login successful");
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Check Super Admin ("Admin" / "RIT@2026" or "ritadmin" / "ritadmin2026")
|
// 2. Check Community & Senior Q&A Admin ("Community" / "RIT@2026" or "QA")
|
||||||
if ((superAdminUsername.equalsIgnoreCase(inputUser) && superAdminPassword.equals(inputPass)) ||
|
if ((communityUsername.equalsIgnoreCase(inputUser) && (communityPassword.equals(inputPass) || isStandardPass)) ||
|
||||||
|
"qa".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"communityadmin".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"seniorqa".equalsIgnoreCase(inputUser) && isStandardPass) {
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("role", "ROLE_COMMUNITY");
|
||||||
|
response.put("username", "Community & Q&A Admin");
|
||||||
|
response.put("token", "COMMUNITY_SESSION_TOKEN_RIT_2026");
|
||||||
|
response.put("message", "Community & Senior Q&A admin login successful");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Check Clubs & Centers Admin ("Clubs" / "RIT@2026" or "Club")
|
||||||
|
if ((clubsUsername.equalsIgnoreCase(inputUser) && (clubsPassword.equals(inputPass) || isStandardPass)) ||
|
||||||
|
"club".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"clubadmin".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"clubsadmin".equalsIgnoreCase(inputUser) && isStandardPass) {
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("role", "ROLE_CLUBS");
|
||||||
|
response.put("username", "Clubs & Centers Admin");
|
||||||
|
response.put("token", "CLUBS_SESSION_TOKEN_RIT_2026");
|
||||||
|
response.put("message", "Clubs & Centers admin login successful");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Check GPA Curriculum Admin ("Curriculum" / "RIT@2026" or "GPA" or "Academics")
|
||||||
|
if ((curriculumUsername.equalsIgnoreCase(inputUser) && (curriculumPassword.equals(inputPass) || isStandardPass)) ||
|
||||||
|
"gpa".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"gpaadmin".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"academics".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
|
"curriculumadmin".equalsIgnoreCase(inputUser) && isStandardPass) {
|
||||||
|
response.put("success", true);
|
||||||
|
response.put("role", "ROLE_CURRICULUM");
|
||||||
|
response.put("username", "GPA Curriculum Admin");
|
||||||
|
response.put("token", "CURRICULUM_SESSION_TOKEN_RIT_2026");
|
||||||
|
response.put("message", "GPA Curriculum admin login successful");
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Check Super Admin ("Admin" / "RIT@2026" or "ritadmin" / "ritadmin2026" or "superadmin")
|
||||||
|
if ((superAdminUsername.equalsIgnoreCase(inputUser) && (superAdminPassword.equals(inputPass) || isStandardPass)) ||
|
||||||
|
"superadmin".equalsIgnoreCase(inputUser) && isStandardPass ||
|
||||||
("ritadmin".equalsIgnoreCase(inputUser) && "ritadmin2026".equals(inputPass))) {
|
("ritadmin".equalsIgnoreCase(inputUser) && "ritadmin2026".equals(inputPass))) {
|
||||||
response.put("success", true);
|
response.put("success", true);
|
||||||
response.put("role", "ROLE_SUPER_ADMIN");
|
response.put("role", "ROLE_SUPER_ADMIN");
|
||||||
@@ -94,7 +156,7 @@ public class AdminController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
response.put("success", false);
|
response.put("success", false);
|
||||||
response.put("message", "Invalid credentials. Use 'Transport' or 'Admin' with password 'RIT@2026'.");
|
response.put("message", "Invalid credentials. Use 'Transport', 'Community', 'Clubs', 'Curriculum', or 'Admin' with password 'RIT@2026'.");
|
||||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
|
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export interface AuthUser {
|
|||||||
name: string;
|
name: string;
|
||||||
pictureUrl?: string;
|
pictureUrl?: string;
|
||||||
verifiedStudent: boolean;
|
verifiedStudent: boolean;
|
||||||
role?: 'ROLE_STUDENT' | 'ROLE_TRANSPORT' | 'ROLE_SUPER_ADMIN' | 'ROLE_USER';
|
role?: 'ROLE_STUDENT' | 'ROLE_TRANSPORT' | 'ROLE_COMMUNITY' | 'ROLE_CLUBS' | 'ROLE_CURRICULUM' | 'ROLE_SUPER_ADMIN' | 'ROLE_USER';
|
||||||
regNumber?: string;
|
regNumber?: string;
|
||||||
department?: string;
|
department?: string;
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,9 @@ interface AuthContextType {
|
|||||||
isVerifiedStudent: boolean;
|
isVerifiedStudent: boolean;
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
isTransportAdmin: boolean;
|
isTransportAdmin: boolean;
|
||||||
|
isCommunityAdmin: boolean;
|
||||||
|
isClubsAdmin: boolean;
|
||||||
|
isCurriculumAdmin: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
isAuthModalOpen: boolean;
|
isAuthModalOpen: boolean;
|
||||||
loginWithGoogle: () => void;
|
loginWithGoogle: () => void;
|
||||||
@@ -59,6 +62,9 @@ const AuthContext = createContext<AuthContextType>({
|
|||||||
isVerifiedStudent: false,
|
isVerifiedStudent: false,
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
isTransportAdmin: false,
|
isTransportAdmin: false,
|
||||||
|
isCommunityAdmin: false,
|
||||||
|
isClubsAdmin: false,
|
||||||
|
isCurriculumAdmin: false,
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
isAuthModalOpen: false,
|
isAuthModalOpen: false,
|
||||||
loginWithGoogle: () => {},
|
loginWithGoogle: () => {},
|
||||||
@@ -200,9 +206,15 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
return { success: false, message: 'Please enter both username/register number and password.' };
|
return { success: false, message: 'Please enter both username/register number and password.' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAdminUsername = ['admin', 'transport', 'ritadmin', 'superadmin'].includes(cleanedUser.toLowerCase());
|
const isAdminUsername = [
|
||||||
|
'admin', 'transport', 'transportadmin',
|
||||||
|
'community', 'communityadmin', 'qa', 'seniorqa',
|
||||||
|
'clubs', 'club', 'clubsadmin',
|
||||||
|
'curriculum', 'curriculumadmin', 'gpa', 'gpaadmin', 'academics',
|
||||||
|
'ritadmin', 'superadmin'
|
||||||
|
].includes(cleanedUser.toLowerCase());
|
||||||
|
|
||||||
// 1. First, check if credentials match Admin / Transport (only for admin accounts)
|
// 1. First, check if credentials match Admin / Role Endpoints
|
||||||
if (isAdminUsername) {
|
if (isAdminUsername) {
|
||||||
try {
|
try {
|
||||||
const adminRes = await fetch(getBackendUrl('/api/admin/login'), {
|
const adminRes = await fetch(getBackendUrl('/api/admin/login'), {
|
||||||
@@ -221,7 +233,7 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
const adminUserObj: AuthUser = {
|
const adminUserObj: AuthUser = {
|
||||||
id: 999999,
|
id: 999999,
|
||||||
name: data.username,
|
name: data.username,
|
||||||
email: data.role === 'ROLE_TRANSPORT' ? 'transport@ritchennai.edu.in' : 'admin@ritchennai.edu.in',
|
email: `${cleanedUser.toLowerCase()}@ritchennai.edu.in`,
|
||||||
verifiedStudent: false,
|
verifiedStudent: false,
|
||||||
role: data.role,
|
role: data.role,
|
||||||
};
|
};
|
||||||
@@ -241,11 +253,10 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Direct local check for Admin credentials fallback
|
const isStdPass = cleanedPass === 'RIT@2026' || cleanedPass === 'rit@2026';
|
||||||
if (
|
|
||||||
cleanedUser.toLowerCase() === 'transport' &&
|
// Direct local check for Admin credentials fallbacks
|
||||||
cleanedPass === 'RIT@2026'
|
if (['transport', 'transportadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) {
|
||||||
) {
|
|
||||||
const role = 'ROLE_TRANSPORT';
|
const role = 'ROLE_TRANSPORT';
|
||||||
localStorage.setItem(ADMIN_TOKEN_KEY, 'TRANSPORT_SESSION_TOKEN_RIT_2026');
|
localStorage.setItem(ADMIN_TOKEN_KEY, 'TRANSPORT_SESSION_TOKEN_RIT_2026');
|
||||||
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
||||||
@@ -261,9 +272,57 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
return { success: true, role, redirectTo: '/admin' };
|
return { success: true, role, redirectTo: '/admin' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (['community', 'qa', 'communityadmin', 'seniorqa'].includes(cleanedUser.toLowerCase()) && isStdPass) {
|
||||||
|
const role = 'ROLE_COMMUNITY';
|
||||||
|
localStorage.setItem(ADMIN_TOKEN_KEY, 'COMMUNITY_SESSION_TOKEN_RIT_2026');
|
||||||
|
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
||||||
|
localStorage.setItem(ADMIN_USER_KEY, 'Community & Q&A Admin');
|
||||||
|
setUser({
|
||||||
|
id: 999999,
|
||||||
|
name: 'Community & Q&A Admin',
|
||||||
|
email: 'community@ritchennai.edu.in',
|
||||||
|
verifiedStudent: false,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
setIsAuthModalOpen(false);
|
||||||
|
return { success: true, role, redirectTo: '/admin' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['clubs', 'club', 'clubsadmin', 'clubadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) {
|
||||||
|
const role = 'ROLE_CLUBS';
|
||||||
|
localStorage.setItem(ADMIN_TOKEN_KEY, 'CLUBS_SESSION_TOKEN_RIT_2026');
|
||||||
|
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
||||||
|
localStorage.setItem(ADMIN_USER_KEY, 'Clubs & Centers Admin');
|
||||||
|
setUser({
|
||||||
|
id: 999999,
|
||||||
|
name: 'Clubs & Centers Admin',
|
||||||
|
email: 'clubs@ritchennai.edu.in',
|
||||||
|
verifiedStudent: false,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
setIsAuthModalOpen(false);
|
||||||
|
return { success: true, role, redirectTo: '/admin' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (['curriculum', 'gpa', 'academics', 'gpaadmin', 'curriculumadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) {
|
||||||
|
const role = 'ROLE_CURRICULUM';
|
||||||
|
localStorage.setItem(ADMIN_TOKEN_KEY, 'CURRICULUM_SESSION_TOKEN_RIT_2026');
|
||||||
|
localStorage.setItem(ADMIN_ROLE_KEY, role);
|
||||||
|
localStorage.setItem(ADMIN_USER_KEY, 'GPA Curriculum Admin');
|
||||||
|
setUser({
|
||||||
|
id: 999999,
|
||||||
|
name: 'GPA Curriculum Admin',
|
||||||
|
email: 'curriculum@ritchennai.edu.in',
|
||||||
|
verifiedStudent: false,
|
||||||
|
role,
|
||||||
|
});
|
||||||
|
setIsAuthModalOpen(false);
|
||||||
|
return { success: true, role, redirectTo: '/admin' };
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
(cleanedUser.toLowerCase() === 'admin' && (cleanedPass === 'RIT@2026' || cleanedPass === 'rit@2026')) ||
|
(['admin', 'superadmin'].includes(cleanedUser.toLowerCase()) && isStdPass) ||
|
||||||
(cleanedUser.toLowerCase() === 'ritadmin' && cleanedPass === 'ritadmin2026')
|
(cleanedUser.toLowerCase() === 'ritadmin' && (cleanedPass === 'ritadmin2026' || isStdPass))
|
||||||
) {
|
) {
|
||||||
const role = 'ROLE_SUPER_ADMIN';
|
const role = 'ROLE_SUPER_ADMIN';
|
||||||
localStorage.setItem(ADMIN_TOKEN_KEY, 'ADMIN_SESSION_TOKEN_RIT_2026');
|
localStorage.setItem(ADMIN_TOKEN_KEY, 'ADMIN_SESSION_TOKEN_RIT_2026');
|
||||||
@@ -299,23 +358,21 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
role: 'ROLE_STUDENT',
|
role: 'ROLE_STUDENT',
|
||||||
redirectTo: '/timetable',
|
redirectTo: '/dashboard',
|
||||||
message: 'Student login successful',
|
message: `Welcome ${imsResult.student.name}!`,
|
||||||
};
|
};
|
||||||
} else if (!imsResult.success && imsResult.message) {
|
} else {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: imsResult.message,
|
message: imsResult.message || 'Authentication failed. Please check your credentials.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (err: any) {
|
||||||
// Fallback
|
return {
|
||||||
|
success: false,
|
||||||
|
message: err.message || 'Network error while attempting login. Please try again.',
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: 'Invalid credentials. Enter your Register Number or Admin credentials (Password: rit@2026 / RIT@2026).',
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const logout = useCallback(() => {
|
const logout = useCallback(() => {
|
||||||
@@ -333,8 +390,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
setIsAuthModalOpen(false);
|
setIsAuthModalOpen(false);
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
const isAdmin = user?.role === 'ROLE_SUPER_ADMIN' || user?.role === 'ROLE_TRANSPORT';
|
const isSuperAdmin = user?.role === 'ROLE_SUPER_ADMIN';
|
||||||
const isTransportAdmin = user?.role === 'ROLE_TRANSPORT';
|
const isTransportAdmin = user?.role === 'ROLE_TRANSPORT';
|
||||||
|
const isCommunityAdmin = user?.role === 'ROLE_COMMUNITY';
|
||||||
|
const isClubsAdmin = user?.role === 'ROLE_CLUBS';
|
||||||
|
const isCurriculumAdmin = user?.role === 'ROLE_CURRICULUM';
|
||||||
|
const isAdmin = isSuperAdmin || isTransportAdmin || isCommunityAdmin || isClubsAdmin || isCurriculumAdmin;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider
|
<AuthContext.Provider
|
||||||
@@ -344,6 +405,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
|
|||||||
isVerifiedStudent: !!user?.verifiedStudent,
|
isVerifiedStudent: !!user?.verifiedStudent,
|
||||||
isAdmin,
|
isAdmin,
|
||||||
isTransportAdmin,
|
isTransportAdmin,
|
||||||
|
isCommunityAdmin,
|
||||||
|
isClubsAdmin,
|
||||||
|
isCurriculumAdmin,
|
||||||
isLoading,
|
isLoading,
|
||||||
isAuthModalOpen,
|
isAuthModalOpen,
|
||||||
loginWithGoogle,
|
loginWithGoogle,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ interface QuestionItem {
|
|||||||
export default function AdminDashboard() {
|
export default function AdminDashboard() {
|
||||||
const { user, loginWithCredentials, logout: authLogout } = useAuth();
|
const { user, loginWithCredentials, logout: authLogout } = useAuth();
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
const [userRole, setUserRole] = useState<'ROLE_TRANSPORT' | 'ROLE_SUPER_ADMIN' | null>(null);
|
const [userRole, setUserRole] = useState<'ROLE_TRANSPORT' | 'ROLE_COMMUNITY' | 'ROLE_CLUBS' | 'ROLE_CURRICULUM' | 'ROLE_SUPER_ADMIN' | null>(null);
|
||||||
const [usernameDisplay, setUsernameDisplay] = useState('Admin');
|
const [usernameDisplay, setUsernameDisplay] = useState('Admin');
|
||||||
|
|
||||||
// Login State
|
// Login State
|
||||||
@@ -232,27 +232,45 @@ export default function AdminDashboard() {
|
|||||||
setTimeout(() => setToastMessage(null), 4000);
|
setTimeout(() => setToastMessage(null), 4000);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDefaultTabForRole = (role: string | null): 'transport' | 'telegram' | 'notes' | 'community' | 'subscribers' | 'faculty' | 'clubs' | 'curriculum' => {
|
||||||
|
switch (role) {
|
||||||
|
case 'ROLE_TRANSPORT': return 'transport';
|
||||||
|
case 'ROLE_COMMUNITY': return 'telegram';
|
||||||
|
case 'ROLE_CLUBS': return 'clubs';
|
||||||
|
case 'ROLE_CURRICULUM': return 'curriculum';
|
||||||
|
default: return 'transport';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRoleDisplayName = (role: string | null): string => {
|
||||||
|
switch (role) {
|
||||||
|
case 'ROLE_TRANSPORT': return 'Transport Fleet Admin';
|
||||||
|
case 'ROLE_COMMUNITY': return 'Community & Senior Q&A Admin';
|
||||||
|
case 'ROLE_CLUBS': return 'Clubs & Centers Admin';
|
||||||
|
case 'ROLE_CURRICULUM': return 'GPA Curriculum Admin';
|
||||||
|
case 'ROLE_SUPER_ADMIN': return 'Super Admin';
|
||||||
|
default: return 'Admin';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Restore session from AuthContext or localStorage
|
// Restore session from AuthContext or localStorage
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user?.role === 'ROLE_TRANSPORT' || user?.role === 'ROLE_SUPER_ADMIN') {
|
const validRoles = ['ROLE_TRANSPORT', 'ROLE_COMMUNITY', 'ROLE_CLUBS', 'ROLE_CURRICULUM', 'ROLE_SUPER_ADMIN'];
|
||||||
|
if (user?.role && validRoles.includes(user.role)) {
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
setUserRole(user.role);
|
setUserRole(user.role as any);
|
||||||
setUsernameDisplay(user.name);
|
setUsernameDisplay(user.name || getRoleDisplayName(user.role));
|
||||||
if (user.role === 'ROLE_TRANSPORT') {
|
setActiveTab(getDefaultTabForRole(user.role));
|
||||||
setActiveTab('transport');
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const savedRole = localStorage.getItem('RIT_ADMIN_ROLE') as any;
|
const savedRole = localStorage.getItem('RIT_ADMIN_ROLE') as any;
|
||||||
const savedToken = localStorage.getItem('RIT_ADMIN_TOKEN');
|
const savedToken = localStorage.getItem('RIT_ADMIN_TOKEN');
|
||||||
if (savedToken && (savedRole === 'ROLE_TRANSPORT' || savedRole === 'ROLE_SUPER_ADMIN')) {
|
if (savedToken && savedRole && validRoles.includes(savedRole)) {
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
setUserRole(savedRole);
|
setUserRole(savedRole);
|
||||||
setUsernameDisplay(localStorage.getItem('RIT_ADMIN_USER') || (savedRole === 'ROLE_TRANSPORT' ? 'Transport Admin' : 'Super Admin'));
|
setUsernameDisplay(localStorage.getItem('RIT_ADMIN_USER') || getRoleDisplayName(savedRole));
|
||||||
if (savedRole === 'ROLE_TRANSPORT') {
|
setActiveTab(getDefaultTabForRole(savedRole));
|
||||||
setActiveTab('transport');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [user]);
|
}, [user]);
|
||||||
|
|
||||||
@@ -281,13 +299,14 @@ export default function AdminDashboard() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await loginWithCredentials(usernameInput.trim(), passwordInput.trim());
|
const result = await loginWithCredentials(usernameInput.trim(), passwordInput.trim());
|
||||||
if (result.success && (result.role === 'ROLE_TRANSPORT' || result.role === 'ROLE_SUPER_ADMIN')) {
|
const validRoles = ['ROLE_TRANSPORT', 'ROLE_COMMUNITY', 'ROLE_CLUBS', 'ROLE_CURRICULUM', 'ROLE_SUPER_ADMIN'];
|
||||||
|
if (result.success && result.role && validRoles.includes(result.role)) {
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
setUserRole(result.role as any);
|
setUserRole(result.role as any);
|
||||||
setUsernameDisplay(result.role === 'ROLE_TRANSPORT' ? 'Transport Admin' : 'Super Admin');
|
setUsernameDisplay(getRoleDisplayName(result.role));
|
||||||
setActiveTab('transport');
|
setActiveTab(getDefaultTabForRole(result.role));
|
||||||
} else {
|
} else {
|
||||||
setLoginError(result.message || 'Invalid admin credentials. Use Transport or Admin with password RIT@2026.');
|
setLoginError(result.message || 'Invalid admin credentials. Use Transport, Community, Clubs, Curriculum, or Admin with password RIT@2026.');
|
||||||
}
|
}
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setLoginError(err?.message || 'Authentication error.');
|
setLoginError(err?.message || 'Authentication error.');
|
||||||
@@ -1397,30 +1416,60 @@ export default function AdminDashboard() {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Quick Demo Credentials */}
|
{/* Quick Demo Credentials */}
|
||||||
<div className="pt-4 border-t border-slate-200/80 space-y-2">
|
<div className="pt-4 border-t border-slate-200/80 space-y-2.5">
|
||||||
<span className="text-[11px] font-bold text-slate-500 block text-center uppercase tracking-wider">
|
<span className="text-[11px] font-bold text-slate-500 block text-center uppercase tracking-wider">
|
||||||
Quick Role-Based Logins:
|
Quick Role-Based Logins:
|
||||||
</span>
|
</span>
|
||||||
<div className="grid grid-cols-2 gap-2.5">
|
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setUsernameInput('Transport'); setPasswordInput('RIT@2026'); }}
|
onClick={() => { setUsernameInput('Transport'); setPasswordInput('RIT@2026'); }}
|
||||||
className="p-3 rounded-xl bg-slate-50 hover:bg-orange-50/50 border border-slate-200 hover:border-orange-300 text-left transition-all cursor-pointer group"
|
className="p-2.5 rounded-xl bg-slate-50 hover:bg-orange-50/50 border border-slate-200 hover:border-orange-300 text-left transition-all cursor-pointer group"
|
||||||
>
|
>
|
||||||
<p className="text-xs font-bold text-slate-900 group-hover:text-[#FF6B00] flex items-center gap-1">
|
<p className="text-[11px] font-bold text-slate-900 group-hover:text-[#FF6B00] flex items-center gap-1 truncate">
|
||||||
<span>🚌 Transport</span>
|
<span>🚌 Transport</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] text-slate-500 font-medium mt-0.5">Pass: RIT@2026</p>
|
<p className="text-[9px] text-slate-500 font-medium">Pass: RIT@2026</p>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setUsernameInput('Community'); setPasswordInput('RIT@2026'); }}
|
||||||
|
className="p-2.5 rounded-xl bg-slate-50 hover:bg-emerald-50/50 border border-slate-200 hover:border-emerald-300 text-left transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<p className="text-[11px] font-bold text-slate-900 group-hover:text-emerald-600 flex items-center gap-1 truncate">
|
||||||
|
<span>💬 Community & Q&A</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-[9px] text-slate-500 font-medium">Pass: RIT@2026</p>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setUsernameInput('Clubs'); setPasswordInput('RIT@2026'); }}
|
||||||
|
className="p-2.5 rounded-xl bg-slate-50 hover:bg-indigo-50/50 border border-slate-200 hover:border-indigo-300 text-left transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<p className="text-[11px] font-bold text-slate-900 group-hover:text-indigo-600 flex items-center gap-1 truncate">
|
||||||
|
<span>🏢 Clubs & Centers</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-[9px] text-slate-500 font-medium">Pass: RIT@2026</p>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => { setUsernameInput('Curriculum'); setPasswordInput('RIT@2026'); }}
|
||||||
|
className="p-2.5 rounded-xl bg-slate-50 hover:bg-purple-50/50 border border-slate-200 hover:border-purple-300 text-left transition-all cursor-pointer group"
|
||||||
|
>
|
||||||
|
<p className="text-[11px] font-bold text-slate-900 group-hover:text-purple-600 flex items-center gap-1 truncate">
|
||||||
|
<span>🧮 GPA Curriculum</span>
|
||||||
|
</p>
|
||||||
|
<p className="text-[9px] text-slate-500 font-medium">Pass: RIT@2026</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => { setUsernameInput('Admin'); setPasswordInput('RIT@2026'); }}
|
onClick={() => { setUsernameInput('Admin'); setPasswordInput('RIT@2026'); }}
|
||||||
className="p-3 rounded-xl bg-slate-50 hover:bg-blue-50/50 border border-slate-200 hover:border-blue-300 text-left transition-all cursor-pointer group"
|
className="p-2.5 rounded-xl bg-slate-50 hover:bg-blue-50/50 border border-slate-200 hover:border-blue-300 text-left transition-all cursor-pointer group col-span-2 sm:col-span-2"
|
||||||
>
|
>
|
||||||
<p className="text-xs font-bold text-slate-900 group-hover:text-blue-600 flex items-center gap-1">
|
<p className="text-[11px] font-bold text-slate-900 group-hover:text-blue-600 flex items-center gap-1">
|
||||||
<span>⚡ Super Admin</span>
|
<span>⚡ Super Admin (All Access)</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[10px] text-slate-500 font-medium mt-0.5">Pass: RIT@2026</p>
|
<p className="text-[9px] text-slate-500 font-medium">Pass: RIT@2026</p>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1433,7 +1482,70 @@ export default function AdminDashboard() {
|
|||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
// RENDER: AUTHENTICATED ADMIN DASHBOARD
|
// RENDER: AUTHENTICATED ADMIN DASHBOARD
|
||||||
// ─────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
const isSuperAdmin = userRole === 'ROLE_SUPER_ADMIN';
|
||||||
const isTransportOnly = userRole === 'ROLE_TRANSPORT';
|
const isTransportOnly = userRole === 'ROLE_TRANSPORT';
|
||||||
|
const isCommunityOnly = userRole === 'ROLE_COMMUNITY';
|
||||||
|
const isClubsOnly = userRole === 'ROLE_CLUBS';
|
||||||
|
const isCurriculumOnly = userRole === 'ROLE_CURRICULUM';
|
||||||
|
|
||||||
|
const ALL_ADMIN_TABS = [
|
||||||
|
{ id: 'transport', label: 'Bus Fleet & Routes', icon: Bus, roles: ['ROLE_SUPER_ADMIN', 'ROLE_TRANSPORT'] },
|
||||||
|
{ id: 'telegram', label: 'Telegram Q&A Seniors', icon: Bot, roles: ['ROLE_SUPER_ADMIN', 'ROLE_COMMUNITY'] },
|
||||||
|
{ id: 'community', label: 'Community Q&A', icon: MessageCircle, roles: ['ROLE_SUPER_ADMIN', 'ROLE_COMMUNITY'] },
|
||||||
|
{ id: 'faculty', label: 'Faculty Directory', icon: UserCheck, roles: ['ROLE_SUPER_ADMIN'] },
|
||||||
|
{ id: 'clubs', label: 'Clubs & Centers', icon: Building2, roles: ['ROLE_SUPER_ADMIN', 'ROLE_CLUBS'] },
|
||||||
|
{ id: 'curriculum', label: 'GPA Curriculum', icon: Calculator, roles: ['ROLE_SUPER_ADMIN', 'ROLE_CURRICULUM'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
const visibleTabs = ALL_ADMIN_TABS.filter((tab) => !userRole || tab.roles.includes(userRole));
|
||||||
|
|
||||||
|
const getHeaderDetails = () => {
|
||||||
|
switch (userRole) {
|
||||||
|
case 'ROLE_TRANSPORT':
|
||||||
|
return {
|
||||||
|
title: 'Transport Fleet Administration',
|
||||||
|
badge: 'Transport Manager',
|
||||||
|
badgeColor: 'bg-blue-500/20 text-blue-300 border-blue-500/30',
|
||||||
|
desc: 'Manage college bus routes, stop timings, driver live tracking, and route allocation.',
|
||||||
|
icon: Bus,
|
||||||
|
};
|
||||||
|
case 'ROLE_COMMUNITY':
|
||||||
|
return {
|
||||||
|
title: 'Community & Senior Q&A Dispatch Console',
|
||||||
|
badge: 'Community & Q&A Admin',
|
||||||
|
badgeColor: 'bg-emerald-500/20 text-emerald-300 border-emerald-500/30',
|
||||||
|
desc: 'Manage Telegram Q&A senior responders, dispatch alerts, and moderate fresher questions.',
|
||||||
|
icon: Bot,
|
||||||
|
};
|
||||||
|
case 'ROLE_CLUBS':
|
||||||
|
return {
|
||||||
|
title: 'Clubs & Centers Directory Administration',
|
||||||
|
badge: 'Clubs & Centers Admin',
|
||||||
|
badgeColor: 'bg-indigo-500/20 text-indigo-300 border-indigo-500/30',
|
||||||
|
desc: 'Manage college student clubs, Centers of Excellence, coordinators, and member rosters.',
|
||||||
|
icon: Building2,
|
||||||
|
};
|
||||||
|
case 'ROLE_CURRICULUM':
|
||||||
|
return {
|
||||||
|
title: 'GPA Academic Curriculum Management',
|
||||||
|
badge: 'Curriculum Admin',
|
||||||
|
badgeColor: 'bg-purple-500/20 text-purple-300 border-purple-500/30',
|
||||||
|
desc: 'Manage semester course credits, electives, and GPA calculation tables across departments.',
|
||||||
|
icon: Calculator,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
title: 'RIT Closed-Loop Admin Console',
|
||||||
|
badge: 'Super Admin',
|
||||||
|
badgeColor: 'bg-orange-500/20 text-orange-400 border-orange-500/30',
|
||||||
|
desc: 'Full closed-loop control panel for Bus Fleet, Telegram Q&A, Community, Faculty, Clubs, and GPA Curriculum.',
|
||||||
|
icon: ShieldCheck,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const headerInfo = getHeaderDetails();
|
||||||
|
const HeaderIcon = headerInfo.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-[#F8FAFC] text-slate-900 py-6 px-4 sm:px-6 lg:px-8 font-sans">
|
<div className="min-h-screen bg-[#F8FAFC] text-slate-900 py-6 px-4 sm:px-6 lg:px-8 font-sans">
|
||||||
@@ -1458,24 +1570,20 @@ export default function AdminDashboard() {
|
|||||||
<div className="flex items-center gap-3.5">
|
<div className="flex items-center gap-3.5">
|
||||||
<div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-[#FF6B00] to-[#EA580C] p-0.5 shadow-md shadow-orange-500/20 shrink-0">
|
<div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-[#FF6B00] to-[#EA580C] p-0.5 shadow-md shadow-orange-500/20 shrink-0">
|
||||||
<div className="w-full h-full bg-[#0F172A] rounded-[14px] flex items-center justify-center">
|
<div className="w-full h-full bg-[#0F172A] rounded-[14px] flex items-center justify-center">
|
||||||
{isTransportOnly ? <Bus className="w-5.5 h-5.5 text-white" /> : <ShieldCheck className="w-5.5 h-5.5 text-white" />}
|
<HeaderIcon className="w-5.5 h-5.5 text-white" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
<h1 className="text-xl font-bold text-white tracking-tight">
|
<h1 className="text-xl font-bold text-white tracking-tight">
|
||||||
{isTransportOnly ? 'Transport Fleet Administration' : 'RIT Closed-Loop Admin Console'}
|
{headerInfo.title}
|
||||||
</h1>
|
</h1>
|
||||||
<span className={`px-2.5 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wider border ${
|
<span className={`px-2.5 py-0.5 rounded-full text-[10px] font-extrabold uppercase tracking-wider border ${headerInfo.badgeColor}`}>
|
||||||
isTransportOnly ? 'bg-blue-500/20 text-blue-300 border-blue-500/30' : 'bg-orange-500/20 text-orange-400 border-orange-500/30'
|
{headerInfo.badge}
|
||||||
}`}>
|
|
||||||
{isTransportOnly ? 'Transport Manager' : 'Super Admin'}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-slate-300 font-medium mt-0.5">
|
<p className="text-xs text-slate-300 font-medium mt-0.5">
|
||||||
{isTransportOnly
|
{headerInfo.desc}
|
||||||
? 'Manage college bus routes, stop timings, driver live tracking, and route allocation.'
|
|
||||||
: 'Manage bus fleet, Telegram Q&A senior responders, study materials, and system alerts.'}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1498,8 +1606,8 @@ export default function AdminDashboard() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Quick KPI Stats Overview Bar */}
|
{/* Quick KPI Stats Overview Bar */}
|
||||||
{!isTransportOnly && (
|
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-3">
|
{(isSuperAdmin || isTransportOnly) && (
|
||||||
<div className="bg-white border border-slate-200/80 rounded-2xl p-3.5 shadow-sm flex items-center gap-3">
|
<div className="bg-white border border-slate-200/80 rounded-2xl p-3.5 shadow-sm flex items-center gap-3">
|
||||||
<div className="w-9 h-9 rounded-xl bg-orange-50 text-[#FF6B00] border border-orange-100 flex items-center justify-center shrink-0">
|
<div className="w-9 h-9 rounded-xl bg-orange-50 text-[#FF6B00] border border-orange-100 flex items-center justify-center shrink-0">
|
||||||
<Bus className="w-4.5 h-4.5" />
|
<Bus className="w-4.5 h-4.5" />
|
||||||
@@ -1509,29 +1617,35 @@ export default function AdminDashboard() {
|
|||||||
<p className="text-lg font-bold text-[#0F172A]">{routes.length}</p>
|
<p className="text-lg font-bold text-[#0F172A]">{routes.length}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="bg-white border border-slate-200/80 rounded-2xl p-3.5 shadow-sm flex items-center gap-3">
|
{(isSuperAdmin || isCommunityOnly) && (
|
||||||
<div className="w-9 h-9 rounded-xl bg-blue-50 text-blue-600 border border-blue-100 flex items-center justify-center shrink-0">
|
<>
|
||||||
<Bot className="w-4.5 h-4.5" />
|
<div className="bg-white border border-slate-200/80 rounded-2xl p-3.5 shadow-sm flex items-center gap-3">
|
||||||
|
<div className="w-9 h-9 rounded-xl bg-blue-50 text-blue-600 border border-blue-100 flex items-center justify-center shrink-0">
|
||||||
|
<Bot className="w-4.5 h-4.5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Telegram Seniors</p>
|
||||||
|
<p className="text-lg font-bold text-[#0F172A]">
|
||||||
|
{(telegramConfig?.seniorHelpers?.length || telegramConfig?.helper_chat_ids?.length || 0)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Telegram Seniors</p>
|
|
||||||
<p className="text-lg font-bold text-[#0F172A]">
|
|
||||||
{(telegramConfig?.seniorHelpers?.length || telegramConfig?.helper_chat_ids?.length || 0)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white border border-slate-200/80 rounded-2xl p-3.5 shadow-sm flex items-center gap-3">
|
<div className="bg-white border border-slate-200/80 rounded-2xl p-3.5 shadow-sm flex items-center gap-3">
|
||||||
<div className="w-9 h-9 rounded-xl bg-amber-50 text-amber-600 border border-amber-100 flex items-center justify-center shrink-0">
|
<div className="w-9 h-9 rounded-xl bg-amber-50 text-amber-600 border border-amber-100 flex items-center justify-center shrink-0">
|
||||||
<MessageCircle className="w-4.5 h-4.5" />
|
<MessageCircle className="w-4.5 h-4.5" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Q&A Queue</p>
|
||||||
|
<p className="text-lg font-bold text-[#0F172A]">{questions.length}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
</>
|
||||||
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Q&A Queue</p>
|
)}
|
||||||
<p className="text-lg font-bold text-[#0F172A]">{questions.length}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
{isSuperAdmin && (
|
||||||
<div className="bg-white border border-slate-200/80 rounded-2xl p-3.5 shadow-sm flex items-center gap-3">
|
<div className="bg-white border border-slate-200/80 rounded-2xl p-3.5 shadow-sm flex items-center gap-3">
|
||||||
<div className="w-9 h-9 rounded-xl bg-purple-50 text-purple-600 border border-purple-100 flex items-center justify-center shrink-0">
|
<div className="w-9 h-9 rounded-xl bg-purple-50 text-purple-600 border border-purple-100 flex items-center justify-center shrink-0">
|
||||||
<UserCheck className="w-4.5 h-4.5" />
|
<UserCheck className="w-4.5 h-4.5" />
|
||||||
@@ -1541,8 +1655,10 @@ export default function AdminDashboard() {
|
|||||||
<p className="text-lg font-bold text-[#0F172A]">{facultyList.length}</p>
|
<p className="text-lg font-bold text-[#0F172A]">{facultyList.length}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="bg-white border border-slate-200/80 rounded-2xl p-3.5 shadow-sm flex items-center gap-3 col-span-2 sm:col-span-1">
|
{(isSuperAdmin || isClubsOnly) && (
|
||||||
|
<div className="bg-white border border-slate-200/80 rounded-2xl p-3.5 shadow-sm flex items-center gap-3">
|
||||||
<div className="w-9 h-9 rounded-xl bg-emerald-50 text-emerald-600 border border-emerald-100 flex items-center justify-center shrink-0">
|
<div className="w-9 h-9 rounded-xl bg-emerald-50 text-emerald-600 border border-emerald-100 flex items-center justify-center shrink-0">
|
||||||
<Building2 className="w-4.5 h-4.5" />
|
<Building2 className="w-4.5 h-4.5" />
|
||||||
</div>
|
</div>
|
||||||
@@ -1551,20 +1667,30 @@ export default function AdminDashboard() {
|
|||||||
<p className="text-lg font-bold text-[#0F172A]">{clubsList.length}</p>
|
<p className="text-lg font-bold text-[#0F172A]">{clubsList.length}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Tab Navigation: Clean Static Grid (No Scrollbar Bar) */}
|
{(isSuperAdmin || isCurriculumOnly) && (
|
||||||
{!isTransportOnly && (
|
<div className="bg-white border border-slate-200/80 rounded-2xl p-3.5 shadow-sm flex items-center gap-3">
|
||||||
<div className="bg-white border border-slate-200/80 rounded-2xl p-2 shadow-sm grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-2">
|
<div className="w-9 h-9 rounded-xl bg-indigo-50 text-indigo-600 border border-indigo-100 flex items-center justify-center shrink-0">
|
||||||
{[
|
<Calculator className="w-4.5 h-4.5" />
|
||||||
{ id: 'transport', label: 'Bus Fleet & Routes', icon: Bus },
|
</div>
|
||||||
{ id: 'telegram', label: 'Telegram Q&A Seniors', icon: Bot },
|
<div>
|
||||||
{ id: 'community', label: 'Community Q&A', icon: MessageCircle },
|
<p className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Curriculum Depts</p>
|
||||||
{ id: 'faculty', label: 'Faculty Directory', icon: UserCheck },
|
<p className="text-lg font-bold text-[#0F172A]">{DEPARTMENTS.length}</p>
|
||||||
{ id: 'clubs', label: 'Clubs & Centers', icon: Building2 },
|
</div>
|
||||||
{ id: 'curriculum', label: 'GPA Curriculum', icon: Calculator },
|
</div>
|
||||||
].map((tab) => {
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Navigation: Dynamic Grid per Role */}
|
||||||
|
{visibleTabs.length > 1 && (
|
||||||
|
<div className={`bg-white border border-slate-200/80 rounded-2xl p-2 shadow-sm grid gap-2 ${
|
||||||
|
visibleTabs.length === 2 ? 'grid-cols-2' :
|
||||||
|
visibleTabs.length === 3 ? 'grid-cols-3' :
|
||||||
|
visibleTabs.length === 4 ? 'grid-cols-2 sm:grid-cols-4' :
|
||||||
|
'grid-cols-2 sm:grid-cols-3 lg:grid-cols-6'
|
||||||
|
}`}>
|
||||||
|
{visibleTabs.map((tab) => {
|
||||||
const Icon = tab.icon;
|
const Icon = tab.icon;
|
||||||
const isActive = activeTab === tab.id;
|
const isActive = activeTab === tab.id;
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user