feat(reports): convert 4Ps beneficiaries to comparative previous vs active year table with JHS/SHS separation and remarks

This commit is contained in:
ssbg04 committed 2026-08-31 21:38:47 +08:00
1 parent 36bd41dea2
commit 4236986d4e
3 files changed
+607 -162

No files matched your search

+72 -47
View File
@@ -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,
),
),
),
],
),
],