diff --git a/backend/src/controllers/reportsController.js b/backend/src/controllers/reportsController.js index bb9b97f..f5a1986 100644 --- a/backend/src/controllers/reportsController.js +++ b/backend/src/controllers/reportsController.js @@ -369,14 +369,54 @@ exports.getStorageUsed = async (req, res) => { // GET /api/reports/transparency-board exports.getTransparencyBoardData = (req, res) => { try { - const academicYears = db.prepare(` - SELECT id, year_range - FROM academic_years - WHERE id IN (SELECT DISTINCT academic_year_id FROM enrollments) - OR status = 'active' - ORDER BY year_range DESC - LIMIT 3 - `).all().reverse(); + const { academicYearId, yearIds } = req.query; + let academicYears = []; + + if (yearIds) { + const ids = yearIds.toString().split(',').map(id => Number(id.trim())).filter(id => !isNaN(id)); + if (ids.length > 0) { + const placeholders = ids.map(() => '?').join(','); + academicYears = db.prepare(` + SELECT id, year_range + FROM academic_years + WHERE id IN (${placeholders}) + ORDER BY year_range ASC + `).all(...ids); + } + } else if (academicYearId) { + const targetYear = db.prepare('SELECT id, year_range FROM academic_years WHERE id = ?').get(academicYearId); + if (targetYear) { + academicYears = db.prepare(` + SELECT id, year_range + FROM academic_years + WHERE year_range <= ? + ORDER BY year_range DESC + LIMIT 3 + `).all(targetYear.year_range).reverse(); + } + } + + if (academicYears.length === 0) { + const activeAy = db.prepare("SELECT id, year_range FROM academic_years WHERE status = 'active' LIMIT 1").get() + || db.prepare("SELECT id, year_range FROM academic_years ORDER BY year_range DESC LIMIT 1").get(); + + if (activeAy) { + academicYears = db.prepare(` + SELECT id, year_range + FROM academic_years + WHERE year_range <= ? + ORDER BY year_range DESC + LIMIT 3 + `).all(activeAy.year_range).reverse(); + } else { + academicYears = db.prepare(` + SELECT id, year_range + FROM academic_years + ORDER BY year_range DESC + LIMIT 3 + `).all().reverse(); + } + } const yearsData = academicYears.map(ay => { // Enrollment breakdown by sex & grade — COUNT DISTINCT students diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 75b300a..d9fada2 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -435,18 +435,33 @@ Files backup/restore - mirror mode or zip --> # STUDENT SCREEN - student profile modal -> make the lrn can be copy add icon button to automatically copy --> - + + + + + + +- Deped transparency board -> rephrase the the description use easy to understand -> make it simple and user friendly -> dont make it AI slop -> make it responsive +--> + --- # DONT DO + +- teacher no assigned section theme color +- bug in bulk add student enrollment + - recycle bin -> search history not delete history - add missing students by document type kpi @@ -455,7 +470,7 @@ Files backup/restore - mirror mode or zip --> - notification separate admin and teacher - bug in bulk add student enrollment -- report all export +- report all filter and export --- # TIS_RMS Server Manager (dont do) diff --git a/frontend/lib/domain/repositories/report_repository.dart b/frontend/lib/domain/repositories/report_repository.dart index 844b8d0..9a176c1 100644 --- a/frontend/lib/domain/repositories/report_repository.dart +++ b/frontend/lib/domain/repositories/report_repository.dart @@ -36,8 +36,9 @@ class ReportRepository { }) async { try { final queryParams = {}; - if (academicYearId != null) + if (academicYearId != null) { queryParams['academicYearId'] = academicYearId; + } if (gradeLevel != null) queryParams['gradeLevel'] = gradeLevel; if (sectionId != null) queryParams['sectionId'] = sectionId; if (status != null) queryParams['status'] = status; @@ -97,10 +98,20 @@ class ReportRepository { } } - Future getTransparencyBoardData() async { + Future getTransparencyBoardData({ + int? academicYearId, + List? yearIds, + }) async { try { + final queryParams = {}; + if (academicYearId != null) queryParams['academicYearId'] = academicYearId; + if (yearIds != null && yearIds.isNotEmpty) { + queryParams['yearIds'] = yearIds.join(','); + } + final res = await _dio.get( '/reports/transparency-board', + queryParameters: queryParams.isNotEmpty ? queryParams : null, options: await _authOptions(), ); return TransparencyBoardData.fromJson(res.data as Map); diff --git a/frontend/lib/ui/providers/reports_provider.dart b/frontend/lib/ui/providers/reports_provider.dart index d16786a..581460e 100644 --- a/frontend/lib/ui/providers/reports_provider.dart +++ b/frontend/lib/ui/providers/reports_provider.dart @@ -210,12 +210,16 @@ final storageStatsProvider = FutureProvider.autoDispose((ref) async { return await ref.read(reportRepositoryProvider).getStorageUsed(); }); +// Selected Academic Year for Transparency Board (null = default to active academic year) +final transparencyBoardYearProvider = StateProvider((ref) => null); + // DepEd Transparency Board Data provider final transparencyBoardProvider = FutureProvider.autoDispose((ref) async { + final selectedYearId = ref.watch(transparencyBoardYearProvider); return await ref .read(reportRepositoryProvider) - .getTransparencyBoardData(); + .getTransparencyBoardData(academicYearId: selectedYearId); }); diff --git a/frontend/lib/ui/screens/reports/reports_screen.dart b/frontend/lib/ui/screens/reports/reports_screen.dart index 856fc97..748dfdc 100644 --- a/frontend/lib/ui/screens/reports/reports_screen.dart +++ b/frontend/lib/ui/screens/reports/reports_screen.dart @@ -630,7 +630,7 @@ class _ReportsScreenState extends ConsumerState { index: 0, icon: Icons.dashboard_outlined, title: 'DepEd Transparency Board', - subtitle: 'Recommended DepEd metrics & equity', + subtitle: 'Enrollment, dropouts, & 4Ps records', ), _buildToggleItem( index: 1, diff --git a/frontend/lib/ui/screens/reports/widgets/transparency_board_section.dart b/frontend/lib/ui/screens/reports/widgets/transparency_board_section.dart index 17a67e1..6c9d345 100644 --- a/frontend/lib/ui/screens/reports/widgets/transparency_board_section.dart +++ b/frontend/lib/ui/screens/reports/widgets/transparency_board_section.dart @@ -38,15 +38,15 @@ class TransparencyBoardSection extends ConsumerWidget { } } -class _TransparencyBoardContent extends StatefulWidget { +class _TransparencyBoardContent extends ConsumerStatefulWidget { final TransparencyBoardData data; const _TransparencyBoardContent({required this.data}); @override - State<_TransparencyBoardContent> createState() => _TransparencyBoardContentState(); + ConsumerState<_TransparencyBoardContent> createState() => _TransparencyBoardContentState(); } -class _TransparencyBoardContentState extends State<_TransparencyBoardContent> { +class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardContent> { final ScrollController _enrollmentChartScrollController = ScrollController(); @override @@ -59,6 +59,8 @@ class _TransparencyBoardContentState extends State<_TransparencyBoardContent> { Widget build(BuildContext context) { final data = widget.data; final isDark = Theme.of(context).brightness == Brightness.dark; + final academicYears = ref.watch(academicYearsProvider).asData?.value ?? []; + final selectedYearId = ref.watch(transparencyBoardYearProvider); return Container( decoration: BoxDecoration( @@ -80,7 +82,7 @@ class _TransparencyBoardContentState extends State<_TransparencyBoardContent> { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // ── Header Banner ────────────────────────────────────────────────── + // ── Header Banner ────────────────────────────────────────── Container( padding: const EdgeInsets.all(AppSizes.p20), decoration: BoxDecoration( @@ -99,7 +101,7 @@ class _TransparencyBoardContentState extends State<_TransparencyBoardContent> { ), child: LayoutBuilder( builder: (context, headerConstraints) { - final isMobileHeader = headerConstraints.maxWidth < 650; + final isMobileHeader = headerConstraints.maxWidth < 750; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -128,10 +130,17 @@ class _TransparencyBoardContentState extends State<_TransparencyBoardContent> { const SizedBox(height: 8), Wrap( spacing: 8, - runSpacing: 4, + runSpacing: 6, + crossAxisAlignment: WrapCrossAlignment.center, children: [ _buildThemeChip('ACCESS', Colors.amber), _buildThemeChip('EQUITY', Colors.lightBlueAccent), + if (academicYears.isNotEmpty) + _buildYearSelector( + context, + academicYears: academicYears, + selectedYearId: selectedYearId, + ), ], ), ] else ...[ @@ -157,14 +166,62 @@ class _TransparencyBoardContentState extends State<_TransparencyBoardContent> { _buildThemeChip('ACCESS', Colors.amber), const SizedBox(width: 8), _buildThemeChip('EQUITY', Colors.lightBlueAccent), + if (academicYears.isNotEmpty) ...[ + const SizedBox(width: 12), + _buildYearSelector( + context, + academicYears: academicYears, + selectedYearId: selectedYearId, + ), + ], ], ), ], const SizedBox(height: 6), - const Text( - 'Comparative student population, mobility, and equity indicators across consecutive academic years.', - style: TextStyle(color: Colors.white70, fontSize: 13), + Text( + 'Track student enrollment, dropouts, transferees, and 4Ps learners across school years.', + style: TextStyle( + color: Colors.white.withValues(alpha: 0.85), + fontSize: isMobileHeader ? 12 : 13, + height: 1.35, + ), ), + if (data.years.isNotEmpty) ...[ + const SizedBox(height: 8), + Wrap( + spacing: 6, + runSpacing: 4, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Text( + 'Showing ${data.years.length} Consecutive Year${data.years.length > 1 ? 's' : ''}:', + style: const TextStyle( + color: Colors.white70, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + ...data.years.map((y) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.2), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.white.withValues(alpha: 0.3)), + ), + child: Text( + 'SY ${y.yearRange}', + style: const TextStyle( + color: Colors.white, + fontSize: 11, + fontWeight: FontWeight.w600, + ), + ), + ); + }), + ], + ), + ], ], ); }, @@ -232,7 +289,83 @@ class _TransparencyBoardContentState extends State<_TransparencyBoardContent> { ); } - // ── Shared Helpers ────────────────────────────────────────────────────────── + Widget _buildYearSelector( + BuildContext context, { + required List academicYears, + required int? selectedYearId, + }) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final activeAy = academicYears + .where((y) => y.status.toLowerCase() == 'active') + .firstOrNull ?? + academicYears.firstOrNull; + final effectiveSelectedId = (selectedYearId != null && + academicYears.any((y) => y.id == selectedYearId)) + ? selectedYearId + : activeAy?.id; + + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.25), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.white.withValues(alpha: 0.35), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.filter_alt_outlined, color: Colors.white, size: 15), + const SizedBox(width: 6), + const Text( + 'Academic Year:', + style: TextStyle( + color: Colors.white70, + fontSize: 12, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(width: 6), + DropdownButtonHideUnderline( + child: DropdownButton( + value: effectiveSelectedId, + isDense: true, + dropdownColor: + isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + icon: const Icon(Icons.arrow_drop_down, color: Colors.white, size: 20), + items: academicYears.map((ay) { + final isActive = ay.status.toLowerCase() == 'active'; + return DropdownMenuItem( + value: ay.id, + child: Text( + 'SY ${ay.yearRange}${isActive ? ' (Active)' : ''}', + style: TextStyle( + color: isDark ? Colors.white : Colors.black87, + fontSize: 12, + fontWeight: isActive ? FontWeight.bold : FontWeight.w500, + ), + ), + ); + }).toList(), + onChanged: (val) { + if (val != null) { + ref.read(transparencyBoardYearProvider.notifier).state = val; + } + }, + ), + ), + ], + ), + ); + } + + // ── Shared Helpers ───────────────────────────────────────────────────────── static Widget _buildThemeChip(String label, Color color) { return Container( @@ -363,7 +496,7 @@ class _TransparencyBoardContentState extends State<_TransparencyBoardContent> { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'Comparative Enrollment Data for Consecutive Years', + 'Enrollment Trends across School Years', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -390,7 +523,7 @@ class _TransparencyBoardContentState extends State<_TransparencyBoardContent> { runSpacing: 8, children: [ Text( - 'JHS (Key Stage 3) & SHS (Key Stage 4) Enrollment Breakdown (${latestYear.yearRange})', + 'Enrollment Summary (SY ${latestYear.yearRange})', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -916,7 +1049,7 @@ class _TransparencyBoardContentState extends State<_TransparencyBoardContent> { const Icon(Icons.error_outline, color: Colors.redAccent, size: 18), const SizedBox(width: 8), Text( - 'Data on Dropout (3 Consecutive Years)', + 'Dropouts by Grade', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -949,7 +1082,7 @@ class _TransparencyBoardContentState extends State<_TransparencyBoardContent> { const Icon(Icons.swap_horiz_outlined, color: Colors.orange, size: 18), const SizedBox(width: 8), Text( - 'Data on Transferees (3 Consecutive Years)', + 'Transferees by Grade', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, @@ -1120,7 +1253,7 @@ class _TransparencyBoardContentState extends State<_TransparencyBoardContent> { ), const SizedBox(width: 8), Text( - '4Ps Beneficiaries Distribution', + '4Ps Beneficiaries by Grade', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, diff --git a/frontend/lib/ui/screens/settings/teacher_management_screen.dart b/frontend/lib/ui/screens/settings/teacher_management_screen.dart index 405415e..ed6ba8e 100644 --- a/frontend/lib/ui/screens/settings/teacher_management_screen.dart +++ b/frontend/lib/ui/screens/settings/teacher_management_screen.dart @@ -1584,7 +1584,7 @@ class _AcademicYearFormModalState extends ConsumerState { showSuccessDialog( context, title: widget.year != null - ? 'Academic Year Updated' + ? 'Updated' : 'Academic Year Created', message: widget.year != null ? 'Academic year has been successfully updated.'