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:
ssbg04 committed 2026-10-09 13:20:51 +08:00
1 parent c8c26240c9
commit 4d95dbc0fc
6 files changed
+1360 -328

No files matched your search

+2 -2
View File
@@ -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