feat(reports): add inactive card, update student status breakdown donut, and fix summary banner width
This commit is contained in:
1 parent
2e370a5faf
commit
8aaefb2aa4
3 files changed
+52
-12
No files matched your search
@@ -48,10 +48,11 @@ exports.getStats = (req, res) => {
|
||||
const whereSql = whereClauses.length > 0 ? 'AND ' + whereClauses.join(' AND ') : '';
|
||||
const enrollWhereSql = enrollWhereClauses.length > 0 ? 'AND ' + enrollWhereClauses.join(' AND ') : '';
|
||||
|
||||
// 1. Fetch counts of active, dropped, transferee, graduated, and 4Ps beneficiaries
|
||||
// 1. Fetch counts of active, inactive, dropped, transferee, graduated, and 4Ps beneficiaries
|
||||
const countsQuery = `
|
||||
SELECT
|
||||
COUNT(DISTINCT CASE WHEN s.status = 'Enrolled' THEN s.id END) as active,
|
||||
COUNT(DISTINCT CASE WHEN s.status = 'Inactive' THEN s.id END) as inactive,
|
||||
COUNT(DISTINCT CASE WHEN s.status = 'Dropped' THEN s.id END) as dropped,
|
||||
COUNT(DISTINCT CASE WHEN s.status = 'Transferred' THEN s.id END) as transferee,
|
||||
COUNT(DISTINCT CASE WHEN s.status = 'Graduated' THEN s.id END) as graduated,
|
||||
|
||||
@@ -20,6 +20,7 @@ class AcademicYear {
|
||||
/// KPI summary numbers for the Reports screen.
|
||||
class StudentCounts {
|
||||
final int active;
|
||||
final int inactive;
|
||||
final int dropped;
|
||||
final int transferee;
|
||||
final int graduated;
|
||||
@@ -27,6 +28,7 @@ class StudentCounts {
|
||||
|
||||
const StudentCounts({
|
||||
required this.active,
|
||||
this.inactive = 0,
|
||||
required this.dropped,
|
||||
required this.transferee,
|
||||
required this.graduated,
|
||||
@@ -35,6 +37,7 @@ class StudentCounts {
|
||||
|
||||
factory StudentCounts.fromJson(Map<String, dynamic> j) => StudentCounts(
|
||||
active: (j['active'] as num?)?.toInt() ?? 0,
|
||||
inactive: (j['inactive'] as num?)?.toInt() ?? 0,
|
||||
dropped: (j['dropped'] as num?)?.toInt() ?? 0,
|
||||
transferee: (j['transferee'] as num?)?.toInt() ?? 0,
|
||||
graduated: (j['graduated'] as num?)?.toInt() ?? 0,
|
||||
|
||||
@@ -1253,14 +1253,20 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
|
||||
final students = reportData.students;
|
||||
final breakdown = reportData.missingDocsBreakdown;
|
||||
|
||||
final total =
|
||||
counts.active + counts.dropped + counts.transferee + counts.graduated;
|
||||
final total = counts.active +
|
||||
counts.inactive +
|
||||
counts.dropped +
|
||||
counts.transferee +
|
||||
counts.graduated;
|
||||
final compliantCount = students.where((s) => s.missingCount == 0).length;
|
||||
final withIssuesCount = students.where((s) => s.missingCount > 0).length;
|
||||
final totalMissing = breakdown.fold<int>(0, (a, b) => a + b.count);
|
||||
final complianceRate =
|
||||
students.isNotEmpty ? (compliantCount / students.length * 100) : 0.0;
|
||||
|
||||
final inactiveRate = total > 0
|
||||
? (counts.inactive / total * 100).toStringAsFixed(1)
|
||||
: '0.0';
|
||||
final gradRate = total > 0
|
||||
? (counts.graduated / total * 100).toStringAsFixed(1)
|
||||
: '0.0';
|
||||
@@ -1402,12 +1408,12 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// ── Secondary Status Row (5 cards) ───────────────────────────
|
||||
// ── Secondary Status Row (6 cards) ───────────────────────────
|
||||
LayoutBuilder(
|
||||
builder: (ctx, constraints) {
|
||||
final cols = constraints.maxWidth >= 900
|
||||
? 5
|
||||
: (constraints.maxWidth >= 600 ? 3 : 2);
|
||||
final cols = constraints.maxWidth >= 1050
|
||||
? 6
|
||||
: (constraints.maxWidth >= 750 ? 3 : 2);
|
||||
return GridView(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
@@ -1425,6 +1431,13 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
|
||||
icon: Icons.check_circle_outline,
|
||||
iconColor: AppColors.primaryGreen,
|
||||
),
|
||||
StatCard(
|
||||
title: 'Inactive',
|
||||
value: counts.inactive.toString(),
|
||||
subtitle: 'Rate: $inactiveRate%',
|
||||
icon: Icons.pause_circle_outline,
|
||||
iconColor: Colors.blueGrey,
|
||||
),
|
||||
StatCard(
|
||||
title: 'Dropouts',
|
||||
value: counts.dropped.toString(),
|
||||
@@ -2660,7 +2673,7 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
|
||||
}
|
||||
|
||||
|
||||
// ── Summary Banner ────────────────────────────────────────────────────────
|
||||
// ── Summary Banner ────────────────────────────────────────────────────────
|
||||
Widget _buildSummaryBanner(ReportStats data) {
|
||||
final total = data.students.length;
|
||||
final compliant = data.students.where((s) => s.missingCount == 0).length;
|
||||
@@ -2677,6 +2690,7 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
|
||||
: Colors.red;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
@@ -2685,7 +2699,7 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
|
||||
AppColors.primaryGreen.withValues(alpha: 0.02),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(AppSizes.radiusMedium),
|
||||
borderRadius: BorderRadius.circular(AppSizes.radiusLarge),
|
||||
border: Border.all(
|
||||
color: AppColors.primaryGreen.withValues(alpha: 0.2),
|
||||
),
|
||||
@@ -2705,8 +2719,13 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
|
||||
'${complianceRate.toStringAsFixed(1)}%', 'Complete Rate',
|
||||
rateColor),
|
||||
];
|
||||
if (constraints.maxWidth < 600) {
|
||||
return Wrap(spacing: 20, runSpacing: 12, children: items);
|
||||
if (constraints.maxWidth < 750) {
|
||||
return Wrap(
|
||||
spacing: 20,
|
||||
runSpacing: 12,
|
||||
alignment: WrapAlignment.spaceAround,
|
||||
children: items,
|
||||
);
|
||||
}
|
||||
final row = <Widget>[];
|
||||
for (int i = 0; i < items.length; i++) {
|
||||
@@ -2759,10 +2778,11 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
// ── Status Donut Chart ────────────────────────────────────────────────────
|
||||
// ── Status Donut Chart ───────────────────────────────────────────────────
|
||||
Widget _buildStatusDonutChart(StudentCounts counts) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final total = counts.active +
|
||||
counts.inactive +
|
||||
counts.dropped +
|
||||
counts.transferee +
|
||||
counts.graduated;
|
||||
@@ -2815,6 +2835,19 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
if (counts.inactive > 0)
|
||||
PieChartSectionData(
|
||||
color: Colors.blueGrey,
|
||||
value: counts.inactive.toDouble(),
|
||||
title:
|
||||
'${(counts.inactive / total * 100).toStringAsFixed(0)}%',
|
||||
radius: 55,
|
||||
titleStyle: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
if (counts.dropped > 0)
|
||||
PieChartSectionData(
|
||||
color: Colors.red,
|
||||
@@ -2890,6 +2923,9 @@ class _ReportsScreenState extends ConsumerState<ReportsScreen> {
|
||||
children: [
|
||||
_buildLegendItem(
|
||||
AppColors.primaryGreen, 'Active (${counts.active})'),
|
||||
if (counts.inactive > 0)
|
||||
_buildLegendItem(
|
||||
Colors.blueGrey, 'Inactive (${counts.inactive})'),
|
||||
_buildLegendItem(Colors.red, 'Dropped (${counts.dropped})'),
|
||||
_buildLegendItem(
|
||||
Colors.blue, 'Graduated (${counts.graduated})'),
|
||||
|
||||
Reference in new issue
Block a user