Compare commits

..
3 Commits
13 changed files with 344 additions and 329 deletions

No files matched your search

+20
View File
@@ -1137,6 +1137,26 @@ Files backup/restore - mirror mode or zip -->
# STUDENT SCREEN
- [x] fix students tab showing all students on re-click: preserved active academic year in studentQueryProvider reset and fail-safe fallback in studentPageProvider for enrolled tab
# DASHBOARD SCREEN
- [x] move the recent activities to the last
- [x] use user friendly labels on needs attention, like 3 students missing SF10, 4 students missing SF9, etc if the count is not more than 1 use student a singular form
- [x] do not use many colors in needs attention and icons make it professional
- [x] fix that the needs attention when fetching from websocket the UI shows a loading icon
# STUDENT SCREEN
- [x] in active year tab, it has add student and enroll existing student, only appear when there is no students enrolled
- [x] in windows app the "grade and section" and document completion counter is not aligned
# ACTIVITY HISTORY SCREEN
- [x] it has too many x button after a search it has search bar x button but not working search bar and reset button, and the search icon that animates to x and clears the search
# WINDOWS RUNNER
- [x] prevent multiple app windows from opening (single-instance mutex and focus existing window)
# ENROLLMENTS & STUDENT SCREEN
- [x] sort enrollments chronologically ascending by school year and grade level across student profile, edit student, edit enrollment, and OCR review
- [x] progression and regression validation: prevent grade skipping and downgrading across academic year chronology
---
@@ -15,15 +15,20 @@ class IpLocationHelper {
return 'Local Machine';
}
// Check private RFC1918 & link-local ranges
if (cleanIp.startsWith('192.168.') ||
cleanIp.startsWith('10.') ||
cleanIp.startsWith('169.254.')) {
final lowerIp = cleanIp.toLowerCase();
// Check private RFC1918, RFC4193, & link-local ranges (both IPv4 and IPv6)
if (lowerIp.startsWith('192.168.') ||
lowerIp.startsWith('10.') ||
lowerIp.startsWith('169.254.') ||
lowerIp.startsWith('fe80:') ||
lowerIp.startsWith('fc') ||
lowerIp.startsWith('fd')) {
return 'Local Network (LAN)';
}
if (cleanIp.startsWith('172.')) {
final parts = cleanIp.split('.');
if (lowerIp.startsWith('172.')) {
final parts = lowerIp.split('.');
if (parts.length >= 2) {
final second = int.tryParse(parts[1]);
if (second != null && second >= 16 && second <= 31) {
@@ -66,6 +66,8 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
_shortcutFocusNode.requestFocus();
}
});
} else {
_clearFilters();
}
});
});
@@ -81,18 +83,15 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
}
void _clearFilters() {
if (!mounted) return;
setState(() {
_fromDate = null;
_toDate = null;
_searchController.clear();
});
if (_selectedTab == AuditTab.activities) {
ref.read(activityQueryProvider.notifier).reset();
} else if (_selectedTab == AuditTab.users) {
ref.read(userHistoryQueryProvider.notifier).reset();
} else {
ref.read(loginSessionsQueryProvider.notifier).reset();
}
ref.read(activityQueryProvider.notifier).reset();
ref.read(userHistoryQueryProvider.notifier).reset();
ref.read(loginSessionsQueryProvider.notifier).reset();
}
@@ -833,15 +832,19 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
labelPadding: const EdgeInsets.symmetric(horizontal: 4),
),
),
TextButton(
onPressed: _clearFilters,
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.symmetric(horizontal: 6),
foregroundColor: Colors.redAccent,
if (_fromDate != null ||
_toDate != null ||
(currentAction.isNotEmpty && currentAction != 'All') ||
(currentRole.isNotEmpty && currentRole != 'All'))
TextButton(
onPressed: _clearFilters,
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.symmetric(horizontal: 6),
foregroundColor: Colors.redAccent,
),
child: const Text('Reset', style: TextStyle(fontSize: 11)),
),
child: const Text('Reset', style: TextStyle(fontSize: 11)),
),
],
),
);
@@ -994,24 +997,17 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
// Search Icon
Tooltip(
message: currentSearch.isNotEmpty ? 'Clear Search' : 'Search',
message: 'Search',
child: IconButton(
icon: Icon(
currentSearch.isNotEmpty ? Icons.close : Icons.search,
Icons.search,
size: 20,
color: isDark ? AppColors.darkTextPrimary : Colors.black87,
),
visualDensity: VisualDensity.compact,
padding: const EdgeInsets.all(6),
constraints: const BoxConstraints(),
onPressed: () {
if (currentSearch.isNotEmpty) {
_searchController.clear();
_applySearch('');
} else {
_showSearchDialog(context);
}
},
onPressed: () => _showSearchDialog(context),
),
),
const SizedBox(width: 8),
@@ -1071,27 +1067,16 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
// Search Icon (same as other screens)
Tooltip(
message: currentSearch.isNotEmpty
? 'Clear Search'
: 'Search (Ctrl+F)',
message: 'Search (Ctrl+F)',
child: IconButton(
icon: Icon(
currentSearch.isNotEmpty
? Icons.close
: Icons.search,
Icons.search,
size: 22,
color: isDark
? AppColors.darkTextPrimary
: Colors.black87,
),
onPressed: () {
if (currentSearch.isNotEmpty) {
_searchController.clear();
_applySearch('');
} else {
_showSearchDialog(context);
}
},
onPressed: () => _showSearchDialog(context),
),
),
@@ -1165,8 +1150,8 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
if (_selectedTab != tab) {
setState(() {
_selectedTab = tab;
_searchController.clear();
});
_clearFilters();
}
},
child: AnimatedContainer(
@@ -2041,8 +2026,12 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
final ipLoc = IpLocationHelper.getFastLocation(log.ipAddress);
onTap: () async {
String? ipLoc = IpLocationHelper.getFastLocation(log.ipAddress);
if (ipLoc == null && log.ipAddress != null && log.ipAddress!.isNotEmpty) {
ipLoc = await IpLocationHelper.resolveLocation(log.ipAddress);
}
if (!context.mounted) return;
final ipDesc = log.ipAddress != null
? (ipLoc != null ? '${log.ipAddress} ($ipLoc)' : log.ipAddress!)
: 'Unknown IP';
@@ -2210,7 +2199,7 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 12,
runSpacing: 4,
runSpacing: 6,
children: [
Row(
mainAxisSize: MainAxisSize.min,
@@ -2236,64 +2225,85 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
),
],
),
if (log.ipAddress != null && log.ipAddress!.isNotEmpty)
Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.lan_outlined,
size: 13,
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.5),
),
const SizedBox(width: 4),
Text(
log.ipAddress!,
style: TextStyle(
fontSize: 12,
if (log.ipAddress != null && log.ipAddress!.isNotEmpty) ...[
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 320),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.lan_outlined,
size: 13,
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.6),
.colorScheme
.onSurface
.withValues(alpha: 0.5),
),
),
FutureBuilder<String>(
future: IpLocationHelper.resolveLocation(log.ipAddress!),
initialData: IpLocationHelper.getFastLocation(log.ipAddress!),
builder: (context, snapshot) {
final loc = snapshot.data;
if (loc == null || loc.isEmpty) return const SizedBox.shrink();
return Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 6),
Icon(
Icons.location_on_outlined,
size: 12,
const SizedBox(width: 4),
Flexible(
child: Tooltip(
message: log.ipAddress!,
child: Text(
log.ipAddress!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
fontFamily: 'monospace',
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.5),
),
const SizedBox(width: 2),
Text(
loc,
style: TextStyle(
fontSize: 11,
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.6),
),
),
),
),
],
),
),
FutureBuilder<String>(
future: IpLocationHelper.resolveLocation(log.ipAddress!),
initialData: IpLocationHelper.getFastLocation(log.ipAddress!),
builder: (context, snapshot) {
final loc = snapshot.data;
if (loc == null || loc.isEmpty) return const SizedBox.shrink();
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 240),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.location_on_outlined,
size: 12,
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.5),
),
const SizedBox(width: 3),
Flexible(
child: Tooltip(
message: loc,
child: Text(
loc,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
color: Theme.of(context)
.colorScheme
.onSurface
.withValues(alpha: 0.65),
),
),
),
],
);
},
),
],
),
],
),
);
},
),
],
if (log.logoutAt != null)
Row(
mainAxisSize: MainAxisSize.min,
@@ -457,35 +457,13 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
const SizedBox(height: 24),
_buildStatGrid(data.stats, user),
const SizedBox(height: 28),
LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 900) {
return const IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(flex: 6, child: NeedsAttentionCard()),
SizedBox(width: 20),
Expanded(flex: 5, child: RecentActivityFeed()),
],
),
);
} else {
return const Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
NeedsAttentionCard(),
SizedBox(height: 20),
RecentActivityFeed(),
],
);
}
},
),
const NeedsAttentionCard(),
if (isAdmin) ...[
const SizedBox(height: 32),
const DashboardKpisSection(),
],
const SizedBox(height: 32),
const RecentActivityFeed(),
const SizedBox(height: 48),
],
),
@@ -14,49 +14,6 @@ import '../../../../domain/entities/dashboard_models.dart';
class NeedsAttentionCard extends ConsumerWidget {
const NeedsAttentionCard({super.key});
IconData _getRequirementIcon(String name) {
final upper = name.toUpperCase();
if (upper.contains('SF9') || upper.contains('REPORT CARD') || upper.contains('FORM 9')) {
return Icons.assignment_late_outlined;
}
if (upper.contains('SF10') || upper.contains('FORM 137') || upper.contains('FORM 10')) {
return Icons.description_outlined;
}
if (upper.contains('PSA') || upper.contains('BIRTH')) {
return Icons.badge_outlined;
}
if (upper.contains('GOOD MORAL')) {
return Icons.verified_outlined;
}
if (upper.contains('MEDICAL')) {
return Icons.medical_services_outlined;
}
if (upper.contains('CERTIFICATE') || upper.contains('ENROLLMENT')) {
return Icons.card_membership_outlined;
}
return Icons.insert_drive_file_outlined;
}
Color _getRequirementColor(String name) {
final upper = name.toUpperCase();
if (upper.contains('SF9') || upper.contains('FORM 9')) {
return Colors.orange.shade800;
}
if (upper.contains('SF10') || upper.contains('FORM 137') || upper.contains('FORM 10')) {
return Colors.amber.shade800;
}
if (upper.contains('PSA') || upper.contains('BIRTH')) {
return const Color(0xFF0284C7); // Sky Blue
}
if (upper.contains('GOOD MORAL')) {
return const Color(0xFF16A34A); // Green
}
if (upper.contains('MEDICAL')) {
return const Color(0xFFDC2626); // Red
}
return const Color(0xFF7C3AED); // Purple
}
@override
Widget build(BuildContext context, WidgetRef ref) {
final isDark = Theme.of(context).brightness == Brightness.dark;
@@ -65,7 +22,7 @@ class NeedsAttentionCard extends ConsumerWidget {
final needsAttentionAsync = ref.watch(dashboardNeedsAttentionProvider);
if (needsAttentionAsync.isLoading) {
if (needsAttentionAsync.isLoading && !needsAttentionAsync.hasValue) {
return Container(
height: 140,
decoration: BoxDecoration(
@@ -86,7 +43,7 @@ class NeedsAttentionCard extends ConsumerWidget {
);
}
if (needsAttentionAsync.hasError) {
if (needsAttentionAsync.hasError && !needsAttentionAsync.hasValue) {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
@@ -202,6 +159,8 @@ class NeedsAttentionCard extends ConsumerWidget {
),
child: Text(
'$totalAlerts items',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.bold,
@@ -217,6 +176,8 @@ class NeedsAttentionCard extends ConsumerWidget {
totalAlerts > 0
? 'Operational items requiring action'
: 'All document requirements compliant',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary,
@@ -256,24 +217,29 @@ class NeedsAttentionCard extends ConsumerWidget {
if (totalAlerts > 0) ...[
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final item in missingGroups)
_buildRequirementChip(
context: context,
item: item,
isDark: isDark,
onTap: () => _openMissingDocSheet(
context,
title: 'Students Missing ${item.name}',
students: item.students,
userRole: userRole,
isDark: isDark,
),
),
],
child: LayoutBuilder(
builder: (context, constraints) {
return Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final item in missingGroups)
_buildRequirementChip(
context: context,
item: item,
isDark: isDark,
maxWidth: constraints.maxWidth,
onTap: () => _openMissingDocSheet(
context,
title: 'Students Missing ${item.name}',
students: item.students,
userRole: userRole,
isDark: isDark,
),
),
],
);
},
),
),
@@ -357,60 +323,76 @@ class NeedsAttentionCard extends ConsumerWidget {
required MissingRequirementGroup item,
required bool isDark,
required VoidCallback onTap,
double? maxWidth,
}) {
final icon = _getRequirementIcon(item.name);
final color = _getRequirementColor(item.name);
final countLabel = item.count <= 1
? '1 student missing ${item.name}'
: '${item.count} students missing ${item.name}';
final accentColor = isDark ? Colors.amber.shade400 : const Color(0xFFB45309);
final bgColor = isDark
? AppColors.darkSurface2
: const Color(0xFFFFFBEB);
final borderColor = isDark
? AppColors.darkBorder
: const Color(0xFFFDE68A);
return Material(
color: Colors.transparent,
child: Tooltip(
message: 'Click to view ${item.count} student${item.count == 1 ? '' : 's'} missing ${item.name}',
message: 'Click to view ${item.count} ${item.count <= 1 ? "student" : "students"} missing ${item.name}',
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(10),
child: Container(
constraints: maxWidth != null ? BoxConstraints(maxWidth: maxWidth) : null,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: color.withValues(alpha: isDark ? 0.15 : 0.08),
color: bgColor,
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: color.withValues(alpha: isDark ? 0.35 : 0.25),
color: borderColor,
width: 1,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 16, color: color),
Icon(Icons.description_outlined, size: 16, color: accentColor),
const SizedBox(width: 8),
Text(
'${item.count} missing ${item.name}',
style: TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : AppColors.textPrimary,
Flexible(
child: Text(
countLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : AppColors.textPrimary,
),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
padding: const EdgeInsets.symmetric(horizontal: 7, vertical: 3),
decoration: BoxDecoration(
color: color,
color: isDark
? Colors.amber.shade900.withValues(alpha: 0.35)
: const Color(0xFFFEF3C7),
borderRadius: BorderRadius.circular(6),
),
child: const Row(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'View',
style: TextStyle(
fontSize: 10,
fontSize: 10.5,
fontWeight: FontWeight.bold,
color: Colors.white,
color: accentColor,
),
),
SizedBox(width: 2),
Icon(Icons.arrow_forward, size: 10, color: Colors.white),
const SizedBox(width: 3),
Icon(Icons.arrow_forward_rounded, size: 11, color: accentColor),
],
),
),
@@ -481,19 +463,29 @@ class NeedsAttentionCard extends ConsumerWidget {
children: [
for (final doc in missingList)
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.red.withValues(alpha: 0.12),
color: isDark
? Colors.amber.shade900.withValues(alpha: 0.25)
: const Color(0xFFFEF3C7),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: isDark
? Colors.amber.shade800.withValues(alpha: 0.3)
: const Color(0xFFFDE68A),
width: 0.8,
),
),
child: Text(
'! ${_formatDocTag(doc)}',
_formatDocTag(doc),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.red.shade800,
fontSize: 10.5,
fontWeight: FontWeight.w600,
color: isDark
? Colors.amber.shade300
: const Color(0xFF92400E),
),
),
),
@@ -610,6 +602,8 @@ class NeedsAttentionCard extends ConsumerWidget {
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@@ -617,7 +611,7 @@ class NeedsAttentionCard extends ConsumerWidget {
),
),
Text(
'${students.length} students currently missing this record',
'${students.length} ${students.length <= 1 ? "student" : "students"} currently missing this record',
style: TextStyle(
fontSize: 12,
color: isDark ? AppColors.darkTextSecondary : AppColors.textSecondary,
@@ -751,9 +751,13 @@ class _StudentProfileModalBodyState
if (student.enrollments != null && student.enrollments!.isNotEmpty) ...[
...(() {
final sorted = List.from(student.enrollments!);
sorted.sort(
(a, b) => (b.gradeLevel ?? 0).compareTo(a.gradeLevel ?? 0),
);
sorted.sort((a, b) {
final ayA = (a.yearRange ?? '').toString();
final ayB = (b.yearRange ?? '').toString();
final cmpYear = ayA.compareTo(ayB);
if (cmpYear != 0) return cmpYear;
return (a.gradeLevel ?? 0).compareTo(b.gradeLevel ?? 0);
});
final seen = <String>{};
final uniqueEnrollments = [];
@@ -1865,7 +1865,6 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
if (students.isEmpty) return _buildEmptyState(noSections: noSections);
final isDark = Theme.of(context).brightness == Brightness.dark;
final showBottomActions = _viewTab == StudentViewTab.enrolled && widget.userRole != 'teacher';
return RefreshIndicator(
color: AppColors.primaryGreen,
@@ -1883,14 +1882,11 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
top: isDesktop ? 10 : 14,
bottom: isDesktop ? 20 : (defaultTargetPlatform == TargetPlatform.android ? 8 : 16),
),
itemCount: students.length + (showBottomActions ? 1 : 0),
itemCount: students.length,
separatorBuilder: (ctx, index) => SizedBox(
height: isDesktop ? 8 : AppSizes.p12,
),
itemBuilder: (context, i) {
if (i == students.length) {
return _buildEnrolledBottomActions(isDark, isDesktop);
}
final s = students[i];
final isSelected = _selectedStudentIds.contains(s.id);
if (isDesktop) {
@@ -1903,49 +1899,6 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
);
}
Widget _buildEnrolledBottomActions(bool isDark, bool isDesktop) {
return Container(
margin: const EdgeInsets.only(top: 8, bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration(
color: isDark ? AppColors.darkSurfaceCard : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isDark ? AppColors.darkBorder : Colors.grey.shade200,
),
),
child: Wrap(
spacing: 12,
runSpacing: 8,
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
OutlinedButton.icon(
onPressed: () => _openModal(),
icon: const Icon(Icons.person_add_rounded, size: 18),
label: const Text('Add Student'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryGreen,
side: const BorderSide(color: AppColors.primaryGreen),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
),
),
ElevatedButton.icon(
onPressed: () => _applyViewTab(StudentViewTab.all),
icon: const Icon(Icons.people_alt_rounded, size: 18),
label: const Text('Enroll Existing Student (All Students)'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryGreen,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
),
),
],
),
);
}
Widget _buildDesktopCard(
BuildContext context,
@@ -2078,9 +2031,8 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
),
const SizedBox(width: 14),
// Name & LRN (flex 3)
// Name & LRN
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
@@ -2112,11 +2064,11 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
],
),
),
const SizedBox(width: 12),
const SizedBox(width: 16),
// Grade & Section (flex 2)
Expanded(
flex: 2,
// Grade & Section (aligned fixed-width column)
SizedBox(
width: 170,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -2128,7 +2080,7 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
: AppColors.textSecondary,
),
const SizedBox(width: 6),
Flexible(
Expanded(
child: Text(
s.gradeSection,
maxLines: 1,
@@ -2145,11 +2097,11 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
],
),
),
const SizedBox(width: 12),
const SizedBox(width: 16),
// Document completion counter (flex 2)
Expanded(
flex: 2,
// Document completion counter (aligned fixed-width column)
SizedBox(
width: 195,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
@@ -2248,64 +2200,70 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
],
),
),
const SizedBox(width: 10),
const SizedBox(width: 14),
// 4Ps & Status
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (s.is4ps) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: (isDark
? const Color(0xFF8B8ED8)
: AppColors.fourPs)
.withValues(alpha: isDark ? 0.2 : 0.08),
borderRadius: BorderRadius.circular(5),
border: Border.all(
// 4Ps & Status (aligned fixed-width column)
SizedBox(
width: 130,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (s.is4ps) ...[
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: (isDark
? const Color(0xFF8B8ED8)
: AppColors.fourPs)
.withValues(alpha: isDark ? 0.6 : 0.35),
.withValues(alpha: isDark ? 0.2 : 0.08),
borderRadius: BorderRadius.circular(5),
border: Border.all(
color: (isDark
? const Color(0xFF8B8ED8)
: AppColors.fourPs)
.withValues(alpha: isDark ? 0.6 : 0.35),
),
),
child: Text(
'4Ps',
style: TextStyle(
color: isDark
? const Color(0xFF8B8ED8)
: AppColors.fourPs,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
child: Text(
'4Ps',
style: TextStyle(
color: isDark
? const Color(0xFF8B8ED8)
: AppColors.fourPs,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 6),
const SizedBox(width: 6),
],
_StatusChip(status: s.status),
],
_StatusChip(status: s.status),
],
),
),
const SizedBox(width: 10),
const SizedBox(width: 8),
// Open Profile chevron / more options
Tooltip(
message: 'View Profile',
child: IconButton(
icon: const Icon(
Icons.chevron_right_rounded,
size: 18,
SizedBox(
width: 28,
child: Tooltip(
message: 'View Profile',
child: IconButton(
icon: const Icon(
Icons.chevron_right_rounded,
size: 18,
),
color: isDark
? AppColors.darkTextSecondary
: AppColors.textSecondary,
padding: const EdgeInsets.all(4),
constraints:
const BoxConstraints(minWidth: 28, minHeight: 28),
onPressed: () => _viewProfile(s),
),
color: isDark
? AppColors.darkTextSecondary
: AppColors.textSecondary,
padding: const EdgeInsets.all(4),
constraints:
const BoxConstraints(minWidth: 28, minHeight: 28),
onPressed: () => _viewProfile(s),
),
),
],
@@ -373,7 +373,13 @@ class _EditEnrollmentModalState extends ConsumerState<EditEnrollmentModal> {
final enrollments =
studentDetail?.enrollments ?? <EnrollmentModel>[];
final sortedEnrollments = List<EnrollmentModel>.from(enrollments)
..sort((a, b) => b.gradeLevel.compareTo(a.gradeLevel));
..sort((a, b) {
final ayA = (a.yearRange ?? '').toString();
final ayB = (b.yearRange ?? '').toString();
final cmpYear = ayA.compareTo(ayB);
if (cmpYear != 0) return cmpYear;
return a.gradeLevel.compareTo(b.gradeLevel);
});
// 1. Determine active/effective Academic Year
final activeYears =
@@ -770,9 +770,13 @@ class _EditStudentModalState extends ConsumerState<EditStudentModal> {
_loadedEnrollments = enrollments;
final sorted = List.from(enrollments);
sorted.sort(
(a, b) => (b.gradeLevel ?? 0).compareTo(a.gradeLevel ?? 0),
);
sorted.sort((a, b) {
final ayA = (a.yearRange ?? '').toString();
final ayB = (b.yearRange ?? '').toString();
final cmpYear = ayA.compareTo(ayB);
if (cmpYear != 0) return cmpYear;
return (a.gradeLevel ?? 0).compareTo(b.gradeLevel ?? 0);
});
final isMobile = MediaQuery.of(context).size.width < 600;
@@ -45,7 +45,6 @@ class OcrEnrollmentValidationModal extends ConsumerStatefulWidget {
final _ValidationMode _mode;
const OcrEnrollmentValidationModal._({
super.key,
this.studentId,
required this.records,
required _ValidationMode mode,
@@ -113,7 +112,14 @@ class _OcrEnrollmentValidationModalState
? gradeNum
: 7,
);
}).toList();
}).toList()
..sort((a, b) {
final syA = a.record.schoolYear.trim();
final syB = b.record.schoolYear.trim();
final cmp = syA.compareTo(syB);
if (cmp != 0) return cmp;
return a.gradeLevel.compareTo(b.gradeLevel);
});
_initDefaults();
}
@@ -541,7 +541,6 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
onPressed: () {
_controller.clear();
widget.onChanged?.call('');
widget.onSubmitted?.call('');
setState(() {
_hasText = false;
});
+1 -1
View File
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.2.52+52
version: 1.4.3+3
environment:
sdk: ^3.11.5
+31
View File
@@ -7,6 +7,29 @@
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
// Prevent multiple instances from running at the same time.
HANDLE single_instance_mutex =
::CreateMutex(nullptr, TRUE, L"TIS_RMS_SingleInstance_Mutex");
if (::GetLastError() == ERROR_ALREADY_EXISTS) {
// Bring the already-running instance to the foreground.
HWND existing_window = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW",
L"TIS Record Management System");
if (!existing_window) {
existing_window =
::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", nullptr);
}
if (existing_window) {
if (::IsIconic(existing_window)) {
::ShowWindow(existing_window, SW_RESTORE);
}
::SetForegroundWindow(existing_window);
}
if (single_instance_mutex) {
::CloseHandle(single_instance_mutex);
}
return EXIT_SUCCESS;
}
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {
@@ -28,6 +51,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
Win32Window::Point origin(10, 10);
Win32Window::Size size(1280, 720);
if (!window.Create(L"TIS Record Management System", origin, size)) {
if (single_instance_mutex) {
::ReleaseMutex(single_instance_mutex);
::CloseHandle(single_instance_mutex);
}
return EXIT_FAILURE;
}
window.SetQuitOnClose(true);
@@ -39,5 +66,9 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
}
::CoUninitialize();
if (single_instance_mutex) {
::ReleaseMutex(single_instance_mutex);
::CloseHandle(single_instance_mutex);
}
return EXIT_SUCCESS;
}