feat: dashboard, reset request modal, and student screen updates
This commit is contained in:
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user