diff --git a/backend/src/controllers/authController.js b/backend/src/controllers/authController.js index 9f7d2f2..c258c2d 100644 --- a/backend/src/controllers/authController.js +++ b/backend/src/controllers/authController.js @@ -173,6 +173,19 @@ exports.login = (req, res) => { `).run(user.id, user.username, fullName, user.role, clientPlatform, clientIp); } catch (_) {} + // Close any open sessions from the same IP belonging to a different user. + // This prevents the History screen from showing stale "ACTIVE NOW" entries + // when a different account logs in from the same device / IP address. + if (clientIp) { + try { + db.prepare(` + UPDATE user_login_logs + SET logout_at = (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) + WHERE ip_address = ? AND user_id != ? AND logout_at IS NULL + `).run(clientIp, user.id); + } catch (_) {} + } + res.json({ token, user: { @@ -997,10 +1010,6 @@ exports.selfDeactivateAccount = async (req, res) => { const user = db.prepare('SELECT id, username, first_name, last_name, email, role, is_active, is_hidden FROM users WHERE id = ?').get(userId); if (!user) return res.status(404).json({ message: 'User account not found' }); - if (user.is_hidden === 1) { - return res.status(403).json({ message: 'Developer super administrator account cannot be deactivated.' }); - } - if (user.role === 'admin') { const otherActiveAdmins = db.prepare( "SELECT COUNT(*) as count FROM users WHERE role = 'admin' AND is_active = 1 AND (is_hidden = 0 OR is_hidden IS NULL) AND id != ?" @@ -1065,9 +1074,6 @@ exports.requestAccountDeletion = async (req, res) => { const user = db.prepare('SELECT id, username, first_name, last_name, email, role, is_hidden FROM users WHERE id = ?').get(userId); if (!user) return res.status(404).json({ message: 'User account not found' }); - if (user.is_hidden === 1) { - return res.status(403).json({ message: 'Developer super administrator account cannot be deleted.' }); - } if (user.role === 'admin') { const otherAdmins = db.prepare("SELECT COUNT(*) as count FROM users WHERE role = 'admin' AND (is_hidden = 0 OR is_hidden IS NULL) AND id != ?").get(user.id); @@ -1326,15 +1332,6 @@ exports.confirmDeleteAccount = (req, res) => { return res.status(410).json({ message: msg }); } - if (record.is_hidden === 1) { - const msg = 'Developer super administrator account cannot be deleted.'; - if (isHtml) return res.status(403).send(renderDeletionPage({ - title: 'Action Prohibited', - isError: true, - contentHtml: `
Action Prohibited: ${msg}
`, - })); - return res.status(403).json({ message: msg }); - } const fullName = [record.first_name, record.middle_name, record.last_name].filter(Boolean).join(' '); diff --git a/backend/src/controllers/userController.js b/backend/src/controllers/userController.js index 6cd2a31..bd9e9b9 100644 --- a/backend/src/controllers/userController.js +++ b/backend/src/controllers/userController.js @@ -223,9 +223,6 @@ exports.updateUser = async (req, res) => { try { const user = db.prepare('SELECT id, username, role, first_name, middle_name, last_name, extension, email, phone, is_hidden FROM users WHERE id = ?').get(id); if (!user) return res.status(404).json({ message: 'User not found.' }); - if (user.is_hidden === 1 && req.user.id !== user.id) { - return res.status(404).json({ message: 'User not found.' }); - } // Validate email with MyEmailVerifier if changed and not empty if (email !== undefined && email !== null && email.trim() !== '' && email.trim().toLowerCase() !== (user.email || '').trim().toLowerCase()) { @@ -296,9 +293,6 @@ exports.resetPassword = async (req, res) => { const user = db.prepare('SELECT id, username, first_name, last_name, role, email, is_hidden FROM users WHERE id = ?').get(id); if (!user) return res.status(404).json({ message: 'User not found.' }); - if (user.is_hidden === 1 && req.user.id !== user.id) { - return res.status(404).json({ message: 'User not found.' }); - } if (user.id === req.user.id) { return res.status(403).json({ message: 'Cannot reset your own password via this route. Use the Change Password profile setting.' }); @@ -414,7 +408,6 @@ exports.deleteUser = (req, res) => { const user = db.prepare('SELECT id, username, first_name, middle_name, last_name, role, is_hidden FROM users WHERE id = ?').get(id); if (!user) return res.status(404).json({ message: 'User not found.' }); - if (user.is_hidden === 1) return res.status(403).json({ message: 'Cannot delete developer super administrator account.' }); if (user.id === adminId) return res.status(403).json({ message: 'Cannot delete your own account.' }); if (user.role === 'admin') { @@ -456,9 +449,6 @@ exports.toggleUserStatus = (req, res) => { try { const user = db.prepare('SELECT id, username, first_name, last_name, role, email, is_active, is_hidden FROM users WHERE id = ?').get(id); if (!user) return res.status(404).json({ message: 'User not found.' }); - if (user.is_hidden === 1) { - return res.status(403).json({ message: 'Cannot modify developer super administrator account.' }); - } const newStatus = user.is_active === 1 ? 0 : 1; db.prepare("UPDATE users SET is_active = ?, updated_at = (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) WHERE id = ?") diff --git a/backend/src/models/schema.js b/backend/src/models/schema.js index bcd1e5d..3d4c78b 100644 --- a/backend/src/models/schema.js +++ b/backend/src/models/schema.js @@ -955,17 +955,49 @@ const initSchema = () => { console.log('Default Admin created: admin / admin123'); } - // Seed Hidden Developer Super Admin if not exists - const devAdminUser = db.prepare("SELECT * FROM users WHERE username = 'developer'").get(); - if (!devAdminUser) { - const hashedDevPassword = bcrypt.hashSync('Developer@2026!', 10); - db.prepare(` - INSERT INTO users (username, password, first_name, last_name, role, email, is_hidden) - VALUES ('developer', ?, 'Developer', 'Super Admin', 'admin', 'dev@tis.edu.ph', 1) - `).run(hashedDevPassword); - console.log('Hidden Developer Super Admin created: developer / Developer@2026!'); - } else if (devAdminUser.is_hidden !== 1) { - db.prepare("UPDATE users SET is_hidden = 1 WHERE username = 'developer'").run(); + // Remove legacy hidden developer account if it still exists in the database + const devAdminUser = db.prepare("SELECT id FROM users WHERE username = 'developer'").get(); + if (devAdminUser) { + const fallbackAdmin = db.prepare("SELECT id FROM users WHERE role = 'admin' AND username != 'developer' ORDER BY id ASC LIMIT 1").get(); + const fallbackAdminId = fallbackAdmin ? fallbackAdmin.id : null; + + if (fallbackAdminId) { + try { db.prepare("UPDATE documents SET uploaded_by = ? WHERE uploaded_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE document_versions SET uploaded_by = ? WHERE uploaded_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE template_versions SET uploaded_by = ? WHERE uploaded_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE document_templates SET created_by = ? WHERE created_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE document_folders SET created_by = ? WHERE created_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE activity_log SET user_id = ? WHERE user_id = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE user_history SET performed_by = ? WHERE performed_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE printed_document_history SET user_id = ? WHERE user_id = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE recent_deleted SET deleted_by = ? WHERE deleted_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE deleted_users_history SET deleted_by = ? WHERE deleted_by = ?").run(fallbackAdminId, devAdminUser.id); } catch (_) {} + } else { + try { db.prepare("UPDATE documents SET uploaded_by = NULL WHERE uploaded_by = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE document_versions SET uploaded_by = NULL WHERE uploaded_by = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE template_versions SET uploaded_by = NULL WHERE uploaded_by = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE document_templates SET created_by = NULL WHERE created_by = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE document_folders SET created_by = NULL WHERE created_by = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE activity_log SET user_id = NULL WHERE user_id = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE user_history SET performed_by = NULL WHERE performed_by = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE printed_document_history SET user_id = NULL WHERE user_id = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE recent_deleted SET deleted_by = NULL WHERE deleted_by = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("UPDATE deleted_users_history SET deleted_by = NULL WHERE deleted_by = ?").run(devAdminUser.id); } catch (_) {} + } + + try { db.prepare("DELETE FROM user_sessions WHERE user_id = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("DELETE FROM user_login_logs WHERE user_id = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("DELETE FROM fcm_tokens WHERE user_id = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("DELETE FROM notifications WHERE user_id = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("DELETE FROM password_reset_requests WHERE user_id = ? OR reviewed_by = ?").run(devAdminUser.id, devAdminUser.id); } catch (_) {} + try { db.prepare("DELETE FROM password_reset_otps WHERE user_id = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("DELETE FROM password_reset_links WHERE user_id = ? OR admin_id = ?").run(devAdminUser.id, devAdminUser.id); } catch (_) {} + try { db.prepare("DELETE FROM account_deletion_requests WHERE user_id = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("DELETE FROM print_queue WHERE user_id = ?").run(devAdminUser.id); } catch (_) {} + try { db.prepare("DELETE FROM teacher_sections WHERE teacher_id = ?").run(devAdminUser.id); } catch (_) {} + + db.prepare("DELETE FROM users WHERE id = ?").run(devAdminUser.id); + console.log('Removed legacy developer account from database.'); } // Seed default document requirements if none exist diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index d2a0129..fec584c 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -900,6 +900,31 @@ Files backup/restore - mirror mode or zip --> - "Parse & Validate", "Clear", and "Import [X] Valid Students" (disabled if 0 valid records or section unselected) - Post-import summary dialog showing successfully created count vs skipped duplicates - Auto uploads OCR file scanned after adding -> rename the scanned file of what type is the document --> + +# HISTORY SCREEN +- [DONE] history -> session one ip different account shows active even 1 current login account + - Backend fix in authController.js login(): when a new login occurs, any open sessions from the same IP address belonging to a different user are automatically stamped with logout_at. This prevents stale "ACTIVE NOW" entries in the Sessions history tab. + +# USERS SCREEN +- [DONE] remove super admin account + - Frontend fix in users_screen.dart _filter(): added a guard to exclude any user whose role is not 'admin' or 'teacher' (e.g. legacy super_admin accounts) from all views. Backend already filters is_hidden users via WHERE COALESCE(u.is_hidden, 0) = 0. +- [DONE] remove the developer account + - Removed developer seeding in schema.js and replaced with auto-migration to delete any existing `developer` user record. + - Removed `is_hidden` protection guards blocking deletion/status changes in userController.js and authController.js. + - Removed developer exemption in frontend security_section.dart so danger zone card is consistently presented. + +# STUDENT SCREEN +- [DONE] student screen tab current year/enrolled students, and all students + - Added _StudentViewTab enum and a segmented tab switcher control below the header. + - "Enrolled · " tab auto-applies status=Enrolled + active academic year filter. + - "All Students" tab clears those filters and shows all student records. + - Tab resets to Enrolled on each navigation away/back to Students screen. + +# STUDENT SCREEN +- loads accurate active year students +- when searching navigates to all tab + + --- diff --git a/frontend/lib/ui/screens/settings/widgets/security_section.dart b/frontend/lib/ui/screens/settings/widgets/security_section.dart index 9ab6aff..cc21568 100644 --- a/frontend/lib/ui/screens/settings/widgets/security_section.dart +++ b/frontend/lib/ui/screens/settings/widgets/security_section.dart @@ -134,72 +134,70 @@ class _SecuritySectionState extends ConsumerState { ), ), - if (widget.user.username.toLowerCase() != 'developer') ...[ - const SizedBox(height: AppSizes.p24), - // ── Danger Zone / Deactivate Account Card ─────────── - widget.buildDangerCard( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: AppColors.error.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon(Icons.person_off_outlined, color: AppColors.error, size: 22), + const SizedBox(height: AppSizes.p24), + // ── Danger Zone / Deactivate Account Card ─────────── + widget.buildDangerCard( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: AppColors.error.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), ), - const SizedBox(width: AppSizes.p12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'Danger Zone: Deactivate Account', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: AppColors.error, - ), - ), - const SizedBox(height: 4), - Text( - 'Temporarily deactivate your account. Your data is preserved and an administrator can reactivate it later.', - style: TextStyle( - fontSize: 13, - color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - Align( - alignment: Alignment.centerRight, - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - foregroundColor: AppColors.error, - side: const BorderSide(color: AppColors.error), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - ), - icon: const Icon(Icons.person_off_outlined, size: 18), - label: const Text( - 'Deactivate Account', - style: TextStyle(fontWeight: FontWeight.w600), - ), - onPressed: () => widget.onDeactivateAccount(context, ref), + child: const Icon(Icons.person_off_outlined, color: AppColors.error, size: 22), ), + const SizedBox(width: AppSizes.p12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Danger Zone: Deactivate Account', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: AppColors.error, + ), + ), + const SizedBox(height: 4), + Text( + 'Temporarily deactivate your account. Your data is preserved and an administrator can reactivate it later.', + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.7), + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + Align( + alignment: Alignment.centerRight, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.error, + side: const BorderSide(color: AppColors.error), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + icon: const Icon(Icons.person_off_outlined, size: 18), + label: const Text( + 'Deactivate Account', + style: TextStyle(fontWeight: FontWeight.w600), + ), + onPressed: () => widget.onDeactivateAccount(context, ref), ), - ], - ), + ), + ], ), - ], + ), ], ); } diff --git a/frontend/lib/ui/screens/students/students_screen.dart b/frontend/lib/ui/screens/students/students_screen.dart index fd362d8..d126dd7 100644 --- a/frontend/lib/ui/screens/students/students_screen.dart +++ b/frontend/lib/ui/screens/students/students_screen.dart @@ -29,6 +29,8 @@ import '../../shared/dialogs/success_dialog.dart'; import '../../shared/widgets/app_pagination.dart'; import '../../shared/widgets/app_error_state.dart'; +enum _StudentViewTab { enrolled, all } + class StudentsScreen extends ConsumerStatefulWidget { final String userRole; const StudentsScreen({super.key, this.userRole = 'teacher'}); @@ -50,6 +52,9 @@ class _StudentsScreenState extends ConsumerState { Timer? _dragResetTimer; ProviderSubscription? _tabListener; + /// Whether the screen is showing enrolled-only (current year) or all students. + _StudentViewTab _viewTab = _StudentViewTab.enrolled; + final ScrollController _scrollController = ScrollController(); bool _showTopFade = false; bool _showBottomFade = false; @@ -90,6 +95,9 @@ class _StudentsScreenState extends ConsumerState { final initialQuery = ref.read(studentQueryProvider).search; if (initialQuery.isNotEmpty) { _searchController.text = initialQuery; + } else { + // Apply the default enrolled-tab filter on first load + _applyViewTab(_StudentViewTab.enrolled); } if (ref.read(activeTabProvider) == 'Students') { _shortcutFocusNode.requestFocus(); @@ -102,6 +110,8 @@ class _StudentsScreenState extends ConsumerState { if (next != 'Students') { if (_searchController.text.isNotEmpty) _searchController.clear(); ref.read(studentQueryProvider.notifier).reset(); + // Reset view tab to enrolled for next visit + if (mounted) setState(() => _viewTab = _StudentViewTab.enrolled); if (_showMultiSelect || _selectedStudentIds.isNotEmpty) { _updateSelection(() { _showMultiSelect = false; @@ -125,6 +135,10 @@ class _StudentsScreenState extends ConsumerState { _searchController.text = currentQuery; if (mounted) setState(() {}); } + // Re-apply the enrolled filter when returning to this tab + if (mounted) { + _applyViewTab(_viewTab); + } } }); }); @@ -154,6 +168,38 @@ class _StudentsScreenState extends ConsumerState { }); } + // ---------------------------------------------------------------- + // VIEW TAB: Enrolled (current year) vs All Students + // ---------------------------------------------------------------- + void _applyViewTab(_StudentViewTab tab) { + if (!mounted) return; + setState(() => _viewTab = tab); + + if (tab == _StudentViewTab.enrolled) { + // Find the currently active academic year + final years = ref.read(academicYearsListProvider).asData?.value ?? []; + final active = years.firstWhere( + (y) => y.status.toLowerCase() == 'active', + orElse: () => years.isNotEmpty ? years.first : AcademicYearModel(id: 0, yearRange: '', status: ''), + ); + // Apply enrolled status + current school year + ref.read(studentQueryProvider.notifier).setFilters( + status: 'Enrolled', + schoolYear: active.yearRange.isNotEmpty ? active.yearRange : '', + gradeLevel: '', + section: '', + ); + } else { + // All students: clear status and school year filters + ref.read(studentQueryProvider.notifier).setFilters( + status: '', + schoolYear: '', + gradeLevel: '', + section: '', + ); + } + } + void _onSearchSubmitted(String query) { _debounce?.cancel(); ref.read(studentQueryProvider.notifier).setSearch(query); @@ -1027,7 +1073,16 @@ class _StudentsScreenState extends ConsumerState { activeCount, ), ), - const SizedBox(height: AppSizes.p24), + // ── View Tab Switcher ── + Padding( + padding: const EdgeInsets.only( + left: AppSizes.p24, + right: AppSizes.p24, + top: AppSizes.p12, + ), + child: _buildViewTabSwitcher(context), + ), + const SizedBox(height: AppSizes.p12), ], // ── Data Table / Cards ── @@ -1376,6 +1431,144 @@ class _StudentsScreenState extends ConsumerState { } + // ================================================================ + // VIEW TAB SWITCHER (Enrolled/Current Year vs All Students) + // ================================================================ + Widget _buildViewTabSwitcher(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final pageTotal = ref.watch(studentPageProvider).asData?.value.total; + + // Resolve academic year label for the enrolled tab + final years = ref.watch(academicYearsListProvider).asData?.value ?? []; + AcademicYearModel? activeYear; + try { + activeYear = years.firstWhere((y) => y.status.toLowerCase() == 'active'); + } catch (_) { + activeYear = years.isNotEmpty ? years.first : null; + } + final yearLabel = activeYear != null ? ' · ${activeYear.yearRange}' : ''; + + return LayoutBuilder( + builder: (_, c) { + final isWide = c.maxWidth > 500; + return Container( + height: 38, + decoration: BoxDecoration( + color: isDark ? AppColors.darkSurface2 : Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: isDark ? AppColors.darkBorder : Colors.grey.shade200, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + _buildTabChip( + label: isWide + ? 'Enrolled$yearLabel' + : 'Enrolled', + icon: Icons.school_rounded, + active: _viewTab == _StudentViewTab.enrolled, + count: _viewTab == _StudentViewTab.enrolled ? pageTotal : null, + isDark: isDark, + onTap: () => _applyViewTab(_StudentViewTab.enrolled), + ), + _buildTabChip( + label: 'All Students', + icon: Icons.people_alt_rounded, + active: _viewTab == _StudentViewTab.all, + count: _viewTab == _StudentViewTab.all ? pageTotal : null, + isDark: isDark, + onTap: () => _applyViewTab(_StudentViewTab.all), + ), + ], + ), + ); + }, + ); + } + + Widget _buildTabChip({ + required String label, + required IconData icon, + required bool active, + required bool isDark, + required VoidCallback onTap, + int? count, + }) { + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + margin: const EdgeInsets.all(3), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), + decoration: BoxDecoration( + color: active + ? (isDark ? AppColors.primaryGreen.withValues(alpha: 0.22) : AppColors.primaryGreen) + : Colors.transparent, + borderRadius: BorderRadius.circular(7), + boxShadow: active && !isDark + ? [ + BoxShadow( + color: AppColors.primaryGreen.withValues(alpha: 0.25), + blurRadius: 4, + offset: const Offset(0, 1), + ), + ] + : null, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + icon, + size: 14, + color: active + ? (isDark ? AppColors.primaryGreen : Colors.white) + : (isDark ? AppColors.darkTextSecondary : AppColors.textSecondary), + ), + const SizedBox(width: 5), + Text( + label, + style: TextStyle( + fontSize: 12.5, + fontWeight: active ? FontWeight.w700 : FontWeight.w500, + color: active + ? (isDark ? AppColors.primaryGreen : Colors.white) + : (isDark ? AppColors.darkTextSecondary : AppColors.textSecondary), + ), + ), + if (count != null) ...[ + const SizedBox(width: 5), + Container( + padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1), + decoration: BoxDecoration( + color: active + ? (isDark + ? AppColors.primaryGreen.withValues(alpha: 0.35) + : Colors.white.withValues(alpha: 0.3)) + : (isDark ? AppColors.darkBorder : Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '$count', + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.w700, + color: active + ? (isDark ? AppColors.primaryGreen : Colors.white) + : (isDark ? AppColors.darkTextSecondary : AppColors.textSecondary), + ), + ), + ), + ], + ], + ), + ), + ); + } + + List _sortStudents( List rawStudents, StudentQueryParams query, diff --git a/frontend/lib/ui/screens/users/users_screen.dart b/frontend/lib/ui/screens/users/users_screen.dart index a2dfb72..df28df9 100644 --- a/frontend/lib/ui/screens/users/users_screen.dart +++ b/frontend/lib/ui/screens/users/users_screen.dart @@ -125,6 +125,10 @@ class _UsersScreenState extends ConsumerState { List _filter(List users, String query) { var result = users; + // Exclude any non-standard roles (e.g. legacy super_admin accounts) from all views. + // The backend already filters is_hidden users, but this guards against stale role values. + result = result.where((u) => u.role == 'admin' || u.role == 'teacher').toList(); + // Apply status and role filter if (_roleFilter == 'inactive') { result = result.where((u) => !u.isActive).toList();