feat(import): implement Priority 5 4-step OCR wizard with disambiguation; fix(ui): sync dashboard card heights, fix needs-attention overflow, and remove student list fade
This commit is contained in:
1 parent
c8c26240c9
commit
4d95dbc0fc
6 files changed
+1360
-328
No files matched your search
+2
-2
@@ -1073,8 +1073,8 @@ Files backup/restore - mirror mode or zip -->
|
||||
- [x] Add simplified recent operational activity feed linking directly to student targets.
|
||||
|
||||
## Priority 5: OCR / Import Review Flow
|
||||
- [ ] Implement 4-step wizard UI: Select Files → Processing → Review & Match Results → Confirm Commit.
|
||||
- [ ] Allow manual student disambiguation on ambiguous or missing matches before database insertion.
|
||||
- [x] Implement 4-step wizard UI: Select Files → Processing → Review & Match Results → Confirm Commit.
|
||||
- [x] Allow manual student disambiguation on ambiguous or missing matches before database insertion.
|
||||
|
||||
## Priority 6: Task-Specific Reports
|
||||
- [ ] Refactor Reports into categorized tasks (Student Masterlists, Document Completion, School Forms SF9/SF10).
|
||||
|
||||
@@ -386,13 +386,15 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth >= 900) {
|
||||
return const Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(flex: 6, child: NeedsAttentionCard()),
|
||||
SizedBox(width: 20),
|
||||
Expanded(flex: 5, child: RecentActivityFeed()),
|
||||
],
|
||||
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(
|
||||
|
||||
@@ -124,12 +124,15 @@ class NeedsAttentionCard extends ConsumerWidget {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Needs Attention',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : AppColors.textPrimary,
|
||||
Flexible(
|
||||
child: Text(
|
||||
'Needs Attention',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (totalAlerts > 0) ...[
|
||||
@@ -274,21 +277,21 @@ class NeedsAttentionCard extends ConsumerWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
itemCount: displayedStudents.length,
|
||||
separatorBuilder: (_, _) => const SizedBox(height: 6),
|
||||
itemBuilder: (context, i) {
|
||||
final s = displayedStudents[i];
|
||||
return _buildStudentTile(
|
||||
context: context,
|
||||
student: s,
|
||||
userRole: userRole,
|
||||
isDark: isDark,
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < displayedStudents.length; i++) ...[
|
||||
if (i > 0) const SizedBox(height: 6),
|
||||
_buildStudentTile(
|
||||
context: context,
|
||||
student: displayedStudents[i],
|
||||
userRole: userRole,
|
||||
isDark: isDark,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
] else ...[
|
||||
@@ -365,6 +368,8 @@ class NeedsAttentionCard extends ConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -461,19 +466,23 @@ class NeedsAttentionCard extends ConsumerWidget {
|
||||
color: isDark ? Colors.white : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
const SizedBox(height: 3),
|
||||
Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 2,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
for (final doc in missingList) ...[
|
||||
for (final doc in missingList)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(right: 4),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
'! $doc',
|
||||
'! ${_formatDocTag(doc)}',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -481,8 +490,7 @@ class NeedsAttentionCard extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (extraCount > 0) ...[
|
||||
if (extraCount > 0)
|
||||
Text(
|
||||
'+$extraCount more',
|
||||
style: TextStyle(
|
||||
@@ -490,7 +498,6 @@ class NeedsAttentionCard extends ConsumerWidget {
|
||||
color: isDark ? Colors.white54 : Colors.black54,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -530,6 +537,15 @@ class NeedsAttentionCard extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDocTag(String doc) {
|
||||
final upper = doc.toUpperCase();
|
||||
if (upper.contains('SF9') || upper.contains('FORM 9')) return 'SF9';
|
||||
if (upper.contains('SF10') || upper.contains('FORM 10')) return 'SF10';
|
||||
if (upper.contains('PSA') || upper.contains('BIRTH')) return 'PSA';
|
||||
if (upper.contains('GOOD MORAL')) return 'Good Moral';
|
||||
return doc.length > 15 ? '${doc.substring(0, 14)}…' : doc;
|
||||
}
|
||||
|
||||
void _openMissingDocSheet(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
|
||||
@@ -141,24 +141,25 @@ class RecentActivityFeed extends ConsumerWidget {
|
||||
),
|
||||
)
|
||||
else
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
itemCount: displayedActivities.length,
|
||||
separatorBuilder: (_, _) => Divider(
|
||||
height: 1,
|
||||
color: isDark ? AppColors.darkBorder : Colors.grey.shade100,
|
||||
child: Column(
|
||||
children: [
|
||||
for (int i = 0; i < displayedActivities.length; i++) ...[
|
||||
if (i > 0)
|
||||
Divider(
|
||||
height: 1,
|
||||
color: isDark ? AppColors.darkBorder : Colors.grey.shade100,
|
||||
),
|
||||
_buildActivityItem(
|
||||
context: context,
|
||||
activity: displayedActivities[i],
|
||||
userRole: userRole,
|
||||
isDark: isDark,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
itemBuilder: (context, i) {
|
||||
final activity = displayedActivities[i];
|
||||
return _buildActivityItem(
|
||||
context: context,
|
||||
activity: activity,
|
||||
userRole: userRole,
|
||||
isDark: isDark,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -54,22 +54,6 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
StudentViewTab _viewTab = StudentViewTab.enrolled;
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
bool _showTopFade = false;
|
||||
bool _showBottomFade = false;
|
||||
|
||||
void _onScroll() {
|
||||
if (!_scrollController.hasClients) return;
|
||||
final offset = _scrollController.offset;
|
||||
final maxScroll = _scrollController.position.maxScrollExtent;
|
||||
final showTop = offset > 4;
|
||||
final showBottom = maxScroll > 0 && offset < (maxScroll - 4);
|
||||
if (showTop != _showTopFade || showBottom != _showBottomFade) {
|
||||
setState(() {
|
||||
_showTopFade = showTop;
|
||||
_showBottomFade = showBottom;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _updateSelection(void Function() updateFn) {
|
||||
setState(updateFn);
|
||||
@@ -84,7 +68,6 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
_showMultiSelect = ref.read(studentMultiSelectProvider);
|
||||
_selectedStudentIds.addAll(ref.read(studentSelectedIdsProvider));
|
||||
|
||||
_scrollController.addListener(_onScroll);
|
||||
_searchController.addListener(_onSearchChanged);
|
||||
_searchFocusNode.addListener(_onSearchFocusChanged);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -152,7 +135,6 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.removeListener(_onScroll);
|
||||
_scrollController.dispose();
|
||||
_dragResetTimer?.cancel();
|
||||
_tabListener?.close();
|
||||
@@ -1207,64 +1189,10 @@ class _StudentsScreenState extends ConsumerState<StudentsScreen> {
|
||||
return LayoutBuilder(
|
||||
builder: (ctx, c) {
|
||||
final isDesktop = c.maxWidth > 800;
|
||||
return Stack(
|
||||
children: [
|
||||
_buildStudentCardList(
|
||||
sortedStudents,
|
||||
isDesktop: isDesktop,
|
||||
noSections: hasNoSections,
|
||||
),
|
||||
// Top scroll fade
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 20,
|
||||
child: IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
opacity: _showTopFade ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
Theme.of(context).scaffoldBackgroundColor.withValues(alpha: 0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom scroll fade
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 24,
|
||||
child: IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
opacity: _showBottomFade ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.bottomCenter,
|
||||
end: Alignment.topCenter,
|
||||
colors: [
|
||||
Theme.of(context).scaffoldBackgroundColor,
|
||||
Theme.of(context).scaffoldBackgroundColor.withValues(alpha: 0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
return _buildStudentCardList(
|
||||
sortedStudents,
|
||||
isDesktop: isDesktop,
|
||||
noSections: hasNoSections,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user