UI polishing and layout fixes for students, archives, and documents screens
This commit is contained in:
1 parent
d983c0d2d8
commit
6ded3a45e4
5 files changed
+226
-131
No files matched your search
@@ -14,6 +14,7 @@ class FolderModel {
|
||||
final String? studentLastName;
|
||||
final String? createdByUsername;
|
||||
final int? documentCount;
|
||||
final String? studentStatus;
|
||||
|
||||
// JHS / SHS completion tracking
|
||||
/// 'JHS' or 'SHS' — the tier that applies to this student
|
||||
@@ -44,6 +45,7 @@ class FolderModel {
|
||||
this.studentLastName,
|
||||
this.createdByUsername,
|
||||
this.documentCount,
|
||||
this.studentStatus,
|
||||
this.studentTier,
|
||||
this.jhsTotal = 0,
|
||||
this.jhsCompleted = 0,
|
||||
@@ -94,6 +96,7 @@ class FolderModel {
|
||||
studentLastName: json['last_name'] as String?,
|
||||
createdByUsername: json['created_by_username'] as String?,
|
||||
documentCount: json['document_count'] as int?,
|
||||
studentStatus: json['student_status'] as String?,
|
||||
studentTier: json['student_tier'] as String?,
|
||||
jhsTotal: (json['jhs_total'] as num?)?.toInt() ?? 0,
|
||||
jhsCompleted: (json['jhs_completed'] as num?)?.toInt() ?? 0,
|
||||
|
||||
@@ -1097,7 +1097,7 @@ class _ArchivesScreenState extends ConsumerState<ArchivesScreen>
|
||||
),
|
||||
if (!isMobile) ...[
|
||||
Expanded(
|
||||
child: _buildStudentStatusChip('Archived'),
|
||||
child: _buildStudentStatusChip(folder.studentStatus ?? 'Archived'),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -1202,6 +1202,8 @@ class _ArchivesScreenState extends ConsumerState<ArchivesScreen>
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_buildStudentStatusChip(folder.studentStatus ?? 'Archived'),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${folder.documentCount ?? 0} docs',
|
||||
style: const TextStyle(
|
||||
|
||||
@@ -743,7 +743,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
),
|
||||
),
|
||||
if (defaultTargetPlatform != TargetPlatform.windows &&
|
||||
(_tabController.index == 1 || isFolderOpened) &&
|
||||
_tabController.index != 2 &&
|
||||
!_isMultiSelectMode) ...[
|
||||
const SizedBox(height: 12),
|
||||
FloatingActionButton(
|
||||
@@ -755,19 +755,21 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
),
|
||||
child: const Icon(Icons.print, color: AppColors.primaryGreen),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
FloatingActionButton(
|
||||
heroTag: 'upload_fab',
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => UploadOcrModal(
|
||||
prefilledStudentId:
|
||||
_openedFolderStudentId ?? widget.initialStudentId,
|
||||
if (_tabController.index == 1 || isFolderOpened) ...[
|
||||
const SizedBox(height: 12),
|
||||
FloatingActionButton(
|
||||
heroTag: 'upload_fab',
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => UploadOcrModal(
|
||||
prefilledStudentId:
|
||||
_openedFolderStudentId ?? widget.initialStudentId,
|
||||
),
|
||||
),
|
||||
child: const Icon(Icons.cloud_upload, color: Colors.white),
|
||||
),
|
||||
child: const Icon(Icons.cloud_upload, color: Colors.white),
|
||||
),
|
||||
],
|
||||
]
|
||||
],
|
||||
),
|
||||
@@ -1030,26 +1032,6 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
),
|
||||
],
|
||||
|
||||
// Desktop action buttons (Upload available to all roles)
|
||||
if (!isMobile && _tabController.index != 2) ...[
|
||||
const SizedBox(width: 8),
|
||||
_buildPrintQueueButton(),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
height: 38,
|
||||
width: 120,
|
||||
child: PrimaryButton(
|
||||
label: 'UPLOAD',
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => UploadOcrModal(
|
||||
prefilledStudentId:
|
||||
_openedFolderStudentId ?? widget.initialStudentId,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1073,7 +1055,28 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
if (!_searchFocusNode.hasFocus) ...[
|
||||
// Multi-select toggle (Documents tab, opened folder, or Recycle Bin)
|
||||
if (_tabController.index == 1 || isFolderOpened) ...[
|
||||
_buildMultiSelectToggle(true),
|
||||
_buildMultiSelectToggle(isMobile),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
|
||||
// Desktop action buttons (Upload available to all roles)
|
||||
if (!isMobile && _tabController.index != 2) ...[
|
||||
_buildPrintQueueButton(),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
height: 38,
|
||||
width: 120,
|
||||
child: PrimaryButton(
|
||||
label: 'UPLOAD',
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (_) => UploadOcrModal(
|
||||
prefilledStudentId:
|
||||
_openedFolderStudentId ?? widget.initialStudentId,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
|
||||
@@ -1169,11 +1172,6 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
onSelected: (value) {
|
||||
if (value == 'grid_list') {
|
||||
setState(() => _isGridView = !_isGridView);
|
||||
} else if (value == 'print') {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => const PrintQueueModal(),
|
||||
);
|
||||
} else if (value == 'recycle_bin') {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -1199,24 +1197,6 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
],
|
||||
),
|
||||
),
|
||||
if (!isMobile)
|
||||
const PopupMenuItem(
|
||||
value: 'print',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.print,
|
||||
size: 20,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Print List',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(),
|
||||
const PopupMenuItem(
|
||||
value: 'recycle_bin',
|
||||
@@ -1766,6 +1746,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
flex: 2,
|
||||
child: Text(
|
||||
'Requirement Progress',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
|
||||
@@ -35,12 +35,43 @@ class _DocumentPreviewDialogState extends State<_DocumentPreviewDialog> {
|
||||
bool _imageLoaded = false;
|
||||
String? _token;
|
||||
|
||||
final PdfViewerController _pdfViewerController = PdfViewerController();
|
||||
final TransformationController _imageTransformationController = TransformationController();
|
||||
|
||||
void _zoomImageIn() {
|
||||
final matrix = _imageTransformationController.value.clone();
|
||||
matrix.scale(1.2, 1.2, 1.0);
|
||||
_imageTransformationController.value = matrix;
|
||||
}
|
||||
|
||||
void _zoomImageOut() {
|
||||
final matrix = _imageTransformationController.value.clone();
|
||||
matrix.scale(1 / 1.2, 1 / 1.2, 1.0);
|
||||
_imageTransformationController.value = matrix;
|
||||
}
|
||||
|
||||
void _zoomPdfIn() {
|
||||
_pdfViewerController.zoomLevel = _pdfViewerController.zoomLevel + 0.5;
|
||||
}
|
||||
|
||||
void _zoomPdfOut() {
|
||||
final newZoom = _pdfViewerController.zoomLevel - 0.5;
|
||||
_pdfViewerController.zoomLevel = newZoom < 1.0 ? 1.0 : newZoom;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadToken();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pdfViewerController.dispose();
|
||||
_imageTransformationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadToken() async {
|
||||
final token = await const FlutterSecureStorage().read(key: 'jwt_token');
|
||||
if (mounted) {
|
||||
@@ -216,10 +247,50 @@ class _DocumentPreviewDialogState extends State<_DocumentPreviewDialog> {
|
||||
}
|
||||
|
||||
Widget _buildContent(bool isMobile) {
|
||||
if (_isImage) return _buildImagePreview();
|
||||
if (_isPdf) return _buildPdfInfo(isMobile);
|
||||
if (_isOffice) return _buildOfficeInfo(isMobile);
|
||||
return _buildGenericInfo(isMobile);
|
||||
Widget content;
|
||||
if (_isImage) {
|
||||
content = _buildImagePreview();
|
||||
} else if (_isPdf) {
|
||||
content = _buildPdfInfo(isMobile);
|
||||
} else if (_isOffice) {
|
||||
content = _buildOfficeInfo(isMobile);
|
||||
} else {
|
||||
content = _buildGenericInfo(isMobile);
|
||||
}
|
||||
|
||||
if (_isImage || _isPdf) {
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned.fill(child: content),
|
||||
Positioned(
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FloatingActionButton.small(
|
||||
heroTag: 'zoom_in',
|
||||
backgroundColor: AppColors.surfaceWhite,
|
||||
foregroundColor: AppColors.primaryGreen,
|
||||
onPressed: _isImage ? _zoomImageIn : _zoomPdfIn,
|
||||
child: const Icon(Icons.zoom_in),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FloatingActionButton.small(
|
||||
heroTag: 'zoom_out',
|
||||
backgroundColor: AppColors.surfaceWhite,
|
||||
foregroundColor: AppColors.primaryGreen,
|
||||
onPressed: _isImage ? _zoomImageOut : _zoomPdfOut,
|
||||
child: const Icon(Icons.zoom_out),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
// ── Image preview ─────────────────────────────────────────
|
||||
@@ -233,8 +304,10 @@ class _DocumentPreviewDialogState extends State<_DocumentPreviewDialog> {
|
||||
children: [
|
||||
// Main image
|
||||
InteractiveViewer(
|
||||
transformationController: _imageTransformationController,
|
||||
minScale: 0.5,
|
||||
maxScale: 5.0,
|
||||
constrained: true,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Image.network(
|
||||
@@ -323,6 +396,7 @@ class _DocumentPreviewDialogState extends State<_DocumentPreviewDialog> {
|
||||
color: Colors.grey.shade100,
|
||||
child: SfPdfViewer.network(
|
||||
_fileUrl,
|
||||
controller: _pdfViewerController,
|
||||
canShowScrollHead: false,
|
||||
canShowScrollStatus: false,
|
||||
),
|
||||
|
||||
@@ -108,6 +108,65 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _showBulkActionMenu(bool isDesktop) {
|
||||
if (isDesktop) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('Bulk Actions'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.group_add, color: AppColors.primaryGreen),
|
||||
title: const Text('Bulk Enroll'),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
_showBulkEnrollModal();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.school, color: Colors.blue),
|
||||
title: const Text('Bulk Graduate'),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
_showBulkGraduateConfirm();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (ctx) => SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.group_add, color: AppColors.primaryGreen),
|
||||
title: const Text('Bulk Enroll'),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
_showBulkEnrollModal();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.school, color: Colors.blue),
|
||||
title: const Text('Bulk Graduate'),
|
||||
onTap: () {
|
||||
Navigator.pop(ctx);
|
||||
_showBulkGraduateConfirm();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showBulkEnrollModal() {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -116,14 +175,15 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
onSuccess: () {
|
||||
setState(() {
|
||||
_selectedStudentIds.clear();
|
||||
_showMultiSelect = false;
|
||||
});
|
||||
showSuccessDialog(context, message: 'Students successfully enrolled.');
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showBulkGraduateConfirm() async {
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
@@ -143,13 +203,16 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
try {
|
||||
await ref.read(studentMutationProvider.notifier).bulkGraduate(_selectedStudentIds);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Students successfully graduated.'), backgroundColor: AppColors.success));
|
||||
setState(() => _selectedStudentIds.clear());
|
||||
setState(() {
|
||||
_selectedStudentIds.clear();
|
||||
_showMultiSelect = false;
|
||||
});
|
||||
showSuccessDialog(context, message: 'Students successfully graduated.');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
final errMsg = e.toString().replaceAll('Exception: ', '');
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(errMsg), backgroundColor: AppColors.error));
|
||||
showErrorDialog(context, 'Error', errMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -288,7 +351,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
floatingActionButton: _searchFocusNode.hasFocus ? null : Column(
|
||||
floatingActionButton: !isMobile ? null : Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
@@ -395,15 +458,6 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
// Desktop: show full button in header
|
||||
if (widget.userRole != 'teacher' && isDesktop)
|
||||
SizedBox(
|
||||
width: 180,
|
||||
child: PrimaryButton(
|
||||
label: 'ADD',
|
||||
onPressed: () => _openModal(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
@@ -423,6 +477,29 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
const SizedBox(width: 8),
|
||||
_buildMultiSelectToggle(false),
|
||||
],
|
||||
if (isDesktop) ...[
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () => _openFilterDialog(query, ref.read(academicYearsListProvider), ref.read(sectionsListProvider)),
|
||||
icon: const Icon(Icons.tune_rounded, size: 20),
|
||||
label: const Text('Filter'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryGreen,
|
||||
side: const BorderSide(color: AppColors.primaryGreen),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
if (widget.userRole != 'teacher') ...[
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 100,
|
||||
child: PrimaryButton(
|
||||
label: 'ADD',
|
||||
onPressed: () => _openModal(),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
|
||||
@@ -458,7 +535,8 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
runSpacing: 8,
|
||||
alignment: WrapAlignment.end,
|
||||
children: [
|
||||
PopupMenuButton<String>(
|
||||
GestureDetector(
|
||||
onTap: () => _showBulkActionMenu(isDesktop),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
@@ -476,23 +554,6 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
onSelected: (value) {
|
||||
if (value == 'enroll') {
|
||||
_showBulkEnrollModal();
|
||||
} else if (value == 'graduate') {
|
||||
_showBulkGraduateConfirm();
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'enroll',
|
||||
child: Row(children: [Icon(Icons.group_add, color: AppColors.primaryGreen, size: 18), SizedBox(width: 10), Text('Bulk Enroll', style: TextStyle(fontSize: 14))]),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'graduate',
|
||||
child: Row(children: [Icon(Icons.school, color: Colors.blue, size: 18), SizedBox(width: 10), Text('Bulk Graduate', style: TextStyle(fontSize: 14))]),
|
||||
),
|
||||
],
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
style: OutlinedButton.styleFrom(
|
||||
@@ -524,7 +585,8 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: PopupMenuButton<String>(
|
||||
child: GestureDetector(
|
||||
onTap: () => _showBulkActionMenu(isDesktop),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
|
||||
decoration: BoxDecoration(
|
||||
@@ -542,23 +604,6 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
onSelected: (value) {
|
||||
if (value == 'enroll') {
|
||||
_showBulkEnrollModal();
|
||||
} else if (value == 'graduate') {
|
||||
_showBulkGraduateConfirm();
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
value: 'enroll',
|
||||
child: Row(children: [Icon(Icons.group_add, color: AppColors.primaryGreen, size: 18), SizedBox(width: 10), Text('Bulk Enroll', style: TextStyle(fontSize: 14))]),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
value: 'graduate',
|
||||
child: Row(children: [Icon(Icons.school, color: Colors.blue, size: 18), SizedBox(width: 10), Text('Bulk Graduate', style: TextStyle(fontSize: 14))]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -1168,7 +1213,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
@@ -1181,6 +1226,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
@@ -1599,27 +1645,16 @@ class _BulkEnrollDialogState extends ConsumerState<BulkEnrollDialog> {
|
||||
children: [
|
||||
yearsAsync.when(
|
||||
data: (years) {
|
||||
return DropdownButtonFormField<int>(
|
||||
value: _selectedAcademicYearId,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Academic Year',
|
||||
prefixIcon: Icon(Icons.calendar_today),
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
items: years.map((y) => DropdownMenuItem<int>(value: y.id, child: Text(y.yearRange))).toList(),
|
||||
onChanged: (val) {
|
||||
setState(() {
|
||||
_selectedAcademicYearId = val;
|
||||
_selectedSectionId = null;
|
||||
});
|
||||
},
|
||||
validator: (v) => v == null ? 'Academic year is required.' : null,
|
||||
);
|
||||
if (_selectedAcademicYearId == null && years.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) setState(() => _selectedAcademicYearId = years.first.id);
|
||||
});
|
||||
}
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
loading: () => const Center(child: Padding(padding: EdgeInsets.all(8), child: CircularProgressIndicator())),
|
||||
error: (e, _) => Text('Error: $e'),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (e, _) => const SizedBox.shrink(),
|
||||
),
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
gradeLevelsAsync.when(
|
||||
data: (grades) {
|
||||
return DropdownButtonFormField<int>(
|
||||
|
||||
Reference in new issue
Block a user