feat(reports): convert 4Ps beneficiaries to comparative previous vs active year table with JHS/SHS separation and remarks
This commit is contained in:
1 parent
36bd41dea2
commit
4236986d4e
3 files changed
+607
-162
No files matched your search
@@ -508,6 +508,62 @@ exports.getTransparencyBoardData = (req, res) => {
|
||||
|
||||
const totalTransferred = gradesTransferee.reduce((sum, g) => sum + g.transferredCount, 0);
|
||||
|
||||
// 4Ps breakdown by grade for this specific academic year
|
||||
const fourPsRows = db.prepare(`
|
||||
SELECT
|
||||
e.grade_level,
|
||||
COUNT(DISTINCT CASE WHEN s.is_4ps = 1 THEN s.id END) as fourPsCount,
|
||||
COUNT(DISTINCT s.id) as totalStudents
|
||||
FROM enrollments e
|
||||
JOIN students s ON e.student_id = s.id
|
||||
WHERE e.academic_year_id = ?
|
||||
GROUP BY e.grade_level
|
||||
`).all(ay.id);
|
||||
|
||||
const grades4Ps = [7, 8, 9, 10, 11, 12].map(g => {
|
||||
const row = fourPsRows.find(r => Number(r.grade_level) === g) || { fourPsCount: 0, totalStudents: 0 };
|
||||
const count = Number(row.fourPsCount) || 0;
|
||||
const total = Number(row.totalStudents) || 0;
|
||||
const percentage = total > 0 ? Number((count / total * 100).toFixed(1)) : 0;
|
||||
return {
|
||||
gradeLevel: g,
|
||||
fourPsCount: count,
|
||||
totalStudents: total,
|
||||
percentage
|
||||
};
|
||||
});
|
||||
|
||||
const jhs4PsCount = grades4Ps.filter(g => g.gradeLevel <= 10).reduce((sum, g) => sum + g.fourPsCount, 0);
|
||||
const jhs4PsTotalStudents = grades4Ps.filter(g => g.gradeLevel <= 10).reduce((sum, g) => sum + g.totalStudents, 0);
|
||||
const jhs4PsPercentage = jhs4PsTotalStudents > 0 ? Number((jhs4PsCount / jhs4PsTotalStudents * 100).toFixed(1)) : 0;
|
||||
|
||||
const shs4PsCount = grades4Ps.filter(g => g.gradeLevel > 10).reduce((sum, g) => sum + g.fourPsCount, 0);
|
||||
const shs4PsTotalStudents = grades4Ps.filter(g => g.gradeLevel > 10).reduce((sum, g) => sum + g.totalStudents, 0);
|
||||
const shs4PsPercentage = shs4PsTotalStudents > 0 ? Number((shs4PsCount / shs4PsTotalStudents * 100).toFixed(1)) : 0;
|
||||
|
||||
const total4Ps = grades4Ps.reduce((sum, g) => sum + g.fourPsCount, 0);
|
||||
const totalStudents4Ps = grades4Ps.reduce((sum, g) => sum + g.totalStudents, 0);
|
||||
const overallPercentage = totalStudents4Ps > 0 ? Number((total4Ps / totalStudents4Ps * 100).toFixed(1)) : 0;
|
||||
|
||||
const fourPs = {
|
||||
grades: grades4Ps,
|
||||
jhsTotal: {
|
||||
fourPsCount: jhs4PsCount,
|
||||
totalStudents: jhs4PsTotalStudents,
|
||||
percentage: jhs4PsPercentage
|
||||
},
|
||||
shsTotal: {
|
||||
fourPsCount: shs4PsCount,
|
||||
totalStudents: shs4PsTotalStudents,
|
||||
percentage: shs4PsPercentage
|
||||
},
|
||||
overallTotal: {
|
||||
fourPsCount: total4Ps,
|
||||
totalStudents: totalStudents4Ps,
|
||||
percentage: overallPercentage
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
yearRange: ay.year_range,
|
||||
enrollment: {
|
||||
@@ -523,61 +579,30 @@ exports.getTransparencyBoardData = (req, res) => {
|
||||
transferees: {
|
||||
grades: gradesTransferee,
|
||||
totalTransferred
|
||||
}
|
||||
},
|
||||
fourPs
|
||||
};
|
||||
});
|
||||
|
||||
// 4Ps equity breakdown for latest academic year with enrollments
|
||||
let latestAyId = null;
|
||||
for (let i = academicYears.length - 1; i >= 0; i--) {
|
||||
const cnt = db.prepare('SELECT COUNT(*) as count FROM enrollments WHERE academic_year_id = ?').get(academicYears[i].id).count;
|
||||
if (cnt > 0) {
|
||||
latestAyId = academicYears[i].id;
|
||||
break;
|
||||
// Top level equity4Ps uses the active / selected year's 4Ps data
|
||||
const latestYearData = yearsData.length > 0 ? yearsData[yearsData.length - 1] : null;
|
||||
const equity4Ps = latestYearData?.fourPs
|
||||
? {
|
||||
grades: latestYearData.fourPs.grades,
|
||||
total4Ps: latestYearData.fourPs.overallTotal.fourPsCount,
|
||||
totalStudents: latestYearData.fourPs.overallTotal.totalStudents,
|
||||
overallPercentage: latestYearData.fourPs.overallTotal.percentage
|
||||
}
|
||||
}
|
||||
if (!latestAyId && academicYears.length > 0) {
|
||||
latestAyId = academicYears[academicYears.length - 1].id;
|
||||
}
|
||||
let fourPsRows = [];
|
||||
if (latestAyId) {
|
||||
fourPsRows = db.prepare(`
|
||||
SELECT
|
||||
e.grade_level,
|
||||
COUNT(DISTINCT CASE WHEN s.is_4ps = 1 THEN s.id END) as fourPsCount,
|
||||
COUNT(DISTINCT s.id) as totalStudents
|
||||
FROM enrollments e
|
||||
JOIN students s ON e.student_id = s.id
|
||||
WHERE e.academic_year_id = ?
|
||||
GROUP BY e.grade_level
|
||||
`).all(latestAyId);
|
||||
}
|
||||
|
||||
const grades4Ps = [7, 8, 9, 10, 11, 12].map(g => {
|
||||
const row = fourPsRows.find(r => Number(r.grade_level) === g) || { fourPsCount: 0, totalStudents: 0 };
|
||||
const count = Number(row.fourPsCount) || 0;
|
||||
const total = Number(row.totalStudents) || 0;
|
||||
const percentage = total > 0 ? Number((count / total * 100).toFixed(1)) : 0;
|
||||
return {
|
||||
gradeLevel: g,
|
||||
fourPsCount: count,
|
||||
totalStudents: total,
|
||||
percentage
|
||||
: {
|
||||
grades: [7, 8, 9, 10, 11, 12].map(g => ({ gradeLevel: g, fourPsCount: 0, totalStudents: 0, percentage: 0 })),
|
||||
total4Ps: 0,
|
||||
totalStudents: 0,
|
||||
overallPercentage: 0
|
||||
};
|
||||
});
|
||||
|
||||
const total4Ps = grades4Ps.reduce((sum, g) => sum + g.fourPsCount, 0);
|
||||
const totalStudents = grades4Ps.reduce((sum, g) => sum + g.totalStudents, 0);
|
||||
const overallPercentage = totalStudents > 0 ? Number((total4Ps / totalStudents * 100).toFixed(1)) : 0;
|
||||
|
||||
res.json({
|
||||
years: yearsData,
|
||||
equity4Ps: {
|
||||
grades: grades4Ps,
|
||||
total4Ps,
|
||||
totalStudents,
|
||||
overallPercentage
|
||||
}
|
||||
equity4Ps
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: 'Failed to fetch transparency board data', error: error.message });
|
||||
|
||||
@@ -302,18 +302,69 @@ class YearlyTransfereeSummary {
|
||||
);
|
||||
}
|
||||
|
||||
class Stage4PsSummary {
|
||||
final int fourPsCount;
|
||||
final int totalStudents;
|
||||
final double percentage;
|
||||
|
||||
const Stage4PsSummary({
|
||||
required this.fourPsCount,
|
||||
required this.totalStudents,
|
||||
required this.percentage,
|
||||
});
|
||||
|
||||
factory Stage4PsSummary.fromJson(Map<String, dynamic> j) => Stage4PsSummary(
|
||||
fourPsCount: (j['fourPsCount'] as num?)?.toInt() ?? 0,
|
||||
totalStudents: (j['totalStudents'] as num?)?.toInt() ?? 0,
|
||||
percentage: (j['percentage'] as num?)?.toDouble() ?? 0.0,
|
||||
);
|
||||
}
|
||||
|
||||
class Yearly4PsSummary {
|
||||
final List<Grade4PsCount> grades;
|
||||
final Stage4PsSummary jhsTotal;
|
||||
final Stage4PsSummary shsTotal;
|
||||
final Stage4PsSummary overallTotal;
|
||||
|
||||
const Yearly4PsSummary({
|
||||
required this.grades,
|
||||
required this.jhsTotal,
|
||||
required this.shsTotal,
|
||||
required this.overallTotal,
|
||||
});
|
||||
|
||||
factory Yearly4PsSummary.fromJson(Map<String, dynamic> j) => Yearly4PsSummary(
|
||||
grades: (j['grades'] as List? ?? [])
|
||||
.map((e) => Grade4PsCount.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
jhsTotal: Stage4PsSummary.fromJson(
|
||||
j['jhsTotal'] as Map<String, dynamic>? ?? {}),
|
||||
shsTotal: Stage4PsSummary.fromJson(
|
||||
j['shsTotal'] as Map<String, dynamic>? ?? {}),
|
||||
overallTotal: Stage4PsSummary.fromJson(
|
||||
j['overallTotal'] as Map<String, dynamic>? ?? {}),
|
||||
);
|
||||
}
|
||||
|
||||
/// Complete yearly Transparency Board item
|
||||
class YearlyTransparencyItem {
|
||||
final String yearRange;
|
||||
final YearlyEnrollmentSummary enrollment;
|
||||
final YearlyDropoutSummary dropouts;
|
||||
final YearlyTransfereeSummary transferees;
|
||||
final Yearly4PsSummary fourPs;
|
||||
|
||||
const YearlyTransparencyItem({
|
||||
required this.yearRange,
|
||||
required this.enrollment,
|
||||
required this.dropouts,
|
||||
required this.transferees,
|
||||
this.fourPs = const Yearly4PsSummary(
|
||||
grades: [],
|
||||
jhsTotal: Stage4PsSummary(fourPsCount: 0, totalStudents: 0, percentage: 0.0),
|
||||
shsTotal: Stage4PsSummary(fourPsCount: 0, totalStudents: 0, percentage: 0.0),
|
||||
overallTotal: Stage4PsSummary(fourPsCount: 0, totalStudents: 0, percentage: 0.0),
|
||||
),
|
||||
});
|
||||
|
||||
factory YearlyTransparencyItem.fromJson(Map<String, dynamic> j) =>
|
||||
@@ -325,6 +376,8 @@ class YearlyTransparencyItem {
|
||||
j['dropouts'] as Map<String, dynamic>? ?? {}),
|
||||
transferees: YearlyTransfereeSummary.fromJson(
|
||||
j['transferees'] as Map<String, dynamic>? ?? {}),
|
||||
fourPs: Yearly4PsSummary.fromJson(
|
||||
j['fourPs'] as Map<String, dynamic>? ?? {}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
|
||||
color: Colors.deepPurple,
|
||||
),
|
||||
const SizedBox(height: AppSizes.p16),
|
||||
_buildEquity4PsSection(context, data.equity4Ps),
|
||||
_buildEquity4PsSection(context, data.years),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1545,13 +1545,23 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
|
||||
return _responsiveTable(child: tableWidget, minWidth: 580);
|
||||
}
|
||||
|
||||
// ── Section 3: 4Ps Equity ───────────────────────────────────────────────────
|
||||
// ── Section 3: 4Ps Beneficiaries ──────────────────────────────────────────
|
||||
|
||||
Widget _buildEquity4PsSection(
|
||||
BuildContext context,
|
||||
Equity4PsSummary equity,
|
||||
List<YearlyTransparencyItem> years,
|
||||
) {
|
||||
if (years.isEmpty) {
|
||||
return const Center(child: Text('No comparative data found.'));
|
||||
}
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final latestYear = years.last;
|
||||
final previousYear = years.length > 1 ? years[years.length - 2] : null;
|
||||
final hasPrev = previousYear != null;
|
||||
final prevLabel = hasPrev ? 'SY ${previousYear.yearRange}' : 'Previous SY';
|
||||
final currentLabel = 'SY ${latestYear.yearRange}';
|
||||
|
||||
final active4Ps = latestYear.fourPs;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -1572,12 +1582,13 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'4Ps Beneficiaries by Grade',
|
||||
previousYear != null
|
||||
? '4Ps Beneficiaries (SY ${previousYear.yearRange} vs. SY ${latestYear.yearRange})'
|
||||
: '4Ps Beneficiaries (SY ${latestYear.yearRange})',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color:
|
||||
isDark ? AppColors.darkTextPrimary : AppColors.textPrimary,
|
||||
color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1589,7 +1600,7 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'Overall: ${equity.total4Ps} of ${equity.totalStudents} (${equity.overallPercentage}%)',
|
||||
'Active SY Total: ${active4Ps.overallTotal.fourPsCount} of ${active4Ps.overallTotal.totalStudents} (${active4Ps.overallTotal.percentage}%)',
|
||||
style: const TextStyle(
|
||||
color: Colors.deepPurple,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -1601,22 +1612,22 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_responsiveTable(
|
||||
minWidth: 580,
|
||||
minWidth: 680,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: isDark
|
||||
? AppColors.darkBorder
|
||||
: Colors.grey.withValues(alpha: 0.3),
|
||||
color: isDark ? AppColors.darkBorder : Colors.grey.withValues(alpha: 0.3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppSizes.radiusMedium),
|
||||
),
|
||||
child: Table(
|
||||
columnWidths: const {
|
||||
0: FlexColumnWidth(2),
|
||||
0: FlexColumnWidth(2.3),
|
||||
1: FlexColumnWidth(1.2),
|
||||
2: FlexColumnWidth(1.2),
|
||||
3: FlexColumnWidth(2),
|
||||
3: FlexColumnWidth(1.1),
|
||||
4: FlexColumnWidth(1.1),
|
||||
5: FlexColumnWidth(1.4),
|
||||
},
|
||||
border: TableBorder.symmetric(
|
||||
inside: BorderSide(
|
||||
@@ -1626,134 +1637,490 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
|
||||
),
|
||||
),
|
||||
children: [
|
||||
// Table Header
|
||||
TableRow(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.deepPurple.withValues(alpha: 0.08),
|
||||
),
|
||||
children: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'Grade Level',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
color: Colors.deepPurple.withValues(alpha: 0.1),
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(AppSizes.radiusMedium),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'4Ps Learners',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'Total Students',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'Percentage',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
...equity.grades.map((g) {
|
||||
return TableRow(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'Grade ${g.gradeLevel}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(g.fourPsCount.toString()),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(g.totalStudents.toString()),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: LinearProgressIndicator(
|
||||
value: (g.percentage / 100).clamp(0.0, 1.0),
|
||||
backgroundColor: isDark
|
||||
? AppColors.darkSurface2
|
||||
: Colors.grey.withValues(alpha: 0.15),
|
||||
color: Colors.deepPurple,
|
||||
minHeight: 8,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
SizedBox(
|
||||
width: 44,
|
||||
child: Text(
|
||||
'${g.percentage}%',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
color: Colors.deepPurple,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
// Total Row
|
||||
TableRow(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? AppColors.darkSurface2
|
||||
: Colors.grey.withValues(alpha: 0.06),
|
||||
),
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'TOTAL',
|
||||
'Key Stage / Grade Level',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
prevLabel,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
currentLabel,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'Difference',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'4Ps Share',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'Remarks',
|
||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 12),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// ── Key Stage 3 Header ──
|
||||
TableRow(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? AppColors.darkSurface2
|
||||
: Colors.grey.withValues(alpha: 0.08),
|
||||
),
|
||||
children: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'KEY STAGE 3 (JUNIOR HIGH SCHOOL)',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox.shrink(),
|
||||
SizedBox.shrink(),
|
||||
SizedBox.shrink(),
|
||||
SizedBox.shrink(),
|
||||
SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
|
||||
// JHS Grades 7-10
|
||||
...[7, 8, 9, 10].map((grade) {
|
||||
final currRow = latestYear.fourPs.grades.firstWhere(
|
||||
(g) => g.gradeLevel == grade,
|
||||
orElse: () => Grade4PsCount(
|
||||
gradeLevel: grade,
|
||||
fourPsCount: 0,
|
||||
totalStudents: 0,
|
||||
percentage: 0.0,
|
||||
),
|
||||
);
|
||||
|
||||
final prevRow = hasPrev
|
||||
? previousYear.fourPs.grades.firstWhere(
|
||||
(g) => g.gradeLevel == grade,
|
||||
orElse: () => Grade4PsCount(
|
||||
gradeLevel: grade,
|
||||
fourPsCount: 0,
|
||||
totalStudents: 0,
|
||||
percentage: 0.0,
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
final curr = currRow.fourPsCount;
|
||||
final prev = prevRow?.fourPsCount;
|
||||
final diff = hasPrev ? (curr - (prev ?? 0)) : null;
|
||||
|
||||
return TableRow(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text('Grade $grade'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
hasPrev ? '$prev' : '—',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'$curr',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Center(
|
||||
child: _buildDifferenceCell(diff, hasPreviousYear: hasPrev, isDark: isDark),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'${currRow.percentage}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.deepPurple,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
||||
child: Center(
|
||||
child: _buildRemarkBadge(diff, hasPreviousYear: hasPrev, isDark: isDark),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
// JHS Subtotal Row
|
||||
TableRow(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.deepPurple.withValues(alpha: 0.05),
|
||||
),
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'Key Stage 3 (JHS) Total',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
equity.total4Ps.toString(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.deepPurple,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
equity.totalStudents.toString(),
|
||||
hasPrev ? '${previousYear.fourPs.jhsTotal.fourPsCount}' : '—',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'${equity.overallPercentage}% of total',
|
||||
'${latestYear.fourPs.jhsTotal.fourPsCount}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.deepPurple,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Center(
|
||||
child: _buildDifferenceCell(
|
||||
hasPrev
|
||||
? (latestYear.fourPs.jhsTotal.fourPsCount -
|
||||
previousYear.fourPs.jhsTotal.fourPsCount)
|
||||
: null,
|
||||
hasPreviousYear: hasPrev,
|
||||
isDark: isDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'${latestYear.fourPs.jhsTotal.percentage}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.deepPurple,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
||||
child: Center(
|
||||
child: _buildRemarkBadge(
|
||||
hasPrev
|
||||
? (latestYear.fourPs.jhsTotal.fourPsCount -
|
||||
previousYear.fourPs.jhsTotal.fourPsCount)
|
||||
: null,
|
||||
hasPreviousYear: hasPrev,
|
||||
isDark: isDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// ── Key Stage 4 Header ──
|
||||
TableRow(
|
||||
decoration: BoxDecoration(
|
||||
color: isDark
|
||||
? AppColors.darkSurface2
|
||||
: Colors.grey.withValues(alpha: 0.08),
|
||||
),
|
||||
children: const [
|
||||
Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'KEY STAGE 4 (SENIOR HIGH SCHOOL)',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox.shrink(),
|
||||
SizedBox.shrink(),
|
||||
SizedBox.shrink(),
|
||||
SizedBox.shrink(),
|
||||
SizedBox.shrink(),
|
||||
],
|
||||
),
|
||||
|
||||
// SHS Grades 11-12
|
||||
...[11, 12].map((grade) {
|
||||
final currRow = latestYear.fourPs.grades.firstWhere(
|
||||
(g) => g.gradeLevel == grade,
|
||||
orElse: () => Grade4PsCount(
|
||||
gradeLevel: grade,
|
||||
fourPsCount: 0,
|
||||
totalStudents: 0,
|
||||
percentage: 0.0,
|
||||
),
|
||||
);
|
||||
|
||||
final prevRow = hasPrev
|
||||
? previousYear.fourPs.grades.firstWhere(
|
||||
(g) => g.gradeLevel == grade,
|
||||
orElse: () => Grade4PsCount(
|
||||
gradeLevel: grade,
|
||||
fourPsCount: 0,
|
||||
totalStudents: 0,
|
||||
percentage: 0.0,
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
final curr = currRow.fourPsCount;
|
||||
final prev = prevRow?.fourPsCount;
|
||||
final diff = hasPrev ? (curr - (prev ?? 0)) : null;
|
||||
|
||||
return TableRow(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text('Grade $grade'),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
hasPrev ? '$prev' : '—',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'$curr',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Center(
|
||||
child: _buildDifferenceCell(diff, hasPreviousYear: hasPrev, isDark: isDark),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'${currRow.percentage}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.deepPurple,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
||||
child: Center(
|
||||
child: _buildRemarkBadge(diff, hasPreviousYear: hasPrev, isDark: isDark),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
// SHS Subtotal Row
|
||||
TableRow(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.deepPurple.withValues(alpha: 0.05),
|
||||
),
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'Key Stage 4 (SHS) Total',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
hasPrev ? '${previousYear.fourPs.shsTotal.fourPsCount}' : '—',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'${latestYear.fourPs.shsTotal.fourPsCount}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.deepPurple,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Center(
|
||||
child: _buildDifferenceCell(
|
||||
hasPrev
|
||||
? (latestYear.fourPs.shsTotal.fourPsCount -
|
||||
previousYear.fourPs.shsTotal.fourPsCount)
|
||||
: null,
|
||||
hasPreviousYear: hasPrev,
|
||||
isDark: isDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'${latestYear.fourPs.shsTotal.percentage}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.deepPurple,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
||||
child: Center(
|
||||
child: _buildRemarkBadge(
|
||||
hasPrev
|
||||
? (latestYear.fourPs.shsTotal.fourPsCount -
|
||||
previousYear.fourPs.shsTotal.fourPsCount)
|
||||
: null,
|
||||
hasPreviousYear: hasPrev,
|
||||
isDark: isDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// ── Overall Total Row ──
|
||||
TableRow(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.deepPurple.withValues(alpha: 0.12),
|
||||
),
|
||||
children: [
|
||||
const Padding(
|
||||
padding: EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'Overall Total (JHS + SHS)',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.deepPurple,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
hasPrev ? '${previousYear.fourPs.overallTotal.fourPsCount}' : '—',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'${latestYear.fourPs.overallTotal.fourPsCount}',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.deepPurple,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Center(
|
||||
child: _buildDifferenceCell(
|
||||
hasPrev
|
||||
? (latestYear.fourPs.overallTotal.fourPsCount -
|
||||
previousYear.fourPs.overallTotal.fourPsCount)
|
||||
: null,
|
||||
hasPreviousYear: hasPrev,
|
||||
isDark: isDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Text(
|
||||
'${latestYear.fourPs.overallTotal.percentage}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.deepPurple,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
|
||||
child: Center(
|
||||
child: _buildRemarkBadge(
|
||||
hasPrev
|
||||
? (latestYear.fourPs.overallTotal.fourPsCount -
|
||||
previousYear.fourPs.overallTotal.fourPsCount)
|
||||
: null,
|
||||
hasPreviousYear: hasPrev,
|
||||
isDark: isDark,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user