feat: enhance deped reports pdf/excel, dashboard rotation, user creation, and history search shortcut

- Reports: migrate compliance export to xlsx, fix android chart tooltip headroom, add deped collapsible section descriptions, and complete 3-page DepEd PDF export

- Dashboard: continuous document breakdown rotation on mobile, persistent setup banner

- Users: automated background password generation, standardized action button dimensions

- History: add Ctrl+F search shortcut with autofocus cursor in search dialog
This commit is contained in:
ssbg04 committed 2026-10-02 17:02:04 +08:00
1 parent a74e0dc7fe
commit 3a3e0dc7b8
12 files changed
+896 -332

No files matched your search

+20 -2
View File
@@ -1011,7 +1011,7 @@ Files backup/restore - mirror mode or zip -->
- [x] remove Ctrl + A for filter (removed from documentation shortcuts table) -->
# HISTORY SCREEN [DONE]
<!-- # HISTORY SCREEN [DONE]
- [x] PRINTED_HISTORY card has a overflowed for date and time
- Made card headers in `audit_trail_screen.dart` responsive using `Wrap` layout so badges, entity types, and timestamps never overflow on any screen size.
@@ -1019,8 +1019,26 @@ Files backup/restore - mirror mode or zip -->
- [x] fix FAQs make sure its on the current system features (added Socket.IO real-time sync, port 18484, 10-char randomized temporary admin passwords, OCR data extraction separated from raw folder upload, server-side lossless WebP PDF previews)
- [x] connect the documentation based on the current system features (updated port 18484, real-time Socket.IO event broadcasts across LAN, and accurate keyboard shortcuts)
- [x] dont speficy the android is for tablet just android (replaced all references of "Android Tablet", "tablet terminals", and "touch tablet" with "Android" / "Android device")
- [x] redesign the entire documentation make it minimalist no extra design just make it normal documentation (removed fake window chrome titlebars/colored dots, removed simulated mobile phone bezels/notches, converted screenshots to standard figures with semantic figcaptions and zoom modals, streamlined typography and styling)
- [x] redesign the entire documentation make it minimalist no extra design just make it normal documentation (removed fake window chrome titlebars/colored dots, removed simulated mobile phone bezels/notches, converted screenshots to standard figures with semantic figcaptions and zoom modals, streamlined typography and styling) -->
<!--
# DASHBOARD SCREEN
- [x] auto play the document breakdown on android dont pause (enabled continuous auto-rotation on all platforms including Android)
- [x] do not hide the set up required (removed banner dismissal action so Setup Required banner remains permanently visible to administrators, with minimize/expand preserved)
# REPORTS SCREEN
- [x] export of compliance is csv make it on excel (migrated compliance report export from CSV to native Excel .xlsx with Summary and Student Compliance List sheets via excel package)
- [x] fix the graph in android app add huge space for graphs to fix the hover cannot read (increased chart heights, expanded topTitles reserved headroom, and enabled fitInsideHorizontally/Vertically for tooltips across all report and DepEd charts)
- [x] make the horizontal bar the same design in deped tab (standardized custom styled horizontal scrollbar across Dropout/Transferee and 4Ps equity charts matching the rest of the DepEd tab)
- [x] make the export pdf of deped tab include all graphs and tables like on in individual pdf export (implemented 3-page comprehensive DepEd PDF report with vector bar charts and full comparative, gender, dropout, transferee, retention, and 4Ps tables across both full and individual exports)
- [x] deped tab add collapsable short description each sections, default is closed (added animated collapsible info guide pills for Enrollment, Dropouts/Transferees, and 4Ps Equity sections defaulting to closed) -->
<!-- # USERS SCREEN
- [x] add new user make the button the same size (standardized Cancel and Create User buttons in the Add User modal with identical height 48px, consistent 14px typography, and matching border radius) -->
<!-- # HISTORY SCREEN
- [x] add keyboard shortcut Ctrl + F for search (implemented CallbackShortcuts and FocusNode in AuditTrailScreen to invoke search dialog on Ctrl+F) -->
---
@@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:calendar_date_picker2/calendar_date_picker2.dart';
import '../../../core/constants/app_colors.dart';
import '../../../core/utils/date_utils.dart' as pht;
import '../../providers/navigation_provider.dart';
import '../../providers/dashboard_provider.dart';
import '../../providers/activity_provider.dart';
import '../../providers/login_sessions_provider.dart';
@@ -29,6 +31,9 @@ class AuditTrailScreen extends ConsumerStatefulWidget {
class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
late AuditTab _selectedTab;
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();
final FocusNode _shortcutFocusNode = FocusNode();
ProviderSubscription<String>? _tabListener;
DateTime? _fromDate;
DateTime? _toDate;
@@ -40,11 +45,35 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
_selectedTab = widget.userRole == 'teacher'
? AuditTab.activities
: widget.initialTab;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
if (ref.read(activeTabProvider) == 'History') {
_shortcutFocusNode.requestFocus();
}
_tabListener = ref.listenManual<String>(activeTabProvider, (
previous,
next,
) {
if (!mounted) return;
if (next == 'History') {
Future.delayed(const Duration(milliseconds: 120), () {
if (mounted) {
_shortcutFocusNode.requestFocus();
}
});
}
});
});
}
@override
void dispose() {
_tabListener?.close();
_searchController.dispose();
_searchFocusNode.dispose();
_shortcutFocusNode.dispose();
super.dispose();
}
@@ -149,6 +178,19 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
? 'Search user or admin...'
: 'Search username or name...';
if (_searchController.text.isNotEmpty) {
_searchController.selection = TextSelection(
baseOffset: 0,
extentOffset: _searchController.text.length,
);
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_searchFocusNode.requestFocus();
}
});
await showDialog(
context: context,
barrierColor: Colors.black54,
@@ -164,6 +206,8 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
child: AppSearchBar(
hint: hint,
controller: _searchController,
focusNode: _searchFocusNode,
autofocus: true,
collapsible: false,
maxWidth: 600,
onSubmitted: (val) {
@@ -179,7 +223,11 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
);
},
);
if (mounted) setState(() {});
if (mounted) {
_searchFocusNode.unfocus();
_shortcutFocusNode.requestFocus();
setState(() {});
}
}
Future<void> _showFilterDialog(BuildContext context) async {
@@ -546,6 +594,9 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
},
),
);
if (mounted) {
_shortcutFocusNode.requestFocus();
}
}
Widget _buildTabSwitcher(bool isDark, {bool isMobile = false}) {
@@ -746,10 +797,19 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
final hasActiveFilters = activeFilterCount > 0;
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: SafeArea(
child: Column(
return CallbackShortcuts(
bindings: <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.keyF, control: true): () {
_showSearchDialog(context);
},
},
child: Focus(
focusNode: _shortcutFocusNode,
autofocus: true,
child: Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ── Compact Header & Controls ─────────────────────────────
@@ -1005,7 +1065,9 @@ class _AuditTrailScreenState extends ConsumerState<AuditTrailScreen> {
],
),
),
);
),
),
);
}
Widget _buildTabItem({
@@ -33,7 +33,6 @@ class DashboardScreen extends ConsumerStatefulWidget {
}
class _DashboardScreenState extends ConsumerState<DashboardScreen> {
bool _setupBannerDismissed = false;
bool _setupBannerMinimized = false;
final TextEditingController _searchController = TextEditingController();
final FocusNode _searchFocusNode = FocusNode();
@@ -97,7 +96,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
if (!mounted) return;
if (next == 'Dashboard' && previous != 'Dashboard') {
setState(() {
_setupBannerDismissed = false;
_setupBannerMinimized = true;
});
if (_searchFocusNode.hasFocus) _searchFocusNode.unfocus();
@@ -369,7 +367,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
),
),
const SizedBox(height: 16),
if (isAdmin && !_setupBannerDismissed) ...[
if (isAdmin) ...[
_buildSetupGuidanceBanner(context),
const SizedBox(height: 24),
],
@@ -551,17 +549,6 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
IconButton(
onPressed: () =>
setState(() => _setupBannerDismissed = true),
icon: Icon(
Icons.close,
size: 18,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.38),
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
if (!_setupBannerMinimized) ...[
@@ -643,38 +630,21 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
],
),
),
// Minimize and Dismiss buttons
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
onPressed: () => setState(
() =>
_setupBannerMinimized = !_setupBannerMinimized,
),
icon: Icon(
_setupBannerMinimized
? Icons.keyboard_arrow_down
: Icons.keyboard_arrow_up,
size: 18,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.38),
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
const SizedBox(width: 8),
IconButton(
onPressed: () =>
setState(() => _setupBannerDismissed = true),
icon: Icon(
Icons.close,
size: 18,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.38),
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
// Minimize toggle button (never dismiss)
IconButton(
onPressed: () => setState(
() =>
_setupBannerMinimized = !_setupBannerMinimized,
),
icon: Icon(
_setupBannerMinimized
? Icons.keyboard_arrow_down
: Icons.keyboard_arrow_up,
size: 18,
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.38),
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
],
),
@@ -628,9 +628,8 @@ class _DocTypePieCardState extends State<_DocTypePieCard>
if (names.isNotEmpty) {
_selectedDocType = names.first;
}
// Disable continuous 60fps auto-rotation animation ticker on mobile/Android to prevent jank & save battery
final isMobile = defaultTargetPlatform == TargetPlatform.android;
_isAutoRotating = !isMobile;
// Auto-play document breakdown rotation across all platforms including Android
_isAutoRotating = true;
_countdownController = AnimationController(
vsync: this,
@@ -1,8 +1,8 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:file_picker/file_picker.dart';
import 'package:excel/excel.dart' as xl;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/constants/app_colors.dart';
@@ -125,10 +125,10 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
.yearRange
: 'All Years';
// Build CSV bytes using the filtered data
final bytes = _buildCsv(data, yearLabel);
// Build Excel (.xlsx) bytes using the filtered data
final bytes = _buildExcel(data, yearLabel);
final defaultFileName =
'TIS_RMS_Report_${yearLabel.replaceAll('-', '_')}_${DateTime.now().millisecondsSinceEpoch}.csv';
'TIS_RMS_Report_${yearLabel.replaceAll('-', '_')}_${DateTime.now().millisecondsSinceEpoch}.xlsx';
if (!mounted) return;
setState(() => _isExporting = false);
@@ -204,7 +204,7 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
showSuccessDialog(
context,
title: 'Export Successful',
message: 'Report has been exported successfully.',
message: 'Report has been exported to Excel (.xlsx) successfully.',
filePath: savedPath,
);
}
@@ -222,10 +222,10 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
Future<String?> _saveExcelFile(List<int> bytes, String fileName) async {
if (Platform.isWindows) {
final savePath = await FilePicker.saveFile(
dialogTitle: 'Save Report As...',
dialogTitle: 'Save Report As Excel...',
fileName: fileName,
type: FileType.custom,
allowedExtensions: ['csv'],
allowedExtensions: ['xlsx', 'xls'],
);
if (savePath == null) return null;
final file = File(savePath);
@@ -239,72 +239,69 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
}
}
List<int> _buildCsv(ReportStats data, String yearLabel) {
final buffer = StringBuffer();
// UTF-8 BOM so Microsoft Excel correctly parses UTF-8 characters
buffer.write('\uFEFF');
List<int> _buildExcel(ReportStats data, String yearLabel) {
final excel = xl.Excel.createExcel();
String escape(String value) {
if (value.contains(',') || value.contains('"') || value.contains('\n') || value.contains('\r')) {
return '"${value.replaceAll('"', '""')}"';
}
return value;
}
// Sheet 1: Summary
final summarySheet = excel['Summary'];
excel.setDefaultSheet('Summary');
// Title Section
buffer.writeln(escape('TIAONG INTEGRATED SCHOOL - TIS RMS'));
buffer.writeln(escape('Annual Report Summary: $yearLabel'));
buffer.writeln(escape('Generated: ${DateTime.now().toString().substring(0, 19)}'));
buffer.writeln();
summarySheet.appendRow([xl.TextCellValue('TIAONG INTEGRATED SCHOOL - TIS RMS')]);
summarySheet.appendRow([xl.TextCellValue('Annual Report Summary: $yearLabel')]);
summarySheet.appendRow([xl.TextCellValue('Generated: ${DateTime.now().toString().substring(0, 19)}')]);
summarySheet.appendRow([xl.TextCellValue('')]);
// Summary Section
buffer.writeln(escape('STUDENT STATISTICS'));
buffer.writeln('${escape('Student Status')},${escape('Total Count')}');
buffer.writeln('${escape('Active (Enrolled)')},${data.studentCounts.active}');
buffer.writeln('${escape('Dropouts (Dropped)')},${data.studentCounts.dropped}');
buffer.writeln('${escape('Transferees (Transferred)')},${data.studentCounts.transferee}');
buffer.writeln('${escape('Graduated')},${data.studentCounts.graduated}');
buffer.writeln();
summarySheet.appendRow([xl.TextCellValue('STUDENT STATISTICS'), xl.TextCellValue('')]);
summarySheet.appendRow([xl.TextCellValue('Student Status'), xl.TextCellValue('Total Count')]);
summarySheet.appendRow([xl.TextCellValue('Active (Enrolled)'), xl.IntCellValue(data.studentCounts.active)]);
summarySheet.appendRow([xl.TextCellValue('Dropouts (Dropped)'), xl.IntCellValue(data.studentCounts.dropped)]);
summarySheet.appendRow([xl.TextCellValue('Transferees (Transferred)'), xl.IntCellValue(data.studentCounts.transferee)]);
summarySheet.appendRow([xl.TextCellValue('Graduated'), xl.IntCellValue(data.studentCounts.graduated)]);
summarySheet.appendRow([xl.TextCellValue('')]);
buffer.writeln(escape('MISSING DOCUMENTS PER REQUIREMENT TYPE'));
buffer.writeln('${escape('Document Type')},${escape('Missing Count')}');
summarySheet.appendRow([xl.TextCellValue('MISSING DOCUMENTS PER REQUIREMENT TYPE'), xl.TextCellValue('')]);
summarySheet.appendRow([xl.TextCellValue('Document Type'), xl.TextCellValue('Missing Count')]);
for (final row in data.missingDocsBreakdown) {
buffer.writeln('${escape(row.name)},${row.count}');
summarySheet.appendRow([xl.TextCellValue(row.name), xl.IntCellValue(row.count)]);
}
buffer.writeln();
// Masterlist Section
buffer.writeln(escape('STUDENT COMPLIANCE REPORT - $yearLabel'));
final headers = [
'#',
'LRN',
'Student Name',
'Sex',
'Grade Level',
'Section',
'Status',
'Missing Count',
'Missing Documents',
];
buffer.writeln(headers.map(escape).join(','));
// Sheet 2: Student Compliance List
final listSheet = excel['Student Compliance List'];
listSheet.appendRow([
xl.TextCellValue('#'),
xl.TextCellValue('LRN'),
xl.TextCellValue('Student Name'),
xl.TextCellValue('Sex'),
xl.TextCellValue('Grade Level'),
xl.TextCellValue('Section'),
xl.TextCellValue('Status'),
xl.TextCellValue('Missing Count'),
xl.TextCellValue('Missing Documents'),
]);
for (int r = 0; r < data.students.length; r++) {
final s = data.students[r];
final row = [
(r + 1).toString(),
s.lrn,
s.fullName,
s.sex,
s.gradeLevel != null ? 'Grade ${s.gradeLevel}' : 'N/A',
s.sectionName ?? 'N/A',
s.status,
s.missingCount.toString(),
s.missingRequirements ?? 'None',
];
buffer.writeln(row.map(escape).join(','));
final missingDocsStr = s.missingRequirements ?? (s.missingCount == 0 ? 'None (Complete)' : 'N/A');
listSheet.appendRow([
xl.IntCellValue(r + 1),
xl.TextCellValue(s.lrn),
xl.TextCellValue(s.fullName),
xl.TextCellValue(s.sex),
xl.TextCellValue(s.gradeLevel != null ? 'Grade ${s.gradeLevel}' : 'N/A'),
xl.TextCellValue(s.sectionName ?? 'N/A'),
xl.TextCellValue(s.status),
xl.IntCellValue(s.missingCount),
xl.TextCellValue(missingDocsStr),
]);
}
return utf8.encode(buffer.toString());
// Delete default Sheet1 if present
if (excel.sheets.containsKey('Sheet1')) {
excel.delete('Sheet1');
}
final bytes = excel.save();
return bytes ?? [];
}
// ── Print Compliance Report Dialog ─────────────────────────────────────────
@@ -3501,10 +3498,10 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
final Widget chartWidget = SizedBox(
width: chartWidth,
height: isDesktop ? 240 : 220,
height: isDesktop ? 260 : 300,
child: BarChart(
BarChartData(
maxY: 100,
maxY: 140,
barGroups: grades.asMap().entries.map((entry) {
final g = entry.value;
final pct = g.total > 0
@@ -3582,7 +3579,7 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
sideTitles: SideTitles(showTitles: false, reservedSize: 32),
),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false),
@@ -3599,7 +3596,16 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
),
borderData: FlBorderData(show: false),
barTouchData: BarTouchData(
enabled: true,
handleBuiltInTouches: true,
touchTooltipData: BarTouchTooltipData(
fitInsideHorizontally: true,
fitInsideVertically: true,
tooltipMargin: 12,
tooltipPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
),
getTooltipColor: (group) => isDark
? AppColors.darkSurface2
: Colors.blueGrey.shade800,
@@ -3957,7 +3963,7 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
if (val > maxY) maxY = val;
}
}
maxY = maxY * 1.2;
maxY = (maxY * 1.55).ceilToDouble();
if (maxY == 0) maxY = 10;
return LayoutBuilder(
@@ -3968,13 +3974,22 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
: (data.length * 150.0);
final Widget chartWidget = SizedBox(
width: chartWidth,
height: 200,
height: isDesktop ? 260 : 300,
child: BarChart(
BarChartData(
alignment: BarChartAlignment.spaceAround,
maxY: maxY,
barTouchData: BarTouchData(
enabled: true,
handleBuiltInTouches: true,
touchTooltipData: BarTouchTooltipData(
fitInsideHorizontally: true,
fitInsideVertically: true,
tooltipMargin: 12,
tooltipPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
),
getTooltipColor: (group) => isDark
? AppColors.darkSurface2
: Colors.blueGrey.shade800,
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/constants/app_colors.dart';
import '../../../../core/constants/app_sizes.dart';
import '../../../../core/services/haptic_service.dart';
import '../../../../core/utils/download_service.dart';
import '../../../../domain/entities/report_models.dart';
import '../../../providers/reports_provider.dart';
@@ -499,12 +500,15 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
// ── Standalone Section Card Container ─────────────────────────────────────
// ── Standalone Section Card Container ─────────────────────────────────────
Widget _buildSectionCardContainer({
required BuildContext context,
required bool isDark,
required IconData icon,
required Color iconColor,
required String title,
String? description,
Widget? trailing,
required Widget content,
}) {
@@ -578,6 +582,11 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
);
},
),
if (description != null && description.isNotEmpty)
_CollapsibleSectionDescription(
description: description,
isDark: isDark,
),
const SizedBox(height: AppSizes.p16),
content,
],
@@ -600,6 +609,8 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
icon: Icons.bar_chart_rounded,
iconColor: AppColors.primaryGreen,
title: 'Data on Enrollment',
description:
'Presents official learner enrollment metrics across Key Stages 3 & 4 (Grades 7–12), comparative multi-year trends against previous school years, and gender disaggregation (male vs. female shares) following DepEd BEIS reporting standards.',
trailing: Wrap(
spacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
@@ -644,6 +655,8 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
icon: Icons.trending_down_rounded,
iconColor: Colors.redAccent,
title: 'Dropouts & Transferees',
description:
'Monitors learner retention and mobility across grade levels, tracking school leavers (dropouts) and learner transfers to calculate net mobility balance and internal institutional efficiency.',
trailing: _buildCategoryExportButton(data, category: 'dropouts_transferees'),
content: _buildDropoutTransfereeSection(context, years),
);
@@ -664,6 +677,8 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
icon: Icons.family_restroom_rounded,
iconColor: Colors.deepPurple,
title: '4Ps Beneficiaries',
description:
'Analyzes Pantawid Pamilyang Pilipino Program (4Ps) learner coverage and social protection equity indicators across Junior and Senior High School grade levels.',
trailing: Wrap(
spacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
@@ -1050,7 +1065,7 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
BarChartData(
alignment: BarChartAlignment.spaceAround,
groupsSpace: 24,
maxY: (maxVal * 1.45).ceilToDouble(),
maxY: (maxVal * 1.65).ceilToDouble(),
barTouchData: BarTouchData(
enabled: true,
handleBuiltInTouches: true,
@@ -1133,7 +1148,7 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
topTitles: const AxisTitles(
sideTitles: SideTitles(
showTitles: false,
reservedSize: 20,
reservedSize: 34,
),
),
rightTitles: const AxisTitles(
@@ -2536,13 +2551,18 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
BarChartData(
alignment: BarChartAlignment.spaceAround,
groupsSpace: 20,
maxY: (maxVal * 1.35).ceilToDouble(),
maxY: (maxVal * 1.65).ceilToDouble(),
barTouchData: BarTouchData(
enabled: true,
handleBuiltInTouches: true,
touchTooltipData: BarTouchTooltipData(
fitInsideHorizontally: true,
fitInsideVertically: true,
tooltipMargin: 12,
tooltipPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
),
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final grade = grades[group.x.toInt()];
final isDrop = rodIndex == 0;
@@ -2560,7 +2580,7 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
),
),
titlesData: FlTitlesData(
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false, reservedSize: 34)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
@@ -2671,17 +2691,24 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
SizedBox(
height: isNarrow ? 360 : 320,
child: isNarrow
? Scrollbar(
? SingleChildScrollView(
controller: _dropoutChartScrollController,
thumbVisibility: true,
child: SingleChildScrollView(
controller: _dropoutChartScrollController,
scrollDirection: Axis.horizontal,
child: chartWidget,
),
scrollDirection: Axis.horizontal,
child: chartWidget,
)
: chartWidget,
),
if (isNarrow) ...[
const SizedBox(height: 6),
_CustomHorizontalScrollBar(
controller: _dropoutChartScrollController,
isDark: isDark,
),
_buildTableScrollHint(
context,
text: 'Scroll horizontally to view all grades',
),
],
],
);
},
@@ -2719,13 +2746,18 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
BarChartData(
alignment: BarChartAlignment.spaceAround,
groupsSpace: 20,
maxY: (maxVal * 1.3).ceilToDouble(),
maxY: (maxVal * 1.65).ceilToDouble(),
barTouchData: BarTouchData(
enabled: true,
handleBuiltInTouches: true,
touchTooltipData: BarTouchTooltipData(
fitInsideHorizontally: true,
fitInsideVertically: true,
tooltipMargin: 12,
tooltipPadding: const EdgeInsets.symmetric(
horizontal: 14,
vertical: 8,
),
getTooltipItem: (group, groupIndex, rod, rodIndex) {
final grade = grades[group.x.toInt()];
final is4Ps = rodIndex == 0;
@@ -2743,7 +2775,7 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
),
),
titlesData: FlTitlesData(
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false, reservedSize: 34)),
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
@@ -2829,17 +2861,24 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
SizedBox(
height: isNarrow ? 360 : 320,
child: isNarrow
? Scrollbar(
? SingleChildScrollView(
controller: _equity4PsChartScrollController,
thumbVisibility: true,
child: SingleChildScrollView(
controller: _equity4PsChartScrollController,
scrollDirection: Axis.horizontal,
child: chartWidget,
),
scrollDirection: Axis.horizontal,
child: chartWidget,
)
: chartWidget,
),
if (isNarrow) ...[
const SizedBox(height: 6),
_CustomHorizontalScrollBar(
controller: _equity4PsChartScrollController,
isDark: isDark,
),
_buildTableScrollHint(
context,
text: 'Scroll horizontally to view all grades',
),
],
],
);
},
@@ -2937,6 +2976,113 @@ class _StatefulResponsiveTableState extends State<_StatefulResponsiveTable> {
}
}
// ── Collapsible Section Description (Default Closed) ─────────────────────────
class _CollapsibleSectionDescription extends StatefulWidget {
final String description;
final bool isDark;
const _CollapsibleSectionDescription({
required this.description,
required this.isDark,
});
@override
State<_CollapsibleSectionDescription> createState() =>
_CollapsibleSectionDescriptionState();
}
class _CollapsibleSectionDescriptionState
extends State<_CollapsibleSectionDescription> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
final isDark = widget.isDark;
return Container(
margin: const EdgeInsets.only(top: 8, bottom: 4),
decoration: BoxDecoration(
color: isDark
? AppColors.darkSurface2.withValues(alpha: 0.45)
: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isDark
? AppColors.darkBorder.withValues(alpha: 0.55)
: const Color(0xFFE2E8F0),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
onTap: () {
HapticService.light();
setState(() => _isExpanded = !_isExpanded);
},
borderRadius: BorderRadius.circular(10),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Row(
children: [
Icon(
Icons.info_outline_rounded,
size: 15,
color: isDark
? AppColors.darkTextSecondary
: AppColors.textSecondary,
),
const SizedBox(width: 8),
Expanded(
child: Text(
'About this section',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: isDark
? AppColors.darkTextSecondary
: AppColors.textSecondary,
),
),
),
Icon(
_isExpanded
? Icons.keyboard_arrow_up_rounded
: Icons.keyboard_arrow_down_rounded,
size: 18,
color: isDark
? AppColors.darkTextSecondary
: AppColors.textSecondary,
),
],
),
),
),
AnimatedCrossFade(
firstChild: const SizedBox(width: double.infinity, height: 0),
secondChild: Padding(
padding: const EdgeInsets.fromLTRB(14, 0, 14, 12),
child: Text(
widget.description,
style: TextStyle(
fontSize: 12.5,
height: 1.45,
color: isDark
? AppColors.darkTextPrimary.withValues(alpha: 0.85)
: const Color(0xFF334155),
),
),
),
crossFadeState: _isExpanded
? CrossFadeState.showSecond
: CrossFadeState.showFirst,
duration: const Duration(milliseconds: 200),
),
],
),
);
}
}
// ── Custom Dedicated Horizontal Scrollbar Under Graphs & Tables ──────────────
class _CustomHorizontalScrollBar extends StatefulWidget {
final ScrollController controller;
+48 -30
View File
@@ -1850,25 +1850,37 @@ class _UserDetailAndEditModalContentState
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: _cancelEditing,
style: OutlinedButton.styleFrom(
minimumSize: const Size.fromHeight(50),
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 14,
child: SizedBox(
height: 48,
child: OutlinedButton(
onPressed: _cancelEditing,
style: OutlinedButton.styleFrom(
foregroundColor: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary,
side: BorderSide(
color: isDark ? AppColors.darkBorder : const Color(0xFFCBD5E1),
width: 1.2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
child: const Text(
'Cancel',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
),
),
child: const Text('Cancel'),
),
),
const SizedBox(width: 12),
Expanded(
child: PrimaryButton(
label: 'Save',
height: 48,
fontSize: 14,
isLoading: _isLoading,
onPressed: _handleSave,
),
@@ -2006,7 +2018,7 @@ class _AddUserModalContentState extends ConsumerState<_AddUserModalContent> {
}
final username = _usernameCtrl.text.trim();
final tempPassword = await ref.read(usersProvider.notifier).createUser(
await ref.read(usersProvider.notifier).createUser(
username: username,
firstName: _firstNameCtrl.text.trim(),
middleName: _middleNameCtrl.text.trim(),
@@ -2022,7 +2034,7 @@ class _AddUserModalContentState extends ConsumerState<_AddUserModalContent> {
_showUserCreatedSuccessDialog(
context,
username: username,
temporaryPassword: tempPassword,
role: _selectedRole,
email: _emailCtrl.text.trim(),
);
} catch (e) {
@@ -2280,24 +2292,37 @@ class _AddUserModalContentState extends ConsumerState<_AddUserModalContent> {
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(false),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 14,
child: SizedBox(
height: 48,
child: OutlinedButton(
onPressed: () => Navigator.of(context).pop(false),
style: OutlinedButton.styleFrom(
foregroundColor: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary,
side: BorderSide(
color: isDark ? AppColors.darkBorder : const Color(0xFFCBD5E1),
width: 1.2,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
child: const Text(
'Cancel',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
letterSpacing: 0.3,
),
),
),
child: const Text('Cancel'),
),
),
const SizedBox(width: 12),
Expanded(
child: PrimaryButton(
label: 'CREATE USER',
height: 48,
fontSize: 14,
isLoading: _isLoading,
onPressed: _handleCreate,
),
@@ -2402,7 +2427,7 @@ Widget _buildEmailDomainSuggestions(
void _showUserCreatedSuccessDialog(
BuildContext ctx, {
required String username,
required String temporaryPassword,
String? role,
String? email,
}) {
final isDark = Theme.of(ctx).brightness == Brightness.dark;
@@ -2428,14 +2453,7 @@ void _showUserCreatedSuccessDialog(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_credentialRow(dialogCtx, 'Username', username),
const SizedBox(height: 10),
_credentialRow(
dialogCtx,
'Temporary Password',
temporaryPassword,
highlight: true,
),
const SizedBox(height: 16),
const SizedBox(height: 14),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
@@ -15,6 +15,7 @@ class AppSearchBar extends ConsumerStatefulWidget {
final bool enableHistory;
final bool collapsible;
final bool hideIconWhenExpanded;
final bool autofocus;
const AppSearchBar({
super.key,
@@ -28,6 +29,7 @@ class AppSearchBar extends ConsumerStatefulWidget {
this.enableHistory = true,
this.collapsible = false,
this.hideIconWhenExpanded = false,
this.autofocus = false,
});
@override
@@ -87,6 +89,13 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
_isExpanded = !widget.collapsible || _controller.text.trim().isNotEmpty;
_controller.addListener(_onTextChanged);
_focusNode.addListener(_onFocusChanged);
if (widget.autofocus) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_focusNode.requestFocus();
}
});
}
}
void _onTextChanged() {
@@ -482,6 +491,7 @@ class _AppSearchBarState extends ConsumerState<AppSearchBar> {
child: TextField(
controller: _controller,
focusNode: _focusNode,
autofocus: widget.autofocus,
textInputAction: TextInputAction.search,
onSubmitted: _handleSubmit,
onChanged: widget.onChanged,
+8
View File
@@ -305,6 +305,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.8"
excel:
dependency: "direct main"
description:
name: excel
sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780"
url: "https://pub.dev"
source: hosted
version: "4.0.6"
fake_async:
dependency: transitive
description:
+2 -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.0.31+31
version: 1.0.32+32
environment:
sdk: ^3.11.5
@@ -60,6 +60,7 @@ dependencies:
audioplayers: ^6.8.1
package_info_plus: ^8.1.0
device_info_plus: ^12.4.0
excel: ^4.0.6
dev_dependencies:
flutter_test: