feat: add academic year filter to DepEd transparency board and update dialog title
This commit is contained in:
1 parent
a0ae156733
commit
3dc10845e0
7 files changed
+234
-31
No files matched your search
@@ -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
|
||||
|
||||
+18
-3
@@ -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 -->
|
||||
|
||||
|
||||
<!--
|
||||
# DOCUMENT REQUIREMENTS SCREEN
|
||||
- teachers cannot upload documents
|
||||
|
||||
# REPORTS SCREEN
|
||||
|
||||
|
||||
- fix all horizontal scroll bars always visible not hide -> need to touch the graphs to show
|
||||
- fix all horizontal scroll bars always visible not hide -> need to touch the graphs to show -->
|
||||
|
||||
<!--
|
||||
# TEACHER & ACADEMIC SETUP
|
||||
- Academic year updated dialog -> just one title word "Updated"
|
||||
-->
|
||||
|
||||
|
||||
|
||||
- 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)
|
||||
|
||||
@@ -36,8 +36,9 @@ class ReportRepository {
|
||||
}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{};
|
||||
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<TransparencyBoardData> getTransparencyBoardData() async {
|
||||
Future<TransparencyBoardData> getTransparencyBoardData({
|
||||
int? academicYearId,
|
||||
List<int>? yearIds,
|
||||
}) async {
|
||||
try {
|
||||
final queryParams = <String, dynamic>{};
|
||||
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<String, dynamic>);
|
||||
|
||||
@@ -210,12 +210,16 @@ final storageStatsProvider = FutureProvider.autoDispose<int>((ref) async {
|
||||
return await ref.read(reportRepositoryProvider).getStorageUsed();
|
||||
});
|
||||
|
||||
// Selected Academic Year for Transparency Board (null = default to active academic year)
|
||||
final transparencyBoardYearProvider = StateProvider<int?>((ref) => null);
|
||||
|
||||
// DepEd Transparency Board Data provider
|
||||
final transparencyBoardProvider =
|
||||
FutureProvider.autoDispose<TransparencyBoardData>((ref) async {
|
||||
final selectedYearId = ref.watch(transparencyBoardYearProvider);
|
||||
return await ref
|
||||
.read(reportRepositoryProvider)
|
||||
.getTransparencyBoardData();
|
||||
.getTransparencyBoardData(academicYearId: selectedYearId);
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -630,7 +630,7 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
|
||||
index: 0,
|
||||
icon: Icons.dashboard_outlined,
|
||||
title: 'DepEd Transparency Board',
|
||||
subtitle: 'Recommended DepEd metrics & equity',
|
||||
subtitle: 'Enrollment, dropouts, & 4Ps records',
|
||||
),
|
||||
_buildToggleItem(
|
||||
index: 1,
|
||||
|
||||
@@ -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<AcademicYear> 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<int>(
|
||||
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<int>(
|
||||
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,
|
||||
|
||||
@@ -1584,7 +1584,7 @@ class _AcademicYearFormModalState extends ConsumerState<AcademicYearFormModal> {
|
||||
showSuccessDialog(
|
||||
context,
|
||||
title: widget.year != null
|
||||
? 'Academic Year Updated'
|
||||
? 'Updated'
|
||||
: 'Academic Year Created',
|
||||
message: widget.year != null
|
||||
? 'Academic year has been successfully updated.'
|
||||
|
||||
Reference in new issue
Block a user