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();