feat(reports): refine filters and export layout, add backend Excel export, optimize logo to webp

This commit is contained in:
ssbg04 committed 2026-10-02 20:30:15 +08:00
1 parent 3a3e0dc7b8
commit e6e8188312
11 files changed
+2982 -892

No files matched your search

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

@@ -1,5 +1,6 @@
const db = require('../config/db');
const pdfService = require('../services/pdfService');
const excelService = require('../services/excelService');
// GET /api/reports/academic-years
exports.getAcademicYears = (req, res) => {
@@ -651,3 +652,191 @@ exports.generateTransparencyBoardPdf = async (req, res) => {
}
};
// Helper to fetch compliance stats data for Excel export
function fetchComplianceStatsPayload(query = {}, user = null) {
const { academicYearId, gradeLevel, sectionId, status } = query;
let params = [];
let enrollParams = [];
let whereClauses = [];
let enrollWhereClauses = [];
if (user?.role === 'teacher') {
whereClauses.push('e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?)');
enrollWhereClauses.push('e.section_id IN (SELECT section_id FROM teacher_sections WHERE teacher_id = ?)');
params.push(user.id);
enrollParams.push(user.id);
}
if (academicYearId) {
whereClauses.push('e.academic_year_id = ?');
enrollWhereClauses.push('e.academic_year_id = ?');
params.push(academicYearId);
enrollParams.push(academicYearId);
}
if (gradeLevel) {
whereClauses.push('e.grade_level = ?');
enrollWhereClauses.push('e.grade_level = ?');
params.push(gradeLevel);
enrollParams.push(gradeLevel);
}
if (sectionId) {
whereClauses.push('e.section_id = ?');
enrollWhereClauses.push('e.section_id = ?');
params.push(sectionId);
enrollParams.push(sectionId);
}
const whereSql = whereClauses.length > 0 ? 'AND ' + whereClauses.join(' AND ') : '';
const enrollWhereSql = enrollWhereClauses.length > 0 ? 'AND ' + enrollWhereClauses.join(' AND ') : '';
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,
COUNT(DISTINCT CASE WHEN s.is_4ps = 1 THEN s.id END) as fourPs
FROM students s
JOIN enrollments e ON s.id = e.student_id
AND e.id = (
SELECT e2.id FROM enrollments e2
JOIN academic_years ay ON e2.academic_year_id = ay.id
WHERE e2.student_id = s.id
${academicYearId ? 'AND e2.academic_year_id = ' + Number(academicYearId) : ''}
ORDER BY e2.grade_level DESC, ay.year_range DESC, e2.id DESC LIMIT 1
)
WHERE 1=1 ${enrollWhereSql}
`;
const studentCounts = db.prepare(countsQuery).get(enrollParams) || {};
const missingQuery = `
SELECT r.id as requirementId, r.category || ' - ' || r.name as name, COUNT(DISTINCT s.id) as count
FROM document_requirements r
CROSS JOIN students s
JOIN enrollments e ON s.id = e.student_id
AND e.id = (
SELECT e2.id FROM enrollments e2
JOIN academic_years ay ON e2.academic_year_id = ay.id
WHERE e2.student_id = s.id
${academicYearId ? 'AND e2.academic_year_id = ' + Number(academicYearId) : ''}
ORDER BY e2.grade_level DESC, ay.year_range DESC, e2.id DESC LIMIT 1
)
WHERE r.is_enabled = 1
AND r.is_mandatory = 1
AND r.category IN (
SELECT DISTINCT CASE WHEN grade_level <= 10 THEN 'JHS' ELSE 'SHS' END
FROM enrollments WHERE student_id = s.id
)
${whereSql}
AND NOT EXISTS (
SELECT 1 FROM documents d
WHERE d.student_id = s.id
AND d.requirement_id = r.id
AND (d.status = 'Completed' OR (d.status = 'Archived' AND s.status != 'Enrolled'))
AND d.deleted_at IS NULL
)
GROUP BY r.id, r.category, r.name
ORDER BY count DESC
`;
const missingDocsBreakdown = db.prepare(missingQuery).all(params);
let studentFilterSql = whereSql;
let studentParams = [...params];
if (status) {
studentFilterSql += " AND s.status = ?";
studentParams.push(status);
}
const studentsQuery = `
SELECT s.id, s.lrn, s.first_name, s.last_name, s.sex, s.status,
e_latest.grade_level, sec.name as section_name,
(
SELECT COUNT(*)
FROM document_requirements r
WHERE r.is_enabled = 1
AND r.is_mandatory = 1
AND r.category IN (
SELECT DISTINCT CASE WHEN grade_level <= 10 THEN 'JHS' ELSE 'SHS' END
FROM enrollments WHERE student_id = s.id
)
AND NOT EXISTS (
SELECT 1 FROM documents d
WHERE d.student_id = s.id
AND d.requirement_id = r.id
AND (d.status = 'Completed' OR (d.status = 'Archived' AND s.status != 'Enrolled'))
AND d.deleted_at IS NULL
)
) as missing_count,
(
SELECT group_concat('[' || r.category || '] ' || r.name, ', ')
FROM document_requirements r
WHERE r.is_enabled = 1
AND r.is_mandatory = 1
AND r.category IN (
SELECT DISTINCT CASE WHEN grade_level <= 10 THEN 'JHS' ELSE 'SHS' END
FROM enrollments WHERE student_id = s.id
)
AND NOT EXISTS (
SELECT 1 FROM documents d
WHERE d.student_id = s.id
AND d.requirement_id = r.id
AND (d.status = 'Completed' OR (d.status = 'Archived' AND s.status != 'Enrolled'))
AND d.deleted_at IS NULL
)
) as missing_requirements
FROM students s
JOIN enrollments e_latest ON s.id = e_latest.student_id
AND e_latest.id = (
SELECT e2.id FROM enrollments e2
JOIN academic_years ay ON e2.academic_year_id = ay.id
WHERE e2.student_id = s.id
${academicYearId ? 'AND e2.academic_year_id = ' + Number(academicYearId) : ''}
ORDER BY e2.grade_level DESC, ay.year_range DESC, e2.id DESC LIMIT 1
)
LEFT JOIN sections sec ON e_latest.section_id = sec.id
WHERE 1=1 ${studentFilterSql.replace(/\be\./g, 'e_latest.')}
ORDER BY s.last_name ASC, s.first_name ASC
LIMIT 1000
`;
const studentsList = db.prepare(studentsQuery).all(studentParams);
let yearLabel = 'All Years';
if (academicYearId) {
const ay = db.prepare('SELECT year_range FROM academic_years WHERE id = ?').get(academicYearId);
if (ay && ay.year_range) {
yearLabel = ay.year_range;
}
}
return {
studentCounts,
missingDocsBreakdown,
students: studentsList,
yearLabel
};
}
// GET or POST /api/reports/compliance/excel
exports.generateComplianceReportExcel = async (req, res) => {
try {
let payload;
if (req.body && req.body.students && req.body.studentCounts) {
payload = req.body;
} else {
payload = fetchComplianceStatsPayload(req.query, req.user);
}
const excelBuffer = await excelService.generateComplianceExcel(payload);
const yearSlug = (payload.yearLabel || 'All_Years').replace(/[^a-zA-Z0-9]/g, '_');
const filename = `TIS_RMS_Report_${yearSlug}_${Date.now()}.xlsx`;
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
res.send(excelBuffer);
} catch (error) {
console.error('generateComplianceReportExcel error:', error);
res.status(500).json({ message: 'Failed to generate compliance report Excel', error: error.message });
}
};
+3
View File
@@ -13,4 +13,7 @@ router.get('/storage', authenticateToken, reportsController.getStorageUsed);
router.get('/transparency-board', authenticateToken, reportsController.getTransparencyBoardData);
router.get('/transparency-board/pdf', authenticateToken, reportsController.generateTransparencyBoardPdf);
router.get('/compliance/excel', authenticateToken, reportsController.generateComplianceReportExcel);
router.post('/compliance/excel', authenticateToken, reportsController.generateComplianceReportExcel);
module.exports = router;
File diff suppressed because it is too large. Load diff
+227
View File
@@ -0,0 +1,227 @@
'use strict';
const ExcelJS = require('exceljs');
/**
* Generate Compliance Report Excel (.xlsx) buffer matching TIS RMS design and layout.
*
* @param {Object} data
* @param {Object} data.studentCounts - { active, dropped, transferee, graduated, inactive, fourPs }
* @param {Array} data.missingDocsBreakdown - [ { name, count } ]
* @param {Array} data.students - [ { lrn, first_name, last_name, fullName, sex, grade_level, gradeLevel, section_name, sectionName, status, missing_count, missingCount, missing_requirements, missingRequirements } ]
* @param {string} data.yearLabel - e.g. "2025-2026" or "All Years"
* @returns {Promise<Buffer>}
*/
async function generateComplianceExcel({ studentCounts = {}, missingDocsBreakdown = [], students = [], yearLabel = 'All Years' }) {
const workbook = new ExcelJS.Workbook();
workbook.creator = 'TIS RMS';
workbook.lastModifiedBy = 'TIS RMS';
workbook.created = new Date();
workbook.modified = new Date();
const borderThin = {
top: { style: 'thin', color: { argb: 'FFE2E8F0' } },
left: { style: 'thin', color: { argb: 'FFE2E8F0' } },
bottom: { style: 'thin', color: { argb: 'FFE2E8F0' } },
right: { style: 'thin', color: { argb: 'FFE2E8F0' } }
};
// ── SHEET 1: Summary ────────────────────────────────────────────────────────
const summarySheet = workbook.addWorksheet('Summary', {
views: [{ showGridLines: true }]
});
summarySheet.columns = [
{ width: 44 },
{ width: 18 }
];
// Title & Metadata
const titleRow = summarySheet.addRow(['TALISAY INTEGRATED SCHOOL - TIS RMS']);
titleRow.font = { name: 'Arial', size: 14, bold: true, color: { argb: 'FF166534' } };
const subTitleRow = summarySheet.addRow([`Annual Report Summary: ${yearLabel}`]);
subTitleRow.font = { name: 'Arial', size: 11, bold: true, color: { argb: 'FF334155' } };
const genDateStr = new Date().toISOString().replace('T', ' ').substring(0, 19);
const dateRow = summarySheet.addRow([`Generated: ${genDateStr}`]);
dateRow.font = { name: 'Arial', size: 9, italic: true, color: { argb: 'FF64748B' } };
summarySheet.addRow([]); // Blank spacer
// Section 1: Student Statistics
const sec1Header = summarySheet.addRow(['STUDENT STATISTICS', '']);
sec1Header.font = { name: 'Arial', size: 11, bold: true, color: { argb: 'FF166534' } };
sec1Header.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0FDF4' } };
sec1Header.getCell(2).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0FDF4' } };
const statHeader = summarySheet.addRow(['Student Status', 'Total Count']);
statHeader.font = { name: 'Arial', size: 10, bold: true, color: { argb: 'FF1E293B' } };
statHeader.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
statHeader.getCell(2).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
statHeader.getCell(2).alignment = { horizontal: 'right' };
statHeader.getCell(1).border = borderThin;
statHeader.getCell(2).border = borderThin;
const statsRowsData = [
['Active (Enrolled)', Number(studentCounts.active || 0)],
['Dropouts (Dropped)', Number(studentCounts.dropped || 0)],
['Transferees (Transferred)', Number(studentCounts.transferee || 0)],
['Graduated', Number(studentCounts.graduated || 0)],
];
for (const [label, count] of statsRowsData) {
const r = summarySheet.addRow([label, count]);
r.font = { name: 'Arial', size: 10, color: { argb: 'FF334155' } };
r.getCell(1).border = borderThin;
r.getCell(2).border = borderThin;
r.getCell(2).alignment = { horizontal: 'right' };
}
summarySheet.addRow([]); // Blank spacer
// Section 2: Missing Documents Per Requirement Type
const sec2Header = summarySheet.addRow(['MISSING DOCUMENTS PER REQUIREMENT TYPE', '']);
sec2Header.font = { name: 'Arial', size: 11, bold: true, color: { argb: 'FF166534' } };
sec2Header.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0FDF4' } };
sec2Header.getCell(2).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF0FDF4' } };
const docHeader = summarySheet.addRow(['Document Type', 'Missing Count']);
docHeader.font = { name: 'Arial', size: 10, bold: true, color: { argb: 'FF1E293B' } };
docHeader.getCell(1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
docHeader.getCell(2).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
docHeader.getCell(2).alignment = { horizontal: 'right' };
docHeader.getCell(1).border = borderThin;
docHeader.getCell(2).border = borderThin;
if (missingDocsBreakdown.length === 0) {
const r = summarySheet.addRow(['All student requirements complete', 0]);
r.font = { name: 'Arial', size: 10, color: { argb: 'FF64748B' } };
r.getCell(1).border = borderThin;
r.getCell(2).border = borderThin;
r.getCell(2).alignment = { horizontal: 'right' };
} else {
for (const row of missingDocsBreakdown) {
const r = summarySheet.addRow([row.name, Number(row.count || 0)]);
r.font = { name: 'Arial', size: 10, color: { argb: 'FF334155' } };
r.getCell(1).border = borderThin;
r.getCell(2).border = borderThin;
r.getCell(2).alignment = { horizontal: 'right' };
}
}
// ── SHEET 2: Student Compliance List ────────────────────────────────────────
const listSheet = workbook.addWorksheet('Student Compliance List', {
views: [{ state: 'frozen', xSplit: 0, ySplit: 1, showGridLines: true }]
});
listSheet.columns = [
{ header: '#', key: 'index', width: 6 },
{ header: 'LRN', key: 'lrn', width: 18 },
{ header: 'Student Name', key: 'student_name', width: 28 },
{ header: 'Sex', key: 'sex', width: 10 },
{ header: 'Grade Level', key: 'grade_level', width: 14 },
{ header: 'Section', key: 'section', width: 18 },
{ header: 'Status', key: 'status', width: 14 },
{ header: 'Missing Count', key: 'missing_count', width: 15 },
{ header: 'Missing Documents', key: 'missing_docs', width: 50 },
];
// Style the header row
const listHeaderRow = listSheet.getRow(1);
listHeaderRow.height = 26;
listHeaderRow.font = { name: 'Arial', size: 10, bold: true, color: { argb: 'FFFFFFFF' } };
listHeaderRow.fill = {
type: 'pattern',
pattern: 'solid',
fgColor: { argb: 'FF166534' } // Forest Green
};
listHeaderRow.alignment = { vertical: 'middle', horizontal: 'center' };
listHeaderRow.getCell('student_name').alignment = { vertical: 'middle', horizontal: 'left' };
listHeaderRow.getCell('missing_docs').alignment = { vertical: 'middle', horizontal: 'left' };
for (let c = 1; c <= 9; c++) {
listHeaderRow.getCell(c).border = borderThin;
}
// Populate Student Data Rows
for (let i = 0; i < students.length; i++) {
const s = students[i];
const fullName = s.fullName ||
([s.last_name, s.first_name].filter(Boolean).join(', ') || 'N/A');
const gradeVal = s.grade_level != null ? s.grade_level : s.gradeLevel;
const gradeStr = gradeVal != null ? `Grade ${gradeVal}` : 'N/A';
const sectionStr = s.section_name || s.sectionName || 'N/A';
const missingCount = s.missing_count != null ? Number(s.missing_count) : Number(s.missingCount || 0);
const missingDocsStr = s.missing_requirements || s.missingRequirements ||
(missingCount === 0 ? 'None (Complete)' : 'N/A');
const row = listSheet.addRow({
index: i + 1,
lrn: s.lrn || '',
student_name: fullName,
sex: s.sex || 'N/A',
grade_level: gradeStr,
section: sectionStr,
status: s.status || 'N/A',
missing_count: missingCount,
missing_docs: missingDocsStr,
});
row.height = 20;
row.font = { name: 'Arial', size: 9.5, color: { argb: 'FF1F2937' } };
const isEven = i % 2 === 1;
const rowBg = isEven ? 'FFF8FAFC' : 'FFFFFFFF';
for (let colIdx = 1; colIdx <= 9; colIdx++) {
const cell = row.getCell(colIdx);
cell.border = borderThin;
cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: rowBg } };
cell.alignment = { vertical: 'middle' };
}
// Specific cell alignments
row.getCell('index').alignment = { vertical: 'middle', horizontal: 'center' };
row.getCell('lrn').alignment = { vertical: 'middle', horizontal: 'center' };
row.getCell('sex').alignment = { vertical: 'middle', horizontal: 'center' };
row.getCell('grade_level').alignment = { vertical: 'middle', horizontal: 'center' };
row.getCell('status').alignment = { vertical: 'middle', horizontal: 'center' };
row.getCell('missing_count').alignment = { vertical: 'middle', horizontal: 'right' };
row.getCell('student_name').alignment = { vertical: 'middle', horizontal: 'left' };
row.getCell('missing_docs').alignment = { vertical: 'middle', horizontal: 'left' };
// Highlight missing count if > 0
if (missingCount > 0) {
row.getCell('missing_count').font = {
name: 'Arial',
size: 9.5,
bold: true,
color: { argb: 'FFDC2626' }
};
} else {
row.getCell('missing_count').font = {
name: 'Arial',
size: 9.5,
color: { argb: 'FF166534' }
};
}
}
// Auto-filter on student list
if (students.length > 0) {
listSheet.autoFilter = {
from: { row: 1, column: 1 },
to: { row: students.length + 1, column: 9 }
};
}
const buffer = await workbook.xlsx.writeBuffer();
return Buffer.from(buffer);
}
module.exports = {
generateComplianceExcel,
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

@@ -161,5 +161,35 @@ class ReportRepository {
);
}
}
Future<Uint8List> downloadComplianceReportExcel({
int? academicYearId,
int? gradeLevel,
int? sectionId,
String? status,
}) async {
try {
final queryParams = <String, dynamic>{};
if (academicYearId != null) queryParams['academicYearId'] = academicYearId;
if (gradeLevel != null) queryParams['gradeLevel'] = gradeLevel;
if (sectionId != null) queryParams['sectionId'] = sectionId;
if (status != null) queryParams['status'] = status;
final options = await _authOptions();
options.responseType = ResponseType.bytes;
final res = await _dio.get<List<int>>(
'/reports/compliance/excel',
queryParameters: queryParams.isNotEmpty ? queryParams : null,
options: options,
);
return Uint8List.fromList(res.data!);
} on DioException catch (e) {
throw Exception(
e.response?.data?['message'] ??
'Failed to generate compliance report Excel.',
);
}
}
}
File diff suppressed because it is too large. Load diff
@@ -1,5 +1,6 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/constants/app_colors.dart';
import '../../../../core/constants/app_sizes.dart';
@@ -180,38 +181,87 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
final isThisButtonExporting = _isExportingPdf && _exportingCategory == 'header_$activeCat';
return MouseRegion(
cursor: _isExportingPdf ? SystemMouseCursors.basic : SystemMouseCursors.click,
child: ElevatedButton.icon(
onPressed: _isExportingPdf ? null : () => _handleExportPdf(data, category: activeCat, isHeader: true),
icon: isThisButtonExporting
? const SizedBox(
width: 13,
height: 13,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
final label = _activeTab == _TransparencyTab.enrollment
? 'Enrollment PDF'
: _activeTab == _TransparencyTab.dropoutsTransferees
? 'Dropouts PDF'
: _activeTab == _TransparencyTab.equity4Ps
? '4Ps Equity PDF'
: 'Export PDF';
return Tooltip(
message: 'Download official DepEd report for current section (.pdf)',
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _isExportingPdf
? null
: () {
HapticFeedback.lightImpact();
_handleExportPdf(data, category: activeCat, isHeader: true);
},
borderRadius: BorderRadius.circular(8),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
blurRadius: 6,
offset: const Offset(0, 2),
),
)
: const Icon(Icons.picture_as_pdf_rounded, size: 14),
label: Text(
isThisButtonExporting ? 'Exporting...' : 'Export PDF',
style: const TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.bold,
color: Colors.white,
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isThisButtonExporting)
const SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFFDC2626),
),
)
else
const Icon(
Icons.picture_as_pdf_rounded,
size: 16,
color: Color(0xFFDC2626),
),
const SizedBox(width: 7),
Text(
isThisButtonExporting ? 'Exporting...' : label,
style: const TextStyle(
fontSize: 12.5,
fontWeight: FontWeight.bold,
color: Color(0xFF1E293B),
),
),
const SizedBox(width: 5),
Container(
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 1.5),
decoration: BoxDecoration(
color: const Color(0xFFDC2626).withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'.pdf',
style: TextStyle(
fontSize: 9.5,
fontWeight: FontWeight.bold,
color: Color(0xFFDC2626),
),
),
),
],
),
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
disabledBackgroundColor: isThisButtonExporting ? Colors.red.shade300 : Colors.red.shade700.withValues(alpha: 0.6),
disabledForegroundColor: Colors.white70,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
minimumSize: const Size(100, 34),
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
),
),
);
}
@@ -221,39 +271,62 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
required String category,
String label = 'Export PDF',
}) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final isThisButtonExporting = _isExportingPdf && _exportingCategory == 'card_$category';
return MouseRegion(
cursor: _isExportingPdf ? SystemMouseCursors.basic : SystemMouseCursors.click,
child: ElevatedButton.icon(
onPressed: _isExportingPdf ? null : () => _handleExportPdf(data, category: category, isHeader: false),
icon: isThisButtonExporting
? const SizedBox(
width: 13,
height: 13,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
return Tooltip(
message: 'Download section report as PDF',
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: _isExportingPdf
? null
: () {
HapticFeedback.lightImpact();
_handleExportPdf(data, category: category, isHeader: false);
},
borderRadius: BorderRadius.circular(7),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
padding: const EdgeInsets.symmetric(horizontal: 11, vertical: 6.5),
decoration: BoxDecoration(
color: isDark ? AppColors.darkSurface2 : const Color(0xFFFEF2F2),
borderRadius: BorderRadius.circular(7),
border: Border.all(
color: const Color(0xFFDC2626).withValues(alpha: isDark ? 0.35 : 0.25),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (isThisButtonExporting)
const SizedBox(
width: 13,
height: 13,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Color(0xFFDC2626),
),
)
else
const Icon(
Icons.picture_as_pdf_rounded,
size: 14,
color: Color(0xFFDC2626),
),
const SizedBox(width: 6),
Text(
isThisButtonExporting ? 'Exporting...' : label,
style: TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.bold,
color: isDark ? Colors.red.shade300 : const Color(0xFFDC2626),
),
),
)
: const Icon(Icons.picture_as_pdf_rounded, size: 14),
label: Text(
isThisButtonExporting ? 'Exporting...' : label,
style: const TextStyle(
fontSize: 11.5,
fontWeight: FontWeight.bold,
color: Colors.white,
],
),
),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade700,
foregroundColor: Colors.white,
disabledBackgroundColor: isThisButtonExporting ? Colors.red.shade300 : Colors.red.shade700.withValues(alpha: 0.6),
disabledForegroundColor: Colors.white70,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
minimumSize: const Size(100, 34),
elevation: 1,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(6)),
),
),
);
}
@@ -738,7 +811,7 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.filter_alt_outlined, color: Colors.white, size: 15),
const Icon(Icons.calendar_month_rounded, color: Colors.white, size: 15),
const SizedBox(width: 6),
const Text(
'Academic Year:',
@@ -776,18 +849,42 @@ class _TransparencyBoardContentState extends ConsumerState<_TransparencyBoardCon
final isActive = ay.status.toLowerCase() == 'active';
return DropdownMenuItem<int>(
value: ay.id,
child: Text(
'SY ${ay.yearRange}${isActive ? ' (Active)' : ''}',
style: TextStyle(
color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary,
fontSize: 12,
fontWeight: isActive ? FontWeight.bold : FontWeight.w500,
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'SY ${ay.yearRange}',
style: TextStyle(
color: isDark ? AppColors.darkTextPrimary : AppColors.textPrimary,
fontSize: 12,
fontWeight: isActive ? FontWeight.bold : FontWeight.w500,
),
),
if (isActive) ...[
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1.5),
decoration: BoxDecoration(
color: AppColors.primaryGreen.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'Active',
style: TextStyle(
color: AppColors.primaryGreen,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
],
],
),
);
}).toList(),
onChanged: (val) {
if (val != null) {
HapticFeedback.selectionClick();
ref.read(transparencyBoardYearProvider.notifier).state = val;
}
},
+4 -60
View File
@@ -17,14 +17,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.3.1"
ansicolor:
dependency: transitive
description:
name: ansicolor
sha256: "50e982d500bc863e1d703448afdbf9e5a72eb48840a4f766fa361ffd6877055f"
url: "https://pub.dev"
source: hosted
version: "2.0.3"
archive:
dependency: transitive
description:
@@ -141,18 +133,10 @@ packages:
dependency: transitive
description:
name: change
sha256: "3bda1ce98526b21927cdea05688563c7065fd7e66d1abbe176c354fef3b2057b"
sha256: "65db7f966dc7e786687f49900a94c5f08b0eb9ca8c4a3e7eed3a55e980b455e2"
url: "https://pub.dev"
source: hosted
version: "0.7.5"
change_app_package_name:
dependency: "direct dev"
description:
name: change_app_package_name
sha256: "8e43b754fe960426904d77ed4c62fa8c9834deaf6e293ae40963fa447482c4c5"
url: "https://pub.dev"
source: hosted
version: "1.5.0"
version: "0.7.4"
characters:
dependency: transitive
description:
@@ -233,14 +217,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.7"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
data_table_2:
dependency: "direct main"
description:
@@ -305,14 +281,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.8"
excel:
dependency: "direct main"
description:
name: excel
sha256: "1a15327dcad260d5db21d1f6e04f04838109b39a2f6a84ea486ceda36e468780"
url: "https://pub.dev"
source: hosted
version: "4.0.6"
fake_async:
dependency: transitive
description:
@@ -462,14 +430,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.2.0"
flutter_native_splash:
dependency: "direct dev"
description:
name: flutter_native_splash
sha256: "7062602e0dbd29141fb8eb19220b5871ca650be5197ab9c1f193a28b17537bc7"
url: "https://pub.dev"
source: hosted
version: "2.4.4"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@@ -576,14 +536,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.2"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http:
dependency: transitive
description:
@@ -612,10 +564,10 @@ packages:
dependency: "direct main"
description:
name: intl
sha256: "1ca20c894b1717686a2319b8548763d812bc0aabdac580420a44c5178c57a867"
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.20.3"
version: "0.19.0"
jni:
dependency: transitive
description:
@@ -1181,14 +1133,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.4.0"
universal_io:
dependency: transitive
description:
name: universal_io
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
url: "https://pub.dev"
source: hosted
version: "2.3.1"
universal_platform:
dependency: transitive
description:
-15
View File
@@ -60,7 +60,6 @@ dependencies:
audioplayers: ^6.8.1
package_info_plus: ^8.1.0
device_info_plus: ^12.4.0
excel: ^4.0.6
dev_dependencies:
flutter_test:
@@ -73,15 +72,9 @@ dev_dependencies:
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^6.0.0
change_app_package_name: ^1.5.0
msix: ^3.16.13
flutter_native_splash: ^2.4.4
cider: ^0.2.10
dependency_overrides:
google_fonts: ^8.1.0
intl: ^0.20.3
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@@ -140,14 +133,6 @@ flutter_launcher_icons:
adaptive_icon_background: "#FFFFFF"
adaptive_icon_foreground: "assets/images/logo_foreground.png"
flutter_native_splash:
color: "#FFFFFF"
image: "assets/images/logo.png"
android: false
web: false
msix_config:
display_name: TIS RMS
publisher_display_name: BSIT3DSB