Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0cf59ef99 | ||
|
|
79a1dd5bb2 | ||
|
|
b7040bff4d |
No files matched your search
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user