feat(reports): refine filters and export layout, add backend Excel export, optimize logo to webp
This commit is contained in:
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 });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
+1731
-368
File diff suppressed because it is too large.
Load diff
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user