feat: dashboard, reset request modal, and student screen updates

This commit is contained in:
ssbg04 committed 2026-07-21 22:57:55 +08:00
1 parent db6a5d5dd4
commit 4aea12840e
5 files changed
+168 -50

No files matched your search

@@ -296,8 +296,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
_buildSetupGuidanceBanner(context),
const SizedBox(height: 32),
],
const Text('Dashboard Overview', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(
'Welcome back, ${user?.firstName ?? 'Admin'}. Here is what is happening today.',
style: TextStyle(fontSize: 16, color: Colors.grey.shade600),
@@ -557,6 +555,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (!_searchFocusNode.hasFocus) ...[
const Text(
'Dashboard Overview',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(width: 16),
],
Expanded(
child: LayoutBuilder(
builder: (context, constraints) {
@@ -38,6 +38,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
final List<int> _selectedStudentIds = [];
bool _showMultiSelect = false;
ProviderSubscription<String>? _tabListener;
String _docStatusSort = '';
// Pending filter state (applied only when user taps "Apply now")
String _pendingSchoolYear = 'All School Years';
@@ -533,7 +534,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
page.total == 0;
return LayoutBuilder(
builder: (ctx, c) => c.maxWidth > 800
? _buildDesktopTable(page.students, noSections: hasNoSections)
? _buildDesktopTable(page.students, query, noSections: hasNoSections)
: _buildMobileCardList(page.students, noSections: hasNoSections),
);
},
@@ -1068,8 +1069,15 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
// ================================================================
// DESKTOP DATA TABLE
// ================================================================
Widget _buildDesktopTable(List<StudentModel> students, {bool noSections = false}) {
if (students.isEmpty) return _buildEmptyState(noSections: noSections);
Widget _buildDesktopTable(List<StudentModel> rawStudents, StudentQueryParams query, {bool noSections = false}) {
if (rawStudents.isEmpty) return _buildEmptyState(noSections: noSections);
List<StudentModel> students = List.from(rawStudents);
if (_docStatusSort == 'asc') {
students.sort((a, b) => a.missingDocumentsCount.compareTo(b.missingDocumentsCount));
} else if (_docStatusSort == 'desc') {
students.sort((a, b) => b.missingDocumentsCount.compareTo(a.missingDocumentsCount));
}
return LayoutBuilder(
builder: (context, constraints) {
@@ -1124,10 +1132,66 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
const DataColumn2(size: ColumnSize.M, label: Text('LRN', style: TextStyle(fontWeight: FontWeight.bold))),
const DataColumn2(size: ColumnSize.L, label: Text('Name', style: TextStyle(fontWeight: FontWeight.bold))),
const DataColumn2(size: ColumnSize.M, label: Text('Grade & Sec.', style: TextStyle(fontWeight: FontWeight.bold))),
const DataColumn2(size: ColumnSize.S, label: Text('4Ps', style: TextStyle(fontWeight: FontWeight.bold))),
const DataColumn2(size: ColumnSize.S, label: Text('Status', style: TextStyle(fontWeight: FontWeight.bold))),
const DataColumn2(size: ColumnSize.M, label: Text('Missing Docs', style: TextStyle(fontWeight: FontWeight.bold))),
const DataColumn2(size: ColumnSize.S, label: Text('Actions', style: TextStyle(fontWeight: FontWeight.bold))),
DataColumn2(
size: ColumnSize.S,
label: Row(
children: [
const Text('4Ps', style: TextStyle(fontWeight: FontWeight.bold)),
PopupMenuButton<String>(
icon: const Icon(Icons.arrow_drop_down, size: 18),
onSelected: (val) {
ref.read(studentQueryProvider.notifier).setIs4Ps(val);
},
itemBuilder: (ctx) => [
CheckedPopupMenuItem(value: '', checked: query.is4Ps == '', child: const Text('All')),
CheckedPopupMenuItem(value: 'true', checked: query.is4Ps == 'true', child: const Text('Yes')),
CheckedPopupMenuItem(value: 'false', checked: query.is4Ps == 'false', child: const Text('No')),
],
)
],
),
),
DataColumn2(
size: ColumnSize.S,
label: Row(
children: [
const Text('Status', style: TextStyle(fontWeight: FontWeight.bold)),
PopupMenuButton<String>(
icon: const Icon(Icons.arrow_drop_down, size: 18),
onSelected: (val) {
ref.read(studentQueryProvider.notifier).setStatus(val);
},
itemBuilder: (ctx) => [
CheckedPopupMenuItem(value: '', checked: query.status == '', child: const Text('All Status')),
CheckedPopupMenuItem(value: 'Enrolled', checked: query.status == 'Enrolled', child: const Text('Enrolled')),
CheckedPopupMenuItem(value: 'Graduated', checked: query.status == 'Graduated', child: const Text('Graduated')),
CheckedPopupMenuItem(value: 'Transferred', checked: query.status == 'Transferred', child: const Text('Transferred')),
CheckedPopupMenuItem(value: 'Dropped', checked: query.status == 'Dropped', child: const Text('Dropped')),
],
)
],
),
),
DataColumn2(
size: ColumnSize.M,
label: Row(
children: [
const Text('Doc Status', style: TextStyle(fontWeight: FontWeight.bold)),
PopupMenuButton<String>(
icon: const Icon(Icons.arrow_drop_down, size: 18),
onSelected: (val) {
setState(() => _docStatusSort = val);
},
itemBuilder: (ctx) => [
CheckedPopupMenuItem(value: '', checked: _docStatusSort == '', child: const Text('None')),
CheckedPopupMenuItem(value: 'asc', checked: _docStatusSort == 'asc', child: const Text('Least-Greatest')),
CheckedPopupMenuItem(value: 'desc', checked: _docStatusSort == 'desc', child: const Text('Greatest-Least')),
],
)
],
),
),
const DataColumn2(size: ColumnSize.S, label: Text('Action', style: TextStyle(fontWeight: FontWeight.bold))),
],
rows: students.map((student) {
final isSelected = _selectedStudentIds.contains(student.id);
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../../../core/constants/app_colors.dart';
import '../../providers/auth_provider.dart';
@@ -30,12 +31,65 @@ class _ResetRequestsModalState extends ConsumerState<ResetRequestsModal> {
});
}
String _formatDate(String isoString) {
try {
final dt = DateTime.parse(isoString).toLocal();
return DateFormat('MMM d, yyyy, hh:mm a').format(dt);
} catch (_) {
return isoString;
}
}
Future<void> _confirmAction(BuildContext context, Map<String, dynamic> req, {required bool isApprove}) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(isApprove ? 'Approve Request' : 'Reject Request'),
content: Text('Are you sure you want to ${isApprove ? 'approve' : 'reject'} the password reset request for @${req['username']}?'),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: const Text('CANCEL'),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: TextButton.styleFrom(foregroundColor: isApprove ? AppColors.success : Colors.red),
child: const Text('CONFIRM'),
),
],
),
);
if (confirmed == true && mounted) {
try {
final repo = ref.read(authRepositoryProvider);
if (isApprove) {
await repo.approveResetRequest(req['id'] as int);
} else {
await repo.rejectResetRequest(req['id'] as int);
}
ref.invalidate(resetRequestsProvider);
if (mounted) {
showSuccessDialog(
context,
title: isApprove ? 'Approved' : 'Rejected',
message: isApprove ? 'Password reset approved.' : 'Request rejected.',
);
}
} catch (e) {
if (mounted) {
showErrorDialog(context, isApprove ? 'Approval Failed' : 'Rejection Failed', e.toString());
}
}
}
}
@override
Widget build(BuildContext context) {
final requestsAsync = ref.watch(resetRequestsProvider);
return CustomModal(
title: 'Password Reset Requests',
title: 'Reset Requests',
icon: Icons.lock_clock,
maxWidth: 500,
content: requestsAsync.when(
@@ -56,44 +110,38 @@ class _ResetRequestsModalState extends ConsumerState<ResetRequestsModal> {
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) {
final req = requests[index];
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
title: Text('${req['first_name']} ${req['last_name']} (@${req['username']})'),
subtitle: Text('Role: ${(req['role'] as String).toUpperCase().replaceAll('_', ' ')} • Requested: ${(req['requested_at'] as String).split('T').first}'),
trailing: Row(
mainAxisSize: MainAxisSize.min,
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextButton.icon(
icon: const Icon(Icons.check_circle, color: AppColors.success, size: 18),
label: const Text('Approve', style: TextStyle(color: AppColors.success)),
onPressed: () async {
try {
final repo = ref.read(authRepositoryProvider);
await repo.approveResetRequest(req['id'] as int);
ref.invalidate(resetRequestsProvider);
if (!context.mounted) return;
showSuccessDialog(context, title: 'Approved', message: 'Password reset approved.');
} catch (e) {
if (!context.mounted) return;
showErrorDialog(context, 'Approval Failed', e.toString());
}
},
),
TextButton.icon(
icon: const Icon(Icons.cancel, color: Colors.red, size: 18),
label: const Text('Reject', style: TextStyle(color: Colors.red)),
onPressed: () async {
try {
final repo = ref.read(authRepositoryProvider);
await repo.rejectResetRequest(req['id'] as int);
ref.invalidate(resetRequestsProvider);
if (!context.mounted) return;
showSuccessDialog(context, title: 'Rejected', message: 'Request rejected.');
} catch (e) {
if (!context.mounted) return;
showErrorDialog(context, 'Rejection Failed', e.toString());
}
},
Text('${req['first_name']} ${req['last_name']} (@${req['username']})',
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
const SizedBox(height: 4),
Text('Requested: ${_formatDate(req['requested_at'] as String)}',
style: TextStyle(color: Colors.grey.shade600, fontSize: 14)),
const SizedBox(height: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
TextButton(
style: TextButton.styleFrom(
foregroundColor: AppColors.success,
backgroundColor: AppColors.success.withValues(alpha: 0.1),
),
child: const Text('Approve'),
onPressed: () => _confirmAction(context, req, isApprove: true),
),
const SizedBox(height: 8),
TextButton(
style: TextButton.styleFrom(
foregroundColor: Colors.red,
backgroundColor: Colors.red.withValues(alpha: 0.1),
),
child: const Text('Reject'),
onPressed: () => _confirmAction(context, req, isApprove: false),
),
],
),
],
),
+3 -3
View File
@@ -593,13 +593,13 @@ packages:
source: hosted
version: "0.2.2"
intl:
dependency: transitive
dependency: "direct main"
description:
name: intl
sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5"
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
url: "https://pub.dev"
source: hosted
version: "0.20.2"
version: "0.20.3"
io:
dependency: transitive
description:
+1
View File
@@ -58,6 +58,7 @@ dependencies:
wolt_modal_sheet: ^0.11.0
printing: ^5.14.3
pdf: ^3.12.0
intl: ^0.20.3
dev_dependencies:
flutter_test: