feat(documents): implement Priority 3 status-driven workflow and requirements tab
This commit is contained in:
1 parent
ced3aa1f09
commit
f37828b534
7 files changed
+1113
-80
No files matched your search
+2
-2
@@ -1065,8 +1065,8 @@ Files backup/restore - mirror mode or zip -->
|
||||
- [x] Implement mobile bottom sheet for filtering (grade, section, status, school year) with active filter chips.
|
||||
|
||||
## Priority 3: Status-Driven Documents & Requirements Workflow
|
||||
- [ ] Integrate document requirements directly into Documents module workflow (All Documents, Requirements, Upload/Import, Print Queue).
|
||||
- [ ] Add status system badges (✓ Complete, ⚠ Pending, ! Missing, ○ Archived) across document lists.
|
||||
- [x] Integrate document requirements directly into Documents module workflow (All Documents, Requirements, Upload/Import, Print Queue).
|
||||
- [x] Add status system badges (✓ Complete, ⚠ Pending, ! Missing, ○ Archived) across document lists.
|
||||
|
||||
## Priority 4: Action-Oriented Dashboard
|
||||
- [ ] Add "Needs Attention" operational card (missing SF9/SF10, documents requiring verification).
|
||||
|
||||
@@ -280,6 +280,28 @@ final studentPageProvider = FutureProvider.autoDispose<StudentPage>((
|
||||
);
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// Enrolled students for Requirements module (excludes non-enrolled/archived)
|
||||
// ============================================================
|
||||
final enrolledStudentsForRequirementsProvider = FutureProvider.autoDispose<List<StudentModel>>((
|
||||
ref,
|
||||
) async {
|
||||
final repo = ref.read(studentRepositoryProvider);
|
||||
|
||||
final sub = repo.onStudentChanged.listen((_) {
|
||||
ref.invalidateSelf();
|
||||
});
|
||||
|
||||
ref.onDispose(() => sub.cancel());
|
||||
|
||||
final page = await repo.getStudents(
|
||||
status: 'Enrolled',
|
||||
limit: 1000,
|
||||
);
|
||||
return page.students;
|
||||
});
|
||||
|
||||
|
||||
// ============================================================
|
||||
// Mutation notifier — handles Create / Update / Delete
|
||||
// ============================================================
|
||||
|
||||
@@ -35,6 +35,7 @@ import 'widgets/bulk_operations_bar.dart';
|
||||
import 'widgets/styled_folder_icon.dart';
|
||||
import '../../../domain/entities/document_model.dart';
|
||||
import 'widgets/folder_filter_dialog.dart';
|
||||
import 'widgets/document_requirements_view.dart';
|
||||
|
||||
class DocumentsScreen extends ConsumerStatefulWidget {
|
||||
final String userRole;
|
||||
@@ -61,6 +62,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
Timer? _debounce;
|
||||
bool _isGridView = false;
|
||||
late final TabController _tabController;
|
||||
bool _skipFilterClearOnNextTabChange = false;
|
||||
|
||||
// --- Windows Explorer State Variables ---
|
||||
int? _openedFolderStudentId;
|
||||
@@ -91,8 +93,8 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 2 tabs: 0=Folders, 1=Documents
|
||||
_tabController = TabController(length: 2, vsync: this);
|
||||
// 3 tabs: 0=Folders, 1=Documents, 2=Requirements
|
||||
_tabController = TabController(length: 3, vsync: this);
|
||||
_searchFocusNode.addListener(_onSearchFocusChanged);
|
||||
|
||||
_pollingTimer = Timer.periodic(const Duration(seconds: 15), (_) {
|
||||
@@ -104,6 +106,8 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
ref.invalidate(studentFoldersProvider);
|
||||
} else if (_tabController.index == 1) {
|
||||
ref.invalidate(documentPageProvider);
|
||||
} else if (_tabController.index == 2) {
|
||||
ref.invalidate(requirementsSettingsProvider);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -156,13 +160,20 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
_selectedDocumentIds.clear();
|
||||
});
|
||||
|
||||
_clearFilters();
|
||||
if (_skipFilterClearOnNextTabChange) {
|
||||
_skipFilterClearOnNextTabChange = false;
|
||||
} else {
|
||||
_clearFilters();
|
||||
}
|
||||
|
||||
// Refresh data to keep UI real-time
|
||||
ref.invalidate(documentPageProvider);
|
||||
ref.invalidate(foldersProvider);
|
||||
ref.invalidate(studentFoldersProvider);
|
||||
ref.invalidate(trashDocumentsProvider);
|
||||
if (_tabController.index == 2) {
|
||||
ref.invalidate(requirementsSettingsProvider);
|
||||
}
|
||||
|
||||
if (_tabController.index != 0 && _openedFolderStudentId != null) {
|
||||
setState(() {
|
||||
@@ -869,6 +880,22 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Tab 2: Document Requirements Workflow
|
||||
DocumentRequirementsView(
|
||||
userRole: widget.userRole,
|
||||
onFilterByDocumentType: (type) {
|
||||
_skipFilterClearOnNextTabChange = true;
|
||||
setState(() {
|
||||
_selectedDocumentType = type;
|
||||
});
|
||||
_tabController.animateTo(1);
|
||||
_applyFilters();
|
||||
},
|
||||
onSwitchToDocumentsTab: () {
|
||||
_tabController.animateTo(1);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -999,6 +1026,17 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
isMobile: isMobile,
|
||||
isMobileOrAndroid: isMobileOrAndroid,
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
_buildSegmentedTabItem(
|
||||
index: 2,
|
||||
icon: Icons.assignment_outlined,
|
||||
activeIcon: Icons.assignment_rounded,
|
||||
label: isMobile ? 'Reqs' : 'Requirements',
|
||||
isSelected: activeIndex == 2,
|
||||
isDark: isDark,
|
||||
isMobile: isMobile,
|
||||
isMobileOrAndroid: isMobileOrAndroid,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -1208,7 +1246,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
],
|
||||
|
||||
// Action buttons
|
||||
if (!isMobile && !isFolderOpened) ...[
|
||||
if (!isMobile && !isFolderOpened && _tabController.index != 2) ...[
|
||||
Tooltip(
|
||||
richMessage: (_searchController.text.isNotEmpty || query.search.isNotEmpty)
|
||||
? const TextSpan(text: 'Clear Search')
|
||||
|
||||
@@ -0,0 +1,950 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../../core/constants/app_colors.dart';
|
||||
import '../../../../domain/entities/document_requirement_model.dart';
|
||||
import '../../../../domain/entities/student_model.dart';
|
||||
import '../../../providers/document_provider.dart';
|
||||
import '../../../providers/student_provider.dart';
|
||||
import 'requirements_settings_modal.dart';
|
||||
import 'student_profile_modal.dart';
|
||||
import 'upload_ocr_modal.dart';
|
||||
|
||||
class DocumentRequirementsView extends ConsumerStatefulWidget {
|
||||
final String userRole;
|
||||
final ValueChanged<String>? onFilterByDocumentType;
|
||||
final VoidCallback? onSwitchToDocumentsTab;
|
||||
|
||||
const DocumentRequirementsView({
|
||||
super.key,
|
||||
required this.userRole,
|
||||
this.onFilterByDocumentType,
|
||||
this.onSwitchToDocumentsTab,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<DocumentRequirementsView> createState() =>
|
||||
_DocumentRequirementsViewState();
|
||||
}
|
||||
|
||||
class _DocumentRequirementsViewState
|
||||
extends ConsumerState<DocumentRequirementsView> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String _searchQuery = '';
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final settingsAsync = ref.watch(requirementsSettingsProvider);
|
||||
final enrolledStudentsAsync = ref.watch(enrolledStudentsForRequirementsProvider);
|
||||
|
||||
return settingsAsync.when(
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.primaryGreen),
|
||||
),
|
||||
error: (e, _) => Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: AppColors.error),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Failed to load document requirements',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'$e',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: isDark ? Colors.white70 : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
ref.invalidate(requirementsSettingsProvider);
|
||||
},
|
||||
icon: const Icon(Icons.refresh),
|
||||
label: const Text('Retry'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
data: (settings) {
|
||||
// Collect all actively enrolled students to calculate missing statistics.
|
||||
// Archived, graduated, transferred, or dropped students are strictly excluded.
|
||||
final allEnrolled = enrolledStudentsAsync.asData?.value ?? [];
|
||||
final students = allEnrolled
|
||||
.where((s) => s.status.toLowerCase() == 'enrolled')
|
||||
.toList();
|
||||
|
||||
// Filter requirements by local search
|
||||
final filteredJhs = settings.jhs.where((r) {
|
||||
if (_searchQuery.isEmpty) return true;
|
||||
return r.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
(r.description?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false);
|
||||
}).toList();
|
||||
|
||||
final filteredShs = settings.shs.where((r) {
|
||||
if (_searchQuery.isEmpty) return true;
|
||||
return r.name.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
(r.description?.toLowerCase().contains(_searchQuery.toLowerCase()) ?? false);
|
||||
}).toList();
|
||||
|
||||
// Calculate compliance stats
|
||||
int totalMissingDocsCount = 0;
|
||||
final Set<int> studentsWithMissingDocs = {};
|
||||
|
||||
for (final s in students) {
|
||||
if (s.missingDocumentsCount > 0 || s.missingDocuments.isNotEmpty) {
|
||||
totalMissingDocsCount += s.missingDocumentsCount > 0
|
||||
? s.missingDocumentsCount
|
||||
: s.missingDocuments.length;
|
||||
studentsWithMissingDocs.add(s.id);
|
||||
}
|
||||
}
|
||||
|
||||
return RefreshIndicator(
|
||||
color: AppColors.primaryGreen,
|
||||
onRefresh: () async {
|
||||
ref.invalidate(requirementsSettingsProvider);
|
||||
ref.invalidate(enrolledStudentsForRequirementsProvider);
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Top Operational Banner / Summary
|
||||
_buildSummaryBanner(
|
||||
isDark: isDark,
|
||||
totalJhs: settings.jhs.where((r) => r.isEnabled).length,
|
||||
totalShs: settings.shs.where((r) => r.isEnabled).length,
|
||||
studentsNeedingAttention: studentsWithMissingDocs.length,
|
||||
totalMissingDocs: totalMissingDocsCount,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Controls Bar: Search & Manage Button
|
||||
_buildControlsBar(isDark),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Category 1: Junior High School (JHS) Requirements
|
||||
_buildCategoryGroup(
|
||||
title: 'Junior High School (JHS)',
|
||||
subtitle: 'Grades 7 to 10 requirements checklist & compliance',
|
||||
icon: Icons.school_rounded,
|
||||
badgeColor: AppColors.primaryGreen,
|
||||
requirements: filteredJhs,
|
||||
students: students,
|
||||
isDark: isDark,
|
||||
isJhs: true,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Category 2: Senior High School (SHS) Requirements
|
||||
_buildCategoryGroup(
|
||||
title: 'Senior High School (SHS)',
|
||||
subtitle: 'Grades 11 to 12 requirements checklist & compliance',
|
||||
icon: Icons.history_edu_rounded,
|
||||
badgeColor: const Color(0xFF1565C0),
|
||||
requirements: filteredShs,
|
||||
students: students,
|
||||
isDark: isDark,
|
||||
isJhs: false,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// SUMMARY BANNER
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
Widget _buildSummaryBanner({
|
||||
required bool isDark,
|
||||
required int totalJhs,
|
||||
required int totalShs,
|
||||
required int studentsNeedingAttention,
|
||||
required int totalMissingDocs,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurfaceCard : Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: isDark ? AppColors.darkBorder : Colors.grey.shade200,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: isDark ? 0.2 : 0.03),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryGreen.withValues(alpha: isDark ? 0.25 : 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.rule_folder_rounded,
|
||||
size: 22,
|
||||
color: AppColors.primaryGreen,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Document Requirements & Compliance',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Department of Education official document standards & tracking',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
const Divider(height: 1),
|
||||
const SizedBox(height: 14),
|
||||
// Metric chips
|
||||
Wrap(
|
||||
spacing: 10,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildMetricPill(
|
||||
label: 'JHS Active',
|
||||
value: '$totalJhs items',
|
||||
icon: Icons.check_circle_outline,
|
||||
color: AppColors.primaryGreen,
|
||||
isDark: isDark,
|
||||
),
|
||||
_buildMetricPill(
|
||||
label: 'SHS Active',
|
||||
value: '$totalShs items',
|
||||
icon: Icons.check_circle_outline,
|
||||
color: const Color(0xFF1565C0),
|
||||
isDark: isDark,
|
||||
),
|
||||
if (studentsNeedingAttention > 0)
|
||||
_buildMetricPill(
|
||||
label: 'Needs Attention',
|
||||
value: '$studentsNeedingAttention students',
|
||||
icon: Icons.warning_amber_rounded,
|
||||
color: Colors.orange.shade800,
|
||||
isDark: isDark,
|
||||
)
|
||||
else
|
||||
_buildMetricPill(
|
||||
label: 'Status',
|
||||
value: 'All Complete',
|
||||
icon: Icons.verified_rounded,
|
||||
color: AppColors.primaryGreen,
|
||||
isDark: isDark,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMetricPill({
|
||||
required String label,
|
||||
required String value,
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required bool isDark,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: isDark ? 0.18 : 0.08),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: color.withValues(alpha: isDark ? 0.35 : 0.25)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 14, color: color),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'$label: ',
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isDark ? Colors.white70 : Colors.black87,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// CONTROLS BAR: SEARCH & CONFIGURE
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
Widget _buildControlsBar(bool isDark) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
height: 38,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurfaceCard : Colors.white,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(
|
||||
color: isDark ? AppColors.darkBorder : Colors.grey.shade300,
|
||||
),
|
||||
),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
onChanged: (val) => setState(() => _searchQuery = val.trim()),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Search requirements (e.g. SF9, PSA, SF10)...',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: isDark ? Colors.white38 : Colors.grey.shade500,
|
||||
),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
size: 18,
|
||||
color: isDark ? Colors.white54 : Colors.grey.shade600,
|
||||
),
|
||||
suffixIcon: _searchQuery.isNotEmpty
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.clear, size: 16),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
setState(() => _searchQuery = '');
|
||||
},
|
||||
)
|
||||
: null,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||
border: InputBorder.none,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: isDark ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.userRole != 'teacher') ...[
|
||||
const SizedBox(width: 10),
|
||||
SizedBox(
|
||||
height: 38,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (_) => const RequirementsSettingsModal(),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.tune_rounded, size: 16),
|
||||
label: const Text(
|
||||
'Configure',
|
||||
style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryGreen,
|
||||
side: const BorderSide(color: AppColors.primaryGreen),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// CATEGORY GROUP (JHS / SHS)
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
Widget _buildCategoryGroup({
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required IconData icon,
|
||||
required Color badgeColor,
|
||||
required List<DocumentRequirementModel> requirements,
|
||||
required List<StudentModel> students,
|
||||
required bool isDark,
|
||||
required bool isJhs,
|
||||
}) {
|
||||
if (requirements.isEmpty && _searchQuery.isNotEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurfaceCard : Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(
|
||||
color: isDark ? AppColors.darkBorder : Colors.grey.shade200,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: badgeColor),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: badgeColor.withValues(alpha: isDark ? 0.25 : 0.12),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
'${requirements.length} Requirements',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: badgeColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
|
||||
// Requirements Items
|
||||
if (requirements.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'No requirements found.',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: isDark ? Colors.white54 : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: requirements.length,
|
||||
separatorBuilder: (ctx, i) => Divider(
|
||||
height: 1,
|
||||
color: isDark ? AppColors.darkBorder : Colors.grey.shade100,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final req = requirements[index];
|
||||
return _buildRequirementRow(
|
||||
req: req,
|
||||
students: students,
|
||||
isDark: isDark,
|
||||
isJhs: isJhs,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// REQUIREMENT ROW
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
Widget _buildRequirementRow({
|
||||
required DocumentRequirementModel req,
|
||||
required List<StudentModel> students,
|
||||
required bool isDark,
|
||||
required bool isJhs,
|
||||
}) {
|
||||
// Filter relevant students based on JHS (grades 7-10) vs SHS (grades 11-12)
|
||||
final categoryStudents = students.where((s) {
|
||||
final grade = s.latestGradeLevel;
|
||||
if (grade == null) return true;
|
||||
if (isJhs) return grade <= 10;
|
||||
return grade >= 11;
|
||||
}).toList();
|
||||
|
||||
// Find students missing this specific document requirement
|
||||
final missingStudents = categoryStudents.where((s) {
|
||||
final cleanReqName = req.name.toLowerCase().trim();
|
||||
final hasMissing = s.missingDocuments.any((doc) {
|
||||
final cleanDoc = doc.toLowerCase().trim();
|
||||
return cleanDoc == cleanReqName ||
|
||||
cleanDoc.contains(cleanReqName) ||
|
||||
cleanReqName.contains(cleanDoc);
|
||||
});
|
||||
return hasMissing;
|
||||
}).toList();
|
||||
|
||||
final isEnabled = req.isEnabled;
|
||||
final missingCount = missingStudents.length;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Icon badge
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
color: (isEnabled ? AppColors.primaryGreen : Colors.grey)
|
||||
.withValues(alpha: isDark ? 0.20 : 0.10),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
isEnabled ? Icons.assignment_turned_in_outlined : Icons.remove_circle_outline,
|
||||
size: 20,
|
||||
color: isEnabled ? AppColors.primaryGreen : Colors.grey,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Title & Description
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
req.name,
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDark ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Standard status badge (✓ Active or ○ Inactive)
|
||||
_buildActiveBadge(isEnabled, isDark),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
if (req.description != null && req.description!.isNotEmpty)
|
||||
Text(
|
||||
req.description!,
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
// Compliance Status Badge (✓ Complete or ⚠ Missing)
|
||||
if (isEnabled)
|
||||
_buildCompliancePill(
|
||||
missingCount: missingCount,
|
||||
totalStudents: categoryStudents.length,
|
||||
isDark: isDark,
|
||||
)
|
||||
else
|
||||
Text(
|
||||
'Requirement currently optional or inactive',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontStyle: FontStyle.italic,
|
||||
color: isDark ? Colors.white38 : Colors.grey.shade500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// Action Buttons
|
||||
if (isEnabled) ...[
|
||||
if (missingCount > 0)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.people_outline_rounded, size: 20),
|
||||
color: Colors.orange.shade800,
|
||||
tooltip: 'View $missingCount missing students',
|
||||
onPressed: () => _showMissingStudentsSheet(
|
||||
context,
|
||||
reqName: req.name,
|
||||
missingStudents: missingStudents,
|
||||
isDark: isDark,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.filter_list_rounded, size: 20),
|
||||
color: AppColors.primaryGreen,
|
||||
tooltip: 'Filter in All Documents',
|
||||
onPressed: () {
|
||||
widget.onFilterByDocumentType?.call(req.name);
|
||||
widget.onSwitchToDocumentsTab?.call();
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// STATUS BADGES ACCORDING TO DESIGN SYSTEM
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
Widget _buildActiveBadge(bool isEnabled, bool isDark) {
|
||||
if (isEnabled) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryGreen.withValues(alpha: isDark ? 0.22 : 0.10),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryGreen.withValues(alpha: isDark ? 0.45 : 0.30),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check, size: 10, color: AppColors.primaryGreen),
|
||||
SizedBox(width: 2),
|
||||
Text(
|
||||
'Required',
|
||||
style: TextStyle(
|
||||
fontSize: 9.5,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primaryGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.white10 : Colors.grey.shade200,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.radio_button_unchecked, size: 9, color: Colors.grey.shade600),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'Inactive',
|
||||
style: TextStyle(
|
||||
fontSize: 9.5,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isDark ? Colors.white54 : Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildCompliancePill({
|
||||
required int missingCount,
|
||||
required int totalStudents,
|
||||
required bool isDark,
|
||||
}) {
|
||||
if (missingCount == 0) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryGreen.withValues(alpha: isDark ? 0.20 : 0.10),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryGreen.withValues(alpha: isDark ? 0.40 : 0.25),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check_circle_outline, size: 12, color: AppColors.primaryGreen),
|
||||
SizedBox(width: 4),
|
||||
Text(
|
||||
'✓ Complete • All students compliant',
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primaryGreen,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final color = Colors.orange.shade800;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 2.5),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: isDark ? 0.22 : 0.10),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: color.withValues(alpha: isDark ? 0.45 : 0.30),
|
||||
width: 0.5,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.warning_amber_rounded, size: 12, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'⚠ $missingCount students missing',
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
// MISSING STUDENTS BOTTOM SHEET / MODAL
|
||||
// ══════════════════════════════════════════════════════════════════════════
|
||||
void _showMissingStudentsSheet(
|
||||
BuildContext context, {
|
||||
required String reqName,
|
||||
required List<StudentModel> missingStudents,
|
||||
required bool isDark,
|
||||
}) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (ctx) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.75,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? AppColors.darkSurfaceCard : Colors.white,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Handle
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.white24 : Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Title
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.orange.withValues(alpha: 0.15),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
size: 20,
|
||||
color: Colors.orange.shade800,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Missing: $reqName',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${missingStudents.length} students have not submitted this requirement',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close),
|
||||
onPressed: () => Navigator.of(ctx).pop(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 20),
|
||||
|
||||
// Student List
|
||||
Flexible(
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
itemCount: missingStudents.length,
|
||||
separatorBuilder: (ctx, i) => Divider(
|
||||
height: 1,
|
||||
color: isDark ? AppColors.darkBorder : Colors.grey.shade100,
|
||||
),
|
||||
itemBuilder: (context, i) {
|
||||
final student = missingStudents[i];
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
title: Text(
|
||||
student.listDisplayName,
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isDark ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
'LRN: ${student.lrn} • ${student.gradeSection}',
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.cloud_upload_outlined, size: 20),
|
||||
tooltip: 'Upload $reqName',
|
||||
color: AppColors.primaryGreen,
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
UploadOcrModal.show(
|
||||
context,
|
||||
prefilledStudentId: student.id,
|
||||
prefilledLrn: student.lrn,
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.person_outline_rounded, size: 20),
|
||||
tooltip: 'View Profile',
|
||||
color: isDark ? Colors.white70 : Colors.black87,
|
||||
onPressed: () {
|
||||
Navigator.of(ctx).pop();
|
||||
showStudentProfileModal(
|
||||
context,
|
||||
studentId: student.id,
|
||||
userRole: widget.userRole,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -472,7 +472,7 @@ class _FileFolderCardState extends State<FileFolderCard> {
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom section: File name & metadata
|
||||
// Bottom section: File name, metadata & status badge
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 6, 8, 8),
|
||||
child: Column(
|
||||
@@ -481,7 +481,7 @@ class _FileFolderCardState extends State<FileFolderCard> {
|
||||
children: [
|
||||
Text(
|
||||
widget.document.fileName,
|
||||
maxLines: 2,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -493,17 +493,25 @@ class _FileFolderCardState extends State<FileFolderCard> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
'${FileIconHelper.formatFileSize(widget.document.fileSize ?? widget.document.size)} • ${formatShortDate(widget.document.createdAt)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isDark
|
||||
? AppColors.darkTextMuted
|
||||
: AppColors.textMuted,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
'${FileIconHelper.formatFileSize(widget.document.fileSize ?? widget.document.size)} • ${formatShortDate(widget.document.createdAt)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isDark
|
||||
? AppColors.darkTextMuted
|
||||
: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
_buildStatusBadge(widget.document.status, isDark),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -650,6 +658,8 @@ class _FileFolderCardState extends State<FileFolderCard> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
_buildStatusBadge(widget.document.status, isDark),
|
||||
if (widget.document.documentType != null &&
|
||||
widget.document.documentType!.isNotEmpty) ...[
|
||||
const SizedBox(width: 6),
|
||||
@@ -714,4 +724,69 @@ class _FileFolderCardState extends State<FileFolderCard> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Standardized status system badge:
|
||||
/// ✓ Complete / Verified, ! Missing, ○ Archived
|
||||
Widget _buildStatusBadge(String? status, bool isDark) {
|
||||
final norm = (status ?? 'Completed').trim().toLowerCase();
|
||||
|
||||
Color fg;
|
||||
Color bg;
|
||||
Color border;
|
||||
String label;
|
||||
IconData icon;
|
||||
|
||||
if (norm == 'completed' || norm == 'complete' || norm == 'verified') {
|
||||
fg = isDark ? const Color(0xFF81C784) : const Color(0xFF2E7D32);
|
||||
bg = isDark ? const Color(0x334CAF50) : const Color(0x182E7D32);
|
||||
border = isDark ? const Color(0x4D81C784) : const Color(0x332E7D32);
|
||||
label = 'Complete';
|
||||
icon = Icons.check_rounded;
|
||||
} else if (norm == 'missing') {
|
||||
fg = isDark ? const Color(0xFFEF5350) : const Color(0xFFC62828);
|
||||
bg = isDark ? const Color(0x33F44336) : const Color(0x1AC62828);
|
||||
border = isDark ? const Color(0x4DEF5350) : const Color(0x33C62828);
|
||||
label = 'Missing';
|
||||
icon = Icons.priority_high_rounded;
|
||||
} else if (norm == 'archived') {
|
||||
fg = isDark ? const Color(0xFFB0BEC5) : const Color(0xFF546E7A);
|
||||
bg = isDark ? const Color(0x3378909C) : const Color(0x1A546E7A);
|
||||
border = isDark ? const Color(0x4DB0BEC5) : const Color(0x33546E7A);
|
||||
label = 'Archived';
|
||||
icon = Icons.circle_outlined;
|
||||
} else {
|
||||
fg = isDark ? const Color(0xFF81C784) : const Color(0xFF2E7D32);
|
||||
bg = isDark ? const Color(0x334CAF50) : const Color(0x182E7D32);
|
||||
border = isDark ? const Color(0x4D81C784) : const Color(0x332E7D32);
|
||||
label = 'Complete';
|
||||
icon = Icons.check_rounded;
|
||||
}
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
border: Border.all(color: border, width: 0.7),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(icon, size: 10.5, color: fg),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 9.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: fg,
|
||||
height: 1.1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,38 +369,6 @@ class _StudentProfileModalBodyState
|
||||
extends ConsumerState<StudentProfileModalBody> {
|
||||
_StudentProfileTab _selectedTab = _StudentProfileTab.overview;
|
||||
|
||||
Future<void> _handleVerifyDocument(DocumentModel doc) async {
|
||||
const newStatus = 'Completed';
|
||||
try {
|
||||
await ref
|
||||
.read(documentRepositoryProvider)
|
||||
.updateDocumentStatus(doc.id, newStatus);
|
||||
ref.invalidate(studentDocumentsProvider(widget.studentId));
|
||||
ref.invalidate(missingRequirementsProvider(widget.studentId));
|
||||
ref.invalidate(studentDetailProvider(widget.studentId));
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Document marked as verified'),
|
||||
backgroundColor: AppColors.primaryGreen,
|
||||
duration: Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to update status: $e'),
|
||||
backgroundColor: AppColors.error,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleAddToPrintQueue(DocumentModel doc) async {
|
||||
try {
|
||||
await ref.read(printQueueMutationProvider.notifier).addToQueue(doc.id);
|
||||
@@ -1190,17 +1158,12 @@ class _StudentProfileModalBodyState
|
||||
bool isDark,
|
||||
) {
|
||||
final bool isMobile = MediaQuery.of(context).size.width < 500;
|
||||
final isPending = doc.status.toLowerCase() == 'pending';
|
||||
final isRejected = doc.status.toLowerCase() == 'rejected';
|
||||
final statusColor = isPending
|
||||
? AppColors.warning
|
||||
: (isRejected ? AppColors.error : AppColors.success);
|
||||
final statusIcon = isPending
|
||||
? Icons.hourglass_top_rounded
|
||||
: (isRejected ? Icons.cancel_outlined : Icons.check_circle_rounded);
|
||||
final statusLabel = isPending
|
||||
? 'Pending'
|
||||
: (isRejected ? 'Rejected' : 'Verified');
|
||||
final isArchived = doc.status.toLowerCase() == 'archived';
|
||||
final statusColor = isArchived ? Colors.grey : AppColors.success;
|
||||
final statusIcon = isArchived
|
||||
? Icons.archive_outlined
|
||||
: Icons.check_circle_rounded;
|
||||
final statusLabel = isArchived ? 'Archived' : 'Verified';
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
@@ -1348,22 +1311,7 @@ class _StudentProfileModalBodyState
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.userRole.toLowerCase() != 'teacher' && isPending)
|
||||
Tooltip(
|
||||
message: 'Mark as Verified',
|
||||
child: IconButton(
|
||||
icon: const Icon(
|
||||
Icons.check_circle_outline_rounded,
|
||||
size: 17,
|
||||
color: AppColors.success,
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
padding: const EdgeInsets.all(2),
|
||||
constraints:
|
||||
const BoxConstraints(minWidth: 26, minHeight: 26),
|
||||
onPressed: () => _handleVerifyDocument(doc),
|
||||
),
|
||||
),
|
||||
|
||||
Tooltip(
|
||||
message: 'Add to Print Queue',
|
||||
child: IconButton(
|
||||
|
||||
@@ -85,7 +85,7 @@ class _StudentFilterDialogState extends ConsumerState<StudentFilterDialog> {
|
||||
static const _docStatusSortItems = [
|
||||
'Default',
|
||||
'Completed',
|
||||
'Pending',
|
||||
'Missing',
|
||||
];
|
||||
static const _allTabStatusItems = [
|
||||
'All Status',
|
||||
@@ -446,13 +446,13 @@ class _StudentFilterDialogState extends ConsumerState<StudentFilterDialog> {
|
||||
? 'Completed'
|
||||
: ((_pendingSortBy == 'doc_status' &&
|
||||
_pendingSortOrder == 'desc')
|
||||
? 'Pending'
|
||||
? 'Missing'
|
||||
: 'Default'),
|
||||
onSelected: (v) => setState(() {
|
||||
if (v == 'Completed') {
|
||||
_pendingSortBy = 'doc_status';
|
||||
_pendingSortOrder = 'asc';
|
||||
} else if (v == 'Pending') {
|
||||
} else if (v == 'Missing') {
|
||||
_pendingSortBy = 'doc_status';
|
||||
_pendingSortOrder = 'desc';
|
||||
} else {
|
||||
|
||||
Reference in new issue
Block a user