diff --git a/backend/index.js b/backend/index.js
index 21de5dd..76dc9a2 100644
--- a/backend/index.js
+++ b/backend/index.js
@@ -1,8 +1,10 @@
process.env.TZ = 'Asia/Manila';
const express = require('express');
+const http = require('http');
const cors = require('cors');
const morgan = require('morgan');
const path = require('path');
+const { initSocket } = require('./src/services/socketService');
require('dotenv').config({ path: path.join(__dirname, '.env'), override: true });
require('dotenv').config({ path: path.join(__dirname, '..', '.env'), override: true });
@@ -32,8 +34,12 @@ const settingsRoutes = require('./src/routes/settings');
const templateRoutes = require('./src/routes/templates');
const app = express();
+const server = http.createServer(app);
const PORT = process.env.PORT || 18484;
+// Initialize Socket.IO Real-Time Server
+initSocket(server);
+
// Initialize Database
initSchema();
@@ -67,7 +73,7 @@ app.get(['/', '/api'], (req, res) => {
res.json({ message: 'TIS RMS API is running' });
});
-app.listen(PORT, '0.0.0.0', () => {
+server.listen(PORT, '0.0.0.0', () => {
console.log(`Server running on port ${PORT} (0.0.0.0)`);
// Run auto-graduation check on startup and daily
try {
diff --git a/backend/package-lock.json b/backend/package-lock.json
index 5714ace..f673ac2 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -25,6 +25,7 @@
"pdfkit": "^0.20.2",
"quickemailverification": "^1.0.4",
"sharp": "^0.35.4",
+ "socket.io": "^4.8.4",
"unzipper": "^0.12.5"
},
"devDependencies": {
@@ -1100,6 +1101,12 @@
"license": "BSD-3-Clause",
"optional": true
},
+ "node_modules/@socket.io/component-emitter": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
+ "license": "MIT"
+ },
"node_modules/@swc/helpers": {
"version": "0.5.23",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz",
@@ -1126,6 +1133,15 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@types/cors": {
+ "version": "2.8.19",
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
@@ -1168,6 +1184,15 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@types/ws": {
+ "version": "8.18.2",
+ "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.2.tgz",
+ "integrity": "sha512-67MQl+fpWKVTT1NYdnmo3U4sc/xPo/zQBncVnI74qmQa0z/b+1g6iYqNmGCPbxO+zz2aklb08a0oHfegiVd0/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@@ -2481,6 +2506,78 @@
"once": "^1.4.0"
}
},
+ "node_modules/engine.io": {
+ "version": "6.6.11",
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.11.tgz",
+ "integrity": "sha512-z34mGpLoY/82ZbkX1LqGMIaf3wjVMPL6gVC681QDqnmzJ26synI8qAE32P+a7UazmpJZ8UGrFIeAavgRfXzYrg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/cors": "^2.8.12",
+ "@types/node": ">=10.0.0",
+ "@types/ws": "^8.5.12",
+ "accepts": "~1.3.4",
+ "cookie": "~0.7.2",
+ "cors": "~2.8.5",
+ "debug": "~4.4.1",
+ "engine.io-parser": "~5.2.1",
+ "ws": "~8.21.0"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/engine.io-parser": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/engine.io/node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/engine.io/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/engine.io/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/engine.io/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -5816,6 +5913,89 @@
"node": ">=10"
}
},
+ "node_modules/socket.io": {
+ "version": "4.8.4",
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.4.tgz",
+ "integrity": "sha512-9ZV9zg8vN8hO3Y25DhR7nUkJ1VDnBhq5O/JrXhFT9s6+F0gqytSASTHVfCFc2mJXaTfARroErplXlzFWwETZqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.4",
+ "cors": "~2.8.5",
+ "debug": "~4.4.1",
+ "engine.io": "~6.6.0",
+ "socket.io-adapter": "~2.5.2",
+ "socket.io-parser": "~4.2.4"
+ },
+ "engines": {
+ "node": ">=10.2.0"
+ }
+ },
+ "node_modules/socket.io-adapter": {
+ "version": "2.5.8",
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.8.tgz",
+ "integrity": "sha512-6Oy52pbg+kvdCVvjcN+FnY7BvxZ7cIHNScbvztT/It5d0vbwoJoVZmF2gjJmnV0/4WlXRfG15zc45ySk9Ah8bw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "~4.4.1",
+ "ws": "~8.21.0"
+ }
+ },
+ "node_modules/socket.io-parser": {
+ "version": "4.2.7",
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.7.tgz",
+ "integrity": "sha512-IH/iSeO9T6gz1KkFleGDWkG9N3dl4jXVYUtMhIqH10Md0ttMer8nUNWiP1DKuNrybD2xBrixLJdCC9J6ECoYkg==",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/component-emitter": "~3.1.0",
+ "debug": "~4.4.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/socket.io/node_modules/accepts": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/socket.io/node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/socket.io/node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/socket.io/node_modules/negotiator": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/sshpk": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz",
@@ -6467,6 +6647,27 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
},
+ "node_modules/ws": {
+ "version": "8.21.3",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
+ "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/xml-naming": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.3.0.tgz",
diff --git a/backend/package.json b/backend/package.json
index 595246d..d540e10 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -35,6 +35,7 @@
"pdfkit": "^0.20.2",
"quickemailverification": "^1.0.4",
"sharp": "^0.35.4",
+ "socket.io": "^4.8.4",
"unzipper": "^0.12.5"
},
"devDependencies": {
diff --git a/backend/src/controllers/archiveController.js b/backend/src/controllers/archiveController.js
index 717e2f9..a784a71 100644
--- a/backend/src/controllers/archiveController.js
+++ b/backend/src/controllers/archiveController.js
@@ -216,13 +216,20 @@ exports.getArchivedDocuments = (req, res) => {
if (types.length > 0) {
const typeConditions = [];
for (const t of types) {
- if (t === 'All JHS') {
+ if (t === 'All JHS' || t === 'All JHS Requirements') {
typeConditions.push(`dr.category = 'JHS'`);
- } else if (t === 'All SHS') {
+ } else if (t === 'All SHS' || t === 'All SHS Requirements') {
typeConditions.push(`dr.category = 'SHS'`);
} else {
- typeConditions.push(`(d.document_type = ? OR dr.name = ?)`);
- params.push(t, t);
+ const cleanT = t.replace(/^(JHS|SHS)\s*-\s*/i, '').trim();
+ typeConditions.push(`(
+ d.document_type = ?
+ OR dr.name = ?
+ OR d.document_type = ?
+ OR d.document_type = ?
+ OR dr.name = ?
+ )`);
+ params.push(t, t, `JHS - ${cleanT}`, `SHS - ${cleanT}`, cleanT);
}
}
conditions.push(`(${typeConditions.join(' OR ')})`);
diff --git a/backend/src/controllers/documentController.js b/backend/src/controllers/documentController.js
index 44387a9..6c60ea5 100644
--- a/backend/src/controllers/documentController.js
+++ b/backend/src/controllers/documentController.js
@@ -450,13 +450,20 @@ exports.getAllDocuments = (req, res) => {
if (types.length > 0) {
const typeConditions = [];
for (const t of types) {
- if (t === 'All JHS') {
+ if (t === 'All JHS' || t === 'All JHS Requirements') {
typeConditions.push(`dr.category = 'JHS'`);
- } else if (t === 'All SHS') {
+ } else if (t === 'All SHS' || t === 'All SHS Requirements') {
typeConditions.push(`dr.category = 'SHS'`);
} else {
- typeConditions.push(`(d.document_type = ? OR dr.name = ?)`);
- params.push(t, t);
+ const cleanT = t.replace(/^(JHS|SHS)\s*-\s*/i, '').trim();
+ typeConditions.push(`(
+ d.document_type = ?
+ OR dr.name = ?
+ OR d.document_type = ?
+ OR d.document_type = ?
+ OR dr.name = ?
+ )`);
+ params.push(t, t, `JHS - ${cleanT}`, `SHS - ${cleanT}`, cleanT);
}
}
conditions.push(`(${typeConditions.join(' OR ')})`);
@@ -598,6 +605,60 @@ exports.getStatuses = (req, res) => {
}
};
+exports.getDocumentTypes = (req, res) => {
+ try {
+ const reqRows = db.prepare(`
+ SELECT DISTINCT name, category
+ FROM document_requirements
+ WHERE is_enabled = 1
+ ORDER BY name ASC
+ `).all();
+
+ const docRows = db.prepare(`
+ SELECT DISTINCT document_type
+ FROM documents
+ WHERE document_type IS NOT NULL AND document_type != ''
+ ORDER BY document_type ASC
+ `).all();
+
+ const jhsSet = new Set();
+ const shsSet = new Set();
+ const generalSet = new Set();
+
+ for (const r of reqRows) {
+ const cleanName = r.name.replace(/^(JHS|SHS)\s*-\s*/i, '').trim();
+ if (r.category === 'JHS') {
+ jhsSet.add(cleanName);
+ } else if (r.category === 'SHS') {
+ shsSet.add(cleanName);
+ } else {
+ generalSet.add(cleanName);
+ }
+ }
+
+ for (const d of docRows) {
+ const dt = d.document_type.trim();
+ if (/^JHS\s*-\s*/i.test(dt)) {
+ jhsSet.add(dt.replace(/^JHS\s*-\s*/i, '').trim());
+ } else if (/^SHS\s*-\s*/i.test(dt)) {
+ shsSet.add(dt.replace(/^SHS\s*-\s*/i, '').trim());
+ } else if (dt) {
+ if (!jhsSet.has(dt) && !shsSet.has(dt)) {
+ generalSet.add(dt);
+ }
+ }
+ }
+
+ res.json({
+ jhs: Array.from(jhsSet).sort(),
+ shs: Array.from(shsSet).sort(),
+ general: Array.from(generalSet).sort(),
+ });
+ } catch (error) {
+ res.status(500).json({ message: 'Failed to fetch document types', error: error.message });
+ }
+};
+
// ============================================================
// GET /api/documents/student/:studentId — get single student's documents
// ============================================================
diff --git a/backend/src/controllers/notificationController.js b/backend/src/controllers/notificationController.js
index bcbcfa8..4a41f2f 100644
--- a/backend/src/controllers/notificationController.js
+++ b/backend/src/controllers/notificationController.js
@@ -49,6 +49,21 @@ exports.createNotification = (userId, title, message, category = 'system', entit
entityId,
notificationId: notifId
}).catch(() => {});
+
+ // Real-time WebSocket notification emission to active clients
+ try {
+ const { emitNotificationCreated } = require('../services/socketService');
+ emitNotificationCreated({
+ id: notifId,
+ userId: userId || null,
+ title,
+ message,
+ category,
+ entityType,
+ entityId,
+ createdAt: new Date().toISOString(),
+ });
+ } catch (_) {}
};
// POST /api/notifications/fcm-token
diff --git a/backend/src/controllers/studentController.js b/backend/src/controllers/studentController.js
index 552e7bd..3123705 100644
--- a/backend/src/controllers/studentController.js
+++ b/backend/src/controllers/studentController.js
@@ -2,6 +2,11 @@ const db = require('../config/db');
const fs = require('fs');
const path = require('path');
const { createNotification } = require('./notificationController');
+const {
+ emitStudentAdded,
+ emitStudentUpdated,
+ emitStudentDeleted,
+} = require('../services/socketService');
// ============================================================
// HELPER — insert one row into activity_log
@@ -534,6 +539,7 @@ exports.createStudent = (req, res) => {
logActivity(req.user?.id, 'CREATE', 'student', newId, `Created student ${firstName} ${lastName}`);
createNotification(null, 'Student Created', `New student ${firstName} ${lastName} (LRN: ${lrn.trim()}) has been enrolled.`, 'student', 'student', newId);
+ emitStudentAdded({ id: newId, lrn: lrn.trim(), firstName, lastName });
res.status(201).json({
id: newId,
@@ -605,6 +611,7 @@ exports.bulkGraduate = (req, res) => {
`${count} student(s) status updated to Graduated.`,
'student'
);
+ emitStudentUpdated({ action: 'bulkGraduate', count });
}
res.json({ message: `${count} student(s) successfully graduated.` });
@@ -751,6 +758,7 @@ exports.updateStudent = (req, res) => {
logActivity(req.user?.id, 'UPDATE', 'student', id, `Updated student ${maskLrn(lrn)}`);
createNotification(null, 'Student Updated', `Student ${firstName} ${lastName} (LRN: ${lrn.trim()}) record was updated.`, 'student', 'student', id);
+ emitStudentUpdated({ id, lrn: lrn.trim() });
res.json({ message: 'Student updated successfully.' });
} catch (error) {
console.error('updateStudent error:', error);
@@ -780,6 +788,7 @@ exports.deleteStudent = (req, res) => {
logActivity(req.user?.id, 'DELETE', 'student', id, `Deleted student: ${student.first_name} ${student.last_name} (LRN: ${maskLrn(student.lrn)})`);
createNotification(null, 'Student Deleted', `Student ${student.first_name} ${student.last_name} was removed.`, 'student', 'student', id);
+ emitStudentDeleted({ id });
res.json({ message: 'Student deleted successfully.' });
} catch (error) {
console.error('deleteStudent error:', error);
@@ -897,6 +906,7 @@ exports.bulkStatusStudents = (req, res) => {
? `Updated ${studentRows.length} students\n${studentListText}`
: (studentRows.length === 1 ? `Updated 1 student ${studentRows[0].last_name || ''} - ${maskLrn(studentRows[0].lrn)}` : `Bulk updated status to "${status}" for ${studentIds.length} students`);
logActivity(req.user?.id, 'UPDATE', 'student', null, bulkDesc);
+ emitStudentUpdated({ action: 'bulkStatus', studentIds, status });
res.json({ message: `Successfully updated status to "${status}" for ${studentIds.length} students.` });
} catch (error) {
console.error('bulkStatusStudents error:', error);
@@ -933,6 +943,7 @@ exports.addEnrollment = (req, res) => {
const lrn = studentRow?.lrn || studentId;
logActivity(req.user?.id, 'CREATE', 'enrollment', info.lastInsertRowid, getEnrollmentLogDesc(academicYearId, gradeLevel, sectionId, lrn));
createNotification(null, 'Student Enrolled', `Student (LRN: ${lrn}) was enrolled in a new section.`, 'student', 'student', studentId);
+ emitStudentUpdated({ id: studentId });
res.status(201).json({ message: 'Enrollment added successfully', id: info.lastInsertRowid });
} catch (error) {
console.error('addEnrollment error:', error);
@@ -969,6 +980,7 @@ exports.updateEnrollment = (req, res) => {
const studentRow = db.prepare('SELECT lrn FROM students WHERE id = ?').get(existing.student_id);
const lrn = studentRow?.lrn || existing.student_id;
logActivity(req.user?.id, 'UPDATE', 'enrollment', enrollmentId, getEnrollmentLogDesc(academicYearId, gradeLevel, sectionId, lrn));
+ emitStudentUpdated({ id: existing.student_id });
res.json({ message: 'Enrollment updated successfully' });
} catch (error) {
console.error('updateEnrollment error:', error);
@@ -990,6 +1002,7 @@ exports.deleteEnrollment = (req, res) => {
const studentRow = db.prepare('SELECT lrn FROM students WHERE id = ?').get(existing.student_id);
const lrn = studentRow?.lrn || existing.student_id;
logActivity(req.user?.id, 'DELETE', 'enrollment', enrollmentId, getEnrollmentLogDesc(existing.academic_year_id, existing.grade_level, existing.section_id, lrn));
+ emitStudentUpdated({ id: existing.student_id });
res.json({ message: 'Enrollment deleted successfully' });
} catch (error) {
console.error('deleteEnrollment error:', error);
@@ -1209,6 +1222,7 @@ exports.bulkCreateStudents = (req, res) => {
`${created} student(s) imported via OCR Bulk Import.`,
'student'
);
+ emitStudentAdded({ action: 'bulkCreate', count: created });
}
res.status(207).json({ created, skipped, failed, results });
diff --git a/backend/src/controllers/userController.js b/backend/src/controllers/userController.js
index bd9e9b9..5b0b6ef 100644
--- a/backend/src/controllers/userController.js
+++ b/backend/src/controllers/userController.js
@@ -153,8 +153,27 @@ exports.createUser = async (req, res) => {
}
}
- // Temporary password = username + 123 (e.g. ccharles123)
- const temporaryPassword = `${username}123`;
+ // Generate secure randomized temporary password for admin users, or username123 for standard users
+ let temporaryPassword;
+ if (role === 'admin') {
+ const uppercase = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
+ const lowercase = 'abcdefghjkmnpqrstuvwxyz';
+ const digits = '23456789';
+ const symbols = '!@#$%&*';
+ const allChars = uppercase + lowercase + digits + symbols;
+ const pwdParts = [
+ uppercase[crypto.randomInt(0, uppercase.length)],
+ lowercase[crypto.randomInt(0, lowercase.length)],
+ digits[crypto.randomInt(0, digits.length)],
+ symbols[crypto.randomInt(0, symbols.length)],
+ ];
+ for (let i = 4; i < 10; i++) {
+ pwdParts.push(allChars[crypto.randomInt(0, allChars.length)]);
+ }
+ temporaryPassword = pwdParts.sort(() => crypto.randomInt(-1, 2)).join('');
+ } else {
+ temporaryPassword = `${username}123`;
+ }
try {
const existing = db.prepare('SELECT id FROM users WHERE username = ?').get(username);
diff --git a/backend/src/routes/documents.js b/backend/src/routes/documents.js
index c0f6c62..6538c86 100644
--- a/backend/src/routes/documents.js
+++ b/backend/src/routes/documents.js
@@ -6,6 +6,7 @@ const { authenticateToken, authorizeRoles } = require('../middleware/auth');
router.get('/', authenticateToken, documentController.getAllDocuments);
router.get('/requirements', authenticateToken, documentController.getRequirements);
router.get('/statuses', authenticateToken, documentController.getStatuses);
+router.get('/types', authenticateToken, documentController.getDocumentTypes);
// Print Queue routes — must be BEFORE /:id routes to avoid conflict
router.get('/print-queue', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.getPrintQueue);
diff --git a/backend/src/services/socketService.js b/backend/src/services/socketService.js
new file mode 100644
index 0000000..d2f753e
--- /dev/null
+++ b/backend/src/services/socketService.js
@@ -0,0 +1,96 @@
+const { Server } = require('socket.io');
+const jwt = require('jsonwebtoken');
+
+let io = null;
+
+/**
+ * Initializes Socket.IO on the given HTTP server.
+ * Supports both WebSocket and polling transports with open CORS for LAN and Cloudflare tunnel clients.
+ */
+function initSocket(httpServer) {
+ io = new Server(httpServer, {
+ cors: {
+ origin: '*',
+ methods: ['GET', 'POST'],
+ credentials: true,
+ },
+ transports: ['websocket', 'polling'],
+ allowEIO3: true,
+ });
+
+ // Optional auth middleware: verifies JWT if supplied, but does not block connection
+ io.use((socket, next) => {
+ const token =
+ socket.handshake.auth?.token ||
+ socket.handshake.headers?.authorization?.replace(/^Bearer\s+/i, '');
+
+ if (token) {
+ try {
+ const secret = process.env.JWT_SECRET || 'tis-rms-jwt-secret-key-development';
+ const decoded = jwt.verify(token, secret);
+ socket.user = decoded;
+ } catch (err) {
+ console.warn(`[Socket.IO] Token verification notice for socket ${socket.id}: ${err.message}`);
+ }
+ }
+ next();
+ });
+
+ io.on('connection', (socket) => {
+ const userDesc = socket.user ? `@${socket.user.username} (${socket.user.role})` : 'client';
+ console.log(`[Socket.IO] Client connected: ${socket.id} [${userDesc}]`);
+
+ socket.on('disconnect', (reason) => {
+ console.log(`[Socket.IO] Client disconnected: ${socket.id} [${reason}]`);
+ });
+ });
+
+ console.log('[Socket.IO] Real-time WebSocket server initialized successfully.');
+ return io;
+}
+
+function getIO() {
+ return io;
+}
+
+function broadcastEvent(eventName, payload) {
+ if (!io) return;
+ try {
+ io.emit(eventName, payload);
+ } catch (err) {
+ console.error(`[Socket.IO] Failed to emit "${eventName}":`, err.message);
+ }
+}
+
+// Real-Time Student Event Emitters
+function emitStudentAdded(data) {
+ broadcastEvent('student_added', data);
+}
+
+function emitStudentUpdated(data) {
+ broadcastEvent('student_updated', data);
+}
+
+function emitStudentDeleted(data) {
+ broadcastEvent('student_deleted', data);
+}
+
+// Real-Time Notification & Document Emitters
+function emitNotificationCreated(data) {
+ broadcastEvent('notification_created', data);
+}
+
+function emitDocumentChanged(data) {
+ broadcastEvent('document_changed', data);
+}
+
+module.exports = {
+ initSocket,
+ getIO,
+ broadcastEvent,
+ emitStudentAdded,
+ emitStudentUpdated,
+ emitStudentDeleted,
+ emitNotificationCreated,
+ emitDocumentChanged,
+};
diff --git a/documentation/app.js b/documentation/app.js
new file mode 100644
index 0000000..149268e
--- /dev/null
+++ b/documentation/app.js
@@ -0,0 +1,295 @@
+/**
+ * TIS-RMS DOCUMENTATION INTERACTIVE CONTROLLER
+ * Handles:
+ * 1. Interactive Walkthrough Reel / Video Player
+ * 2. Mockup Platform Tab Switching & Header Platform Filtering
+ * 3. Live Client-Side Search with Highlighting
+ * 4. Image Lightbox Zoom Modal
+ * 5. Theme Toggle (Dark/Light) with LocalStorage
+ * 6. Sidebar Scrollspy Navigation
+ * 7. Print Handler & Global Keyboard Shortcuts
+ */
+
+document.addEventListener('DOMContentLoaded', () => {
+
+ // ==========================================
+ // 1. THEME TOGGLE
+ // ==========================================
+ const themeToggleBtn = document.getElementById('themeToggleBtn');
+ const savedTheme = localStorage.getItem('tis_doc_theme') || 'light';
+ document.documentElement.setAttribute('data-theme', savedTheme);
+
+ themeToggleBtn.addEventListener('click', () => {
+ const currentTheme = document.documentElement.getAttribute('data-theme');
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
+ document.documentElement.setAttribute('data-theme', newTheme);
+ localStorage.setItem('tis_doc_theme', newTheme);
+ });
+
+ // ==========================================
+ // 2. PRINT HANDLER
+ // ==========================================
+ const printBtn = document.getElementById('printBtn');
+ if (printBtn) {
+ printBtn.addEventListener('click', () => {
+ window.print();
+ });
+ }
+
+ // ==========================================
+ // 2B. MOBILE NAVIGATION & SEARCH
+ // ==========================================
+ const mobileMenuBtn = document.getElementById('mobileMenuBtn');
+ const docSidebar = document.getElementById('docSidebar');
+ const sidebarBackdrop = document.getElementById('sidebarBackdrop');
+ const mobileSearchBtn = document.getElementById('mobileSearchBtn');
+ const headerSearchBox = document.getElementById('headerSearchBox');
+ const docSearchInput = document.getElementById('docSearchInput');
+
+ function openMobileSidebar() {
+ if (docSidebar) docSidebar.classList.add('open');
+ if (sidebarBackdrop) sidebarBackdrop.classList.add('active');
+ if (mobileMenuBtn) {
+ const ham = mobileMenuBtn.querySelector('.hamburger-icon');
+ const close = mobileMenuBtn.querySelector('.close-icon');
+ if (ham) ham.style.display = 'none';
+ if (close) close.style.display = 'block';
+ }
+ document.body.style.overflow = 'hidden';
+ }
+
+ function closeMobileSidebar() {
+ if (docSidebar) docSidebar.classList.remove('open');
+ if (sidebarBackdrop) sidebarBackdrop.classList.remove('active');
+ if (mobileMenuBtn) {
+ const ham = mobileMenuBtn.querySelector('.hamburger-icon');
+ const close = mobileMenuBtn.querySelector('.close-icon');
+ if (ham) ham.style.display = 'block';
+ if (close) close.style.display = 'none';
+ }
+ document.body.style.overflow = '';
+ }
+
+ if (mobileMenuBtn) {
+ mobileMenuBtn.addEventListener('click', () => {
+ const isOpen = docSidebar && docSidebar.classList.contains('open');
+ if (isOpen) {
+ closeMobileSidebar();
+ } else {
+ openMobileSidebar();
+ }
+ });
+ }
+
+ if (sidebarBackdrop) {
+ sidebarBackdrop.addEventListener('click', closeMobileSidebar);
+ }
+
+ // Close mobile sidebar when clicking any navigation link
+ document.querySelectorAll('.doc-sidebar .nav-link').forEach(link => {
+ link.addEventListener('click', () => {
+ if (window.innerWidth <= 1024) {
+ closeMobileSidebar();
+ }
+ });
+ });
+
+ // Mobile search toggle
+ if (mobileSearchBtn && headerSearchBox) {
+ mobileSearchBtn.addEventListener('click', () => {
+ const isOpen = headerSearchBox.classList.toggle('mobile-open');
+ if (isOpen && docSearchInput) {
+ docSearchInput.focus();
+ }
+ });
+ }
+
+ // ==========================================
+ // 3. FAQ ACCORDION HANDLER
+ // ==========================================
+ document.querySelectorAll('.faq-item').forEach(item => {
+ const questionBtn = item.querySelector('.faq-question');
+ if (questionBtn) {
+ questionBtn.addEventListener('click', () => {
+ const isActive = item.classList.contains('active');
+ item.classList.toggle('active', !isActive);
+ });
+ }
+ });
+
+ // ==========================================
+ // 4. MOCKUP TAB SWITCHING (WINDOWS / ANDROID)
+ // ==========================================
+ document.querySelectorAll('.mockup-container').forEach(container => {
+ const tabBtns = container.querySelectorAll('.mockup-tab-btn');
+ const panes = container.querySelectorAll('.mockup-pane');
+
+ tabBtns.forEach(btn => {
+ btn.addEventListener('click', () => {
+ const targetId = btn.getAttribute('data-tab-target');
+
+ tabBtns.forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+
+ panes.forEach(pane => {
+ if (pane.id === targetId) {
+ pane.style.display = 'block';
+ pane.classList.add('active');
+ } else {
+ pane.style.display = 'none';
+ pane.classList.remove('active');
+ }
+ });
+ });
+ });
+ });
+
+ // ==========================================
+ // 5. GLOBAL PLATFORM FILTER IN HEADER
+ // ==========================================
+ const platformFilterBtns = document.querySelectorAll('.platform-pill-toggle .platform-btn');
+ platformFilterBtns.forEach(btn => {
+ btn.addEventListener('click', () => {
+ platformFilterBtns.forEach(b => b.classList.remove('active'));
+ btn.classList.add('active');
+
+ const filter = btn.getAttribute('data-platform-filter');
+
+ document.querySelectorAll('.mockup-container').forEach(container => {
+ const winTabBtn = container.querySelector('[data-tab-target^="win-"]');
+ const andTabBtn = container.querySelector('[data-tab-target^="and-"]');
+
+ if (filter === 'windows' && winTabBtn) {
+ winTabBtn.click();
+ } else if (filter === 'android' && andTabBtn) {
+ andTabBtn.click();
+ } else if (filter === 'all' && winTabBtn) {
+ winTabBtn.click();
+ }
+ });
+ });
+ });
+
+ // ==========================================
+ // 6. LIGHTBOX ZOOM MODAL
+ // ==========================================
+ const lightboxModal = document.getElementById('lightboxModal');
+ const lightboxImg = document.getElementById('lightboxImg');
+ const lightboxCaption = document.getElementById('lightboxCaption');
+ const lightboxCloseBtn = document.getElementById('lightboxCloseBtn');
+
+ function openLightbox(src, caption) {
+ if (!lightboxModal || !lightboxImg) return;
+ lightboxImg.src = src;
+ if (lightboxCaption) {
+ lightboxCaption.textContent = caption || '';
+ }
+ lightboxModal.classList.add('active');
+ document.body.style.overflow = 'hidden';
+ }
+
+ function closeLightbox() {
+ if (!lightboxModal) return;
+ lightboxModal.classList.remove('active');
+ document.body.style.overflow = '';
+ }
+
+ document.querySelectorAll('[data-lightbox]').forEach(el => {
+ el.addEventListener('click', () => {
+ const src = el.getAttribute('data-lightbox');
+ const caption = el.getAttribute('data-caption') || '';
+ openLightbox(src, caption);
+ });
+ });
+
+ if (lightboxCloseBtn) {
+ lightboxCloseBtn.addEventListener('click', closeLightbox);
+ }
+
+ if (lightboxModal) {
+ lightboxModal.addEventListener('click', (e) => {
+ if (e.target === lightboxModal) {
+ closeLightbox();
+ }
+ });
+ }
+
+ // ==========================================
+ // 7. LIVE SEARCH WITH HIGHLIGHTING & SHORTCUT
+ // ==========================================
+ const searchInput = document.getElementById('docSearchInput');
+ const searchableSections = document.querySelectorAll('.doc-section, .hero-banner');
+
+ if (searchInput) {
+ searchInput.addEventListener('input', (e) => {
+ const query = e.target.value.trim().toLowerCase();
+
+ if (!query) {
+ searchableSections.forEach(sec => {
+ sec.style.display = '';
+ sec.querySelectorAll('.feature-card, .step-card, tr').forEach(item => {
+ item.style.display = '';
+ });
+ });
+ return;
+ }
+
+ searchableSections.forEach(sec => {
+ const text = sec.textContent.toLowerCase();
+ if (text.includes(query)) {
+ sec.style.display = '';
+
+ // Filter internal child cards
+ sec.querySelectorAll('.feature-card, .step-card').forEach(card => {
+ const cardText = card.textContent.toLowerCase();
+ card.style.display = cardText.includes(query) ? '' : 'none';
+ });
+ } else {
+ sec.style.display = 'none';
+ }
+ });
+ });
+
+ // Global keyboard shortcut: Ctrl+K or / focuses search
+ window.addEventListener('keydown', (e) => {
+ if ((e.ctrlKey && e.key === 'k') || (e.key === '/' && document.activeElement !== searchInput)) {
+ e.preventDefault();
+ searchInput.focus();
+ searchInput.select();
+ } else if (e.key === 'Escape') {
+ if (lightboxModal && lightboxModal.classList.contains('active')) {
+ closeLightbox();
+ } else if (document.activeElement === searchInput) {
+ searchInput.blur();
+ }
+ }
+ });
+ }
+
+ // ==========================================
+ // 8. SCROLLSPY NAVIGATION
+ // ==========================================
+ const navLinks = document.querySelectorAll('.doc-sidebar .nav-link');
+ const sections = document.querySelectorAll('.hero-banner, .doc-section');
+
+ function updateActiveNavLink() {
+ const scrollY = window.pageYOffset || document.documentElement.scrollTop;
+
+ sections.forEach(section => {
+ const sectionTop = section.offsetTop - 120;
+ const sectionHeight = section.offsetHeight;
+ const sectionId = section.getAttribute('id');
+
+ if (scrollY >= sectionTop && scrollY < sectionTop + sectionHeight) {
+ navLinks.forEach(link => {
+ link.classList.remove('active');
+ if (link.getAttribute('href') === `#${sectionId}`) {
+ link.classList.add('active');
+ }
+ });
+ }
+ });
+ }
+
+ window.addEventListener('scroll', updateActiveNavLink, { passive: true });
+});
diff --git a/documentation/assets/images/android/android.webp b/documentation/assets/images/android/android.webp
new file mode 100644
index 0000000..a8a205c
Binary files /dev/null and b/documentation/assets/images/android/android.webp differ
diff --git a/documentation/assets/images/android/archives.webp b/documentation/assets/images/android/archives.webp
new file mode 100644
index 0000000..566d9e1
Binary files /dev/null and b/documentation/assets/images/android/archives.webp differ
diff --git a/documentation/assets/images/android/dashboard.webp b/documentation/assets/images/android/dashboard.webp
new file mode 100644
index 0000000..d291042
Binary files /dev/null and b/documentation/assets/images/android/dashboard.webp differ
diff --git a/documentation/assets/images/android/documents.webp b/documentation/assets/images/android/documents.webp
new file mode 100644
index 0000000..4de673c
Binary files /dev/null and b/documentation/assets/images/android/documents.webp differ
diff --git a/documentation/assets/images/android/history.webp b/documentation/assets/images/android/history.webp
new file mode 100644
index 0000000..ecd2696
Binary files /dev/null and b/documentation/assets/images/android/history.webp differ
diff --git a/documentation/assets/images/android/login.webp b/documentation/assets/images/android/login.webp
new file mode 100644
index 0000000..a8a205c
Binary files /dev/null and b/documentation/assets/images/android/login.webp differ
diff --git a/documentation/assets/images/android/reports.webp b/documentation/assets/images/android/reports.webp
new file mode 100644
index 0000000..760465d
Binary files /dev/null and b/documentation/assets/images/android/reports.webp differ
diff --git a/documentation/assets/images/android/settings.webp b/documentation/assets/images/android/settings.webp
new file mode 100644
index 0000000..4c872ad
Binary files /dev/null and b/documentation/assets/images/android/settings.webp differ
diff --git a/documentation/assets/images/android/students.webp b/documentation/assets/images/android/students.webp
new file mode 100644
index 0000000..a14c239
Binary files /dev/null and b/documentation/assets/images/android/students.webp differ
diff --git a/documentation/assets/images/android/users.webp b/documentation/assets/images/android/users.webp
new file mode 100644
index 0000000..ef84dcb
Binary files /dev/null and b/documentation/assets/images/android/users.webp differ
diff --git a/documentation/assets/images/windows/archives.webp b/documentation/assets/images/windows/archives.webp
new file mode 100644
index 0000000..fafe2f8
Binary files /dev/null and b/documentation/assets/images/windows/archives.webp differ
diff --git a/documentation/assets/images/windows/dashboard.webp b/documentation/assets/images/windows/dashboard.webp
new file mode 100644
index 0000000..10bc16a
Binary files /dev/null and b/documentation/assets/images/windows/dashboard.webp differ
diff --git a/documentation/assets/images/windows/documents.webp b/documentation/assets/images/windows/documents.webp
new file mode 100644
index 0000000..7c7609e
Binary files /dev/null and b/documentation/assets/images/windows/documents.webp differ
diff --git a/documentation/assets/images/windows/history.webp b/documentation/assets/images/windows/history.webp
new file mode 100644
index 0000000..ea5050f
Binary files /dev/null and b/documentation/assets/images/windows/history.webp differ
diff --git a/documentation/assets/images/windows/login.webp b/documentation/assets/images/windows/login.webp
new file mode 100644
index 0000000..51aeb8a
Binary files /dev/null and b/documentation/assets/images/windows/login.webp differ
diff --git a/documentation/assets/images/windows/reports.webp b/documentation/assets/images/windows/reports.webp
new file mode 100644
index 0000000..85df427
Binary files /dev/null and b/documentation/assets/images/windows/reports.webp differ
diff --git a/documentation/assets/images/windows/settings.webp b/documentation/assets/images/windows/settings.webp
new file mode 100644
index 0000000..34369a8
Binary files /dev/null and b/documentation/assets/images/windows/settings.webp differ
diff --git a/documentation/assets/images/windows/students.webp b/documentation/assets/images/windows/students.webp
new file mode 100644
index 0000000..bf1ac98
Binary files /dev/null and b/documentation/assets/images/windows/students.webp differ
diff --git a/documentation/assets/images/windows/users.webp b/documentation/assets/images/windows/users.webp
new file mode 100644
index 0000000..afa154a
Binary files /dev/null and b/documentation/assets/images/windows/users.webp differ
diff --git a/documentation/assets/logo.png b/documentation/assets/logo.png
new file mode 100644
index 0000000..35a8200
Binary files /dev/null and b/documentation/assets/logo.png differ
diff --git a/documentation/index.html b/documentation/index.html
new file mode 100644
index 0000000..2490c15
--- /dev/null
+++ b/documentation/index.html
@@ -0,0 +1,1332 @@
+
+
+
+
+
+ TIS-RMS User Guide & System Documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ System Manual & Operational Reference
+ Talisay Integrated School - Records Management System
+
+ An institutional records management and document archiving system built for secondary schools. Supports offline SQLite operation, automated DepEd SF10 PDF compiling, multi-page batch scanning, and role-based access control.
+
+
+ Windows Desktop
+ Android Tablet
+ Offline Local Cache
+ RA 10173 Audit Ledger
+ DepEd SF10 Engine
+
+
+
+
+
+
+
+
+
+
+ Administrator: Unrestricted access to master data, user accounts, system configuration, permanent deletions, and school-wide reporting.
+ Teacher / Staff: Scoped access to assigned sections, student enrollment profiles, document scanning, and Form 138/SF9 grade entry.
+ Audit Logging: Every record access, print action, and modification is committed to an append-only audit trail with workstation IP and timestamp.
+
+
+
+
+ TIS-RMS separates duties between the school registrar (Administrator) and classroom advisors (Teachers). This ensures student records are safeguarded in accordance with DepEd orders and the Philippine Data Privacy Act of 2012 (RA 10173).
+
+
+
+
+
+
+ Operation / Capability
+ Administrator
+ Teacher / Staff
+ Enforcement Rule
+
+
+
+
+ Access System Dashboard
+ School-Wide
+ Assigned Section
+ Scoped by teacher ID
+
+
+ Create / Edit Student Profile
+ Allowed
+ Allowed
+ Logged in audit trail
+
+
+ Upload Scanned Documents (SF9, PSA)
+ Allowed
+ Allowed
+ File type & size validated
+
+
+ Archive Completed Records
+ Allowed
+ Restricted
+ Admin credential required
+
+
+ Delete Student / Document
+ Allowed
+ Restricted
+ Moved to Soft-Delete Trash
+
+
+ Batch Print SF10 / SF9 Records
+ Allowed
+ Allowed
+ Compiled via background queue
+
+
+ Modify Checklist Requirements
+ Allowed
+ Restricted
+ Settings configuration panel
+
+
+ Database Backup & Maintenance
+ Allowed
+ Restricted
+ Password/PIN re-verification
+
+
+
+
+
+
+
+
+
+
+ Password Management: User passwords are encrypted using bcrypt hashing. If a teacher forgets their credentials, the school administrator can reset their temporary password through the User Accounts screen.
+
+
+
+
+
+
+
+
+
+
+
+ Authentication Summary
+
+
+ Single Sign-On Terminal: Users sign in using their registered username and encrypted password.
+ Role Auto-Detection: Upon successful sign-in, the system automatically tailors the navigation drawer to either Administrator or Teacher permissions.
+ Local Server Connection: The Android tablet client connects directly to the school host machine via local IP configuration.
+
+
+
+
+
+
+ Windows Desktop
+ Android Tablet
+
+
+
+
+
+
+
TIS-RMS - Authentication Portal
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Summary Counters: Displays total enrolled students, total archived records, digitized pages, and folders with incomplete documents.
+ Demographic Graphs: Visualizes student distribution across grade levels (Grade 7 through Grade 12) and academic strands.
+ Activity Feed: Shows chronological stream of recent scans, edits, and status transitions.
+
+
+
+
+ The Dashboard serves as the central operational hub for both administrative staff and classroom advisers. Statistics are updated reactively from local SQLite cache queries, ensuring zero latency even when internet connectivity is unavailable.
+
+
+
+
+
+ Windows Desktop
+ Android Tablet
+
+
+
+
+
+
+
TIS-RMS - Dashboard & Analytics
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+ Dashboard Metric Breakdown
+
+
+
+
Real-Time Stat Counters
+
Active Enrollees, Archived Students, Total Scanned Pages, and Incomplete Student Folders updated instantaneously through local reactive streams.
+
+
+
+
Grade & Strand Ratios
+
Visual distribution graphs indicating cohort sizes across Grade 7 through Grade 12, allowing administrators to balance section loading.
+
+
+
+
Audit Stream Feed
+
Chronological log of document scans, grade updates, and folder status changes with user attribution and timestamping.
+
+
+
+
+
+
+
+
+
+
+
+ LRN Master Index: All records are keyed by DepEd's unique 12-digit Learner Reference Number to prevent duplication.
+ Multi-Select Toolbar: Batch operations to print, export to Excel, or archive multiple selected students simultaneously.
+ Missing Requirements Flag: Red status badges immediately indicate students with missing birth certificates or Form 137 records.
+
+
+
+
+ The Student module holds complete demographic and academic records. Search queries match against LRN, last name, first name, grade level, and section in real-time as you type.
+
+
+
+
+
+ Windows Desktop
+ Android Tablet
+
+
+
+
+
+
+
TIS-RMS - Students Master List
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+ Step-by-Step: Adding a New Student
+
+
+
1
+
+
Open Registration Form
+
Navigate to the Students screen and select Add Student in the upper right header or press keyboard shortcut Ctrl+N .
+
+
+
+
2
+
+
Provide Learner Profile
+
Enter the 12-digit LRN, legal name (Last, First, Middle, Suffix), date of birth, sex, grade level, section, and guardian contact numbers.
+
+
+
+
3
+
+
Automatic Folder Provisioning
+
The system creates a student folder with standard requirement slots: PSA Birth Certificate, SF9 (Form 138), Good Moral, and 2x2 Photo.
+
+
+
+
4
+
+
Save and Commit
+
Click Save Record . The student is immediately indexed in local SQLite storage and replicated across the local network.
+
+
+
+
+
+
+
+
+
+
+
+
+ Virtual Filing Cabinets: Organizes student documents by School Year, Grade Level, and Section without file path conflicts.
+ Standard DepEd Taxonomy: Categorizes documents into PSA Birth Certificates, SF9 Progress Reports, SF10 Permanent Records, and Ancillary Certifications.
+ Optimized Image Rendering: Multi-page documents and scanned PDFs are converted into optimized WebP thumbnails for immediate in-app viewing.
+
+
+
+
+ TIS-RMS implements a tree-based Virtual Folder Architecture mimicking physical registrar filing cabinets. Folders are automatically maintained per school year and section.
+
+
+
+
+
+ Windows Desktop
+ Android Tablet
+
+
+
+
+
+
+
TIS-RMS - Documents & Virtual Folders
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+ Supported Document Classifications
+
+
+
+
PSA / NSO Birth Certificate
+
Legal record verifying student name, date of birth, sex, and parentage during enrollment intake.
+
+
+
+
+
+
DepEd Form 138 (SF9)
+
Quarterly progress report detailing subject grades, core values, attendance, and annual promotion status.
+
+
+
+
DepEd Form 137 (SF10)
+
Official cumulative scholastic transcript spanning all school years attended in junior or senior high school.
+
+
+
+
Certificates & Credentials
+
Certificate of Good Moral Character, clinic medical records, DepEd Form 58, and transfer documentation.
+
+
+
+
+
+
+
+
+ WebP Rendering Engine: Uploaded documents are automatically converted to optimized WebP image pages in the backend. This enables rapid thumbnail generation and in-app viewing without depending on external PDF applications.
+
+
+
+
+
+
+
+
+
+
+
+ Cohort Separation: Separates graduated or transferred students from active enrollment while keeping scholastic history accessible.
+ Real-Time Backend Filter: Archive search queries execute asynchronous SQLite queries with debouncing, returning results in under 10ms.
+ Reinstatement: Administrators can restore incorrectly archived students with a single click, preserving all attached document links.
+
+
+
+
+ Under national educational records policies, records of graduated students must be retained for permanent access. TIS-RMS isolates archived students from active section rosters while preserving full document searchability.
+
+
+
+
+
+ Windows Desktop
+ Android Tablet
+
+
+
+
+
+
+
TIS-RMS - Archival Vault
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+ Archival Guardrails
+
+
+
+
Read-Only Locking
+
Archived transcripts are locked in read-only mode to prevent inadvertent grade changes after graduation certificates are issued.
+
+
+
+
Instant Restoration
+
Students archived by mistake can be restored to active registry status with all prior grades and documents preserved.
+
+
+
+
+
+
Cohort Multi-Filtering
+
Filter archives across School Year (e.g., 2023–2024), Grade Level, Section, or Status (Graduated, Transferred Out, Dropped).
+
+
+
+
+
+
+
+
+
+
+
+ Automated Compilation: Merges multiple student records into a single consolidated PDF package for division audits.
+ Background Queue: Large print jobs process asynchronously in the background so workstation responsiveness is maintained.
+ Watermark Enforcement: Embeds official school seal, verification hash, and "OFFICIAL COPY" watermarks on exported files.
+
+
+
+
+ Generating records for an entire graduating section can be time-consuming. TIS-RMS compiles and spools documents in batches of 10 to prevent memory pressure while producing clean, standardized vector PDF packages.
+
+
+
+
+
1
+
+
Select Target Students
+
In the Students or Archives screen, check the target student rows or choose Select All in Section .
+
+
+
+
2
+
+
Choose Document Types
+
In the Print dialog, select which records to include (e.g., SF10 only, or Complete Package: PSA + SF9 + SF10 + Certificate of Good Moral).
+
+
+
+
3
+
+
Formatting and Margins
+
The compiler formats pages according to DepEd margins, page numbers, and optional division office watermarks.
+
+
+
+
4
+
+
Spool to Printer or Save PDF
+
Send jobs directly to networked printers or download a consolidated multi-page PDF.
+
+
+
+
+
+
+
+
+
+
+
+
+ DepEd Standard Templates: Reads official DepEd SF10 Excel workbooks for Junior High (JHS) and Senior High (SHS).
+ Automated Computation: Calculates general averages, remedial subject credits, and promotion remarks automatically.
+ Direct PDF Rendering: Produces print-ready vector PDF files with correct DepEd header seals and signatory blocks.
+
+
+
+
+ The system includes a dedicated Excel-to-PDF Template Engine . It loads official DepEd SF10 spreadsheets, fills demographic and scholastic data into template cells, and renders standardized vector PDFs.
+
+
+
+
+
+ Windows Desktop
+ Android Tablet
+
+
+
+
+
+
+
TIS-RMS - Reports & Form Generator
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Data Privacy Act Compliance: Maintains an immutable activity log recording user ID, target student LRN, action type, IP address, and timestamp.
+ Account Administration: Manage teacher accounts, assign section advisory roles, and reset temporary credentials.
+ Access Forensics: Filter audit logs by user, action (View, Update, Delete, Print), or student record.
+
+
+
+
+ To comply with the Philippine Data Privacy Act of 2012 (RA 10173), all actions performed within TIS-RMS—including viewing, downloading, printing, editing, and deleting records—are committed to an immutable audit ledger.
+
+
+
+
+
+ Windows Desktop
+ Android Tablet
+
+
+
+
+
+
+
TIS-RMS - User Account Management
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+ Audit History & Activity Ledger
+
+ The History screen logs every operation performed in the system. Entries cannot be cleared or modified by non-root users.
+
+
+
+
+
+ Windows Desktop
+ Android Tablet
+
+
+
+
+
+
+
TIS-RMS - Immutable Audit History Log
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Checklist Configurator: Configure mandatory versus optional documents per grade level (JHS vs. SHS requirements).
+ Institutional Headers: Set School Name, DepEd School ID, District, Division, and Region for all printed transcripts.
+ Local Database Backup: Generate timestamped SQLite snapshot files for off-site backup or disaster recovery.
+
+
+
+
+ The Settings panel allows Administrators to configure school profile headers, customize required document checklists per grade level, export offline SQL backups, and check local network health.
+
+
+
+
+
+ Windows Desktop
+ Android Tablet
+
+
+
+
+
+
+
TIS-RMS - System Settings
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+
+
+
+
+
+
+ Expand view
+
+
+
+
+
+
+ Managing Mandatory Document Rules
+
+ Administrators can toggle whether a document is Mandatory or Optional . If a student is missing any required document, their record is automatically flagged with a red badge on the dashboard and student list.
+
+
+
+
+
+
+
+
+
+
+
+ Key Combination
+ Operation
+ Scope / Context
+
+
+
+
+ Ctrl + F or /
+ Focus Search Input Field
+ Global
+
+
+ Ctrl + N
+ Open "New Student" Registration Dialog
+ Students Screen
+
+
+ Ctrl + U
+ Open Document Upload / Scanner Dialog
+ Documents Screen
+
+
+ Ctrl + P
+ Open Batch Print Compilation Dialog
+ Documents / Archives Screen
+
+
+ Esc
+ Dismiss Active Dialog or Lightbox Modal
+ Global
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ System Access & Security
+
+
+
+
+ What credentials do I use to log into TIS-RMS?
+
+
+
+
Teachers and staff are issued an individual username and password by the School Registrar or System Administrator. If this is your first time logging in, you will be prompted to choose a personal password.
+
+
+
+
+
+ Who can reset a forgotten password or PIN?
+
+
+
+
Passwords can be reset by any authorized Administrator. The Administrator navigates to User Accounts , selects the user's name, and clicks Reset Password . The user is issued a temporary password that must be updated upon their next sign-in.
+
+
+
+
+
+ Can multiple teachers access the system simultaneously?
+
+
+
+
Yes. TIS-RMS supports concurrent connections across the local area network (LAN). Teachers can scan records, update profiles, and check document checklists on their Android tablets or desktop PCs simultaneously without data lock conflicts.
+
+
+
+
+
+
+
+
+ Student Records & Enrollment
+
+
+
+
+ What happens if an LRN is accidentally entered twice?
+
+
+
+
TIS-RMS strictly enforces a unique constraint on the 12-digit Learner Reference Number (LRN). If a user attempts to register an LRN that already exists in either the active directory or the archives, the system prevents duplicate creation and prompts the user to view the existing student record.
+
+
+
+
+
+ Why does a student record show an "Incomplete Requirements" badge?
+
+
+
+
The red badge indicates that one or more documents marked as Mandatory in the Settings Checklist (e.g., PSA Birth Certificate or Grade 7 SF10) have not yet been uploaded for that student. Once the missing file is uploaded, the badge automatically updates to green.
+
+
+
+
+
+ How do I process students who transfer out to another school?
+
+
+
+
Navigate to the student's profile, click Update Status , and select Transferred Out . Enter the receiving school name and division. The student record is cleanly moved to the Archives under the Transferred cohort, locking it from active section attendance while keeping scholastic history accessible.
+
+
+
+
+
+
+
+
+ Document Management & Scanning
+
+
+
+
+ What file types and sizes are supported for document uploads?
+
+
+
+
The system accepts PDF documents, PNG images, and JPEG files up to 10MB per file. During upload, the backend automatically optimizes the file and generates WebP thumbnails for instantaneous in-app rendering.
+
+
+
+
+
+ How do I view uploaded documents without opening an external PDF application?
+
+
+
+
TIS-RMS converts scanned PDF pages into optimized WebP images on the server. Clicking any document card opens the built-in high-resolution preview modal with instant page navigation, eliminating the need to launch third-party PDF viewers like Adobe Acrobat.
+
+
+
+
+
+ What should I do if a scanned document preview appears blank?
+
+
+
+
Right-click the document card (or tap the three-dot context menu on tablet) and select Regenerate Thumbnail . The system will re-parse the raw source file and re-render lossless WebP preview pages.
+
+
+
+
+
+
+
+
+ SF10 & DepEd Reports
+
+
+
+
+ How does the automated SF10 (Form 137) generator work?
+
+
+
+
The system loads the official DepEd Excel SF10 template workbook, pulls student demographics, quarterly ratings, general averages, and remedial notes directly from the database, and compiles them into a standardized vector PDF with official school headers and signatures.
+
+
+
+
+
+ How do I print permanent records for an entire graduating class?
+
+
+
+
From the Students or Archives screen, select the desired section using the filter dropdown, click the Select All in Section checkbox, and tap the Batch Print button. The system queues the records in background chunks and outputs a single consolidated PDF package ready for duplex printing.
+
+
+
+
+
+
+
+
+ Retention, Archives & Data Privacy
+
+
+
+
+ What is the difference between Archiving and Deleting a record?
+
+
+
+
Archiving preserves the student's complete record permanently in read-only storage for future transcript requests (SF10 reproduction) while keeping active class lists clean. Deleting moves a record to the soft-delete trash for removal of erroneous entries. Teachers cannot delete records.
+
+
+
+
+
+ How does TIS-RMS ensure compliance with RA 10173 (Data Privacy Act)?
+
+
+
+
Every view, download, edit, and print action is recorded in an immutable audit ledger with the user's account name, terminal IP address, and timestamp. Passwords use salted bcrypt hashes, and records cannot be accessed outside the authenticated local school network.
+
+
+
+
+
+
+
+
+ Network Sync & Offline Operation
+
+
+
+
+ Does TIS-RMS require an active internet connection to work?
+
+
+
+
No. TIS-RMS runs completely offline on the school's local area network (LAN). It uses a local SQLite database that guarantees full system operation even during power interruptions or ISP service outages.
+
+
+
+
+
+ How do I connect an Android tablet to the school server?
+
+
+
+
Connect both the server computer and tablet to the same Wi-Fi router. On the server, check your IPv4 address using ipconfig. On the tablet login screen, enter the host address (e.g., 192.168.1.100:3000) and tap Connect .
+
+
+
+
+
+ How and when should database backups be performed?
+
+
+
+
Administrators should create a database backup at least once a week or before major grading periods. Navigate to Settings > Database Backup and click Create Snapshot . Save the exported .sqlite file to a secondary external USB drive or secured storage.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Android Tablet Cannot Reach Server
+
Verify both the computer hosting tis_rms_server and the Android tablet are connected to the same local network router. Ensure the Server IP in the app login matches the host machine's LAN address (e.g., 192.168.1.100:3000).
+
+
+
+
Scanned Document Preview Fails
+
If an uploaded PDF has damaged headers, select Regenerate Thumbnail from the document context menu. The server will re-render pages as lossless WebP images.
+
+
+
+
Batch Print Memory Allocation
+
When compiling large cohorts (>50 students), ensure the host machine has at least 500MB free temporary disk space. TIS-RMS streams pages in chunks of 10 to prevent memory exhaustion.
+
+
+
+
+
+
+
+
+
+
+
+
+
×
+
+
+
+
+
+
+
diff --git a/documentation/style.css b/documentation/style.css
new file mode 100644
index 0000000..f58586f
--- /dev/null
+++ b/documentation/style.css
@@ -0,0 +1,1558 @@
+/* ==========================================================================
+ TIS-RMS DOCUMENTATION & USER GUIDE - STYLESHEET
+ Design System: Emerald / Dark Green, Slate Glassmorphism, Modern Typography
+ ========================================================================== */
+
+:root {
+ /* Color Palette - Light Mode */
+ --bg-primary: #F8FAFC;
+ --bg-surface: #FFFFFF;
+ --bg-surface-elevated: #FFFFFF;
+ --bg-subtle: #F1F5F9;
+ --border-subtle: #E2E8F0;
+ --border-strong: #CBD5E1;
+
+ --text-primary: #0F172A;
+ --text-secondary: #475569;
+ --text-muted: #94A3B8;
+
+ --primary: #00B074;
+ --primary-hover: #009663;
+ --primary-light: #ECFDF5;
+ --primary-dark: #064E3B;
+ --primary-glow: rgba(0, 176, 116, 0.25);
+
+ --accent-blue: #2563EB;
+ --accent-amber: #D97706;
+ --accent-rose: #E11D48;
+ --accent-purple: #7C3AED;
+
+ --sidebar-w: 280px;
+ --header-h: 70px;
+ --radius-sm: 6px;
+ --radius-md: 10px;
+ --radius-lg: 16px;
+ --radius-xl: 24px;
+
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.08), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.08), 0 4px 6px -4px rgba(0, 0, 0, 0.04);
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.05);
+ --transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+[data-theme="dark"] {
+ --bg-primary: #0B0F17;
+ --bg-surface: #111827;
+ --bg-surface-elevated: #1F2937;
+ --bg-subtle: #1E293B;
+ --border-subtle: #1F2937;
+ --border-strong: #374151;
+
+ --text-primary: #F8FAFC;
+ --text-secondary: #94A3B8;
+ --text-muted: #64748B;
+
+ --primary: #10B981;
+ --primary-hover: #059669;
+ --primary-light: rgba(16, 185, 129, 0.15);
+ --primary-dark: #A7F3D0;
+ --primary-glow: rgba(16, 185, 129, 0.35);
+
+ --shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
+ --shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.4);
+ --shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
+}
+
+/* Base & Reset */
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html {
+ scroll-behavior: smooth;
+ scroll-padding-top: calc(var(--header-h) + 24px);
+}
+
+body {
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+ background-color: var(--bg-primary);
+ color: var(--text-primary);
+ line-height: 1.6;
+ -webkit-font-smoothing: antialiased;
+ overflow-x: hidden;
+}
+
+a {
+ color: var(--primary);
+ text-decoration: none;
+ transition: var(--transition);
+}
+
+a:hover {
+ text-decoration: underline;
+}
+
+/* ── Top Header ─────────────────────────────────────────── */
+.doc-header {
+ position: sticky;
+ top: 0;
+ z-index: 100;
+ height: var(--header-h);
+ background: var(--bg-surface);
+ border-bottom: 1px solid var(--border-subtle);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 24px;
+}
+
+.header-left {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.mobile-menu-btn,
+.mobile-search-btn {
+ display: none;
+ width: 38px;
+ height: 38px;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border-subtle);
+ background: var(--bg-subtle);
+ color: var(--text-primary);
+ cursor: pointer;
+ align-items: center;
+ justify-content: center;
+ transition: var(--transition);
+ flex-shrink: 0;
+}
+
+.mobile-menu-btn:hover,
+.mobile-search-btn:hover {
+ background: var(--bg-surface-elevated);
+ border-color: var(--border-strong);
+}
+
+.sidebar-backdrop {
+ display: none;
+ position: fixed;
+ inset: 0;
+ z-index: 95;
+ background: rgba(0, 0, 0, 0.55);
+ backdrop-filter: blur(4px);
+ -webkit-backdrop-filter: blur(4px);
+ opacity: 0;
+ transition: opacity 0.25s ease;
+ pointer-events: none;
+}
+
+.sidebar-backdrop.active {
+ display: block;
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.header-brand {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ text-decoration: none;
+}
+
+.header-brand img {
+ width: 40px;
+ height: 40px;
+ border-radius: var(--radius-md);
+ object-fit: contain;
+ box-shadow: 0 2px 8px var(--primary-glow);
+ flex-shrink: 0;
+}
+
+.brand-text h1 {
+ font-size: 1.05rem;
+ font-weight: 700;
+ color: var(--text-primary);
+ line-height: 1.2;
+}
+
+.brand-text span {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ font-weight: 500;
+}
+
+.header-search {
+ flex: 1;
+ max-width: 420px;
+ margin: 0 24px;
+ position: relative;
+}
+
+.search-input-wrap {
+ display: flex;
+ align-items: center;
+ background: var(--bg-subtle);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ padding: 0 14px;
+ transition: var(--transition);
+}
+
+.search-input-wrap:focus-within {
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px var(--primary-glow);
+ background: var(--bg-surface);
+}
+
+.search-icon {
+ color: var(--text-muted);
+ margin-right: 8px;
+}
+
+.search-input {
+ width: 100%;
+ height: 40px;
+ background: transparent;
+ border: none;
+ outline: none;
+ font-size: 0.88rem;
+ color: var(--text-primary);
+}
+
+.search-kbd {
+ font-size: 0.7rem;
+ font-family: monospace;
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: 4px;
+ padding: 2px 6px;
+ color: var(--text-muted);
+}
+
+.header-actions {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.platform-pill-toggle {
+ display: flex;
+ background: var(--bg-subtle);
+ border-radius: var(--radius-md);
+ padding: 3px;
+ border: 1px solid var(--border-subtle);
+}
+
+.platform-btn {
+ padding: 6px 12px;
+ font-size: 0.8rem;
+ font-weight: 600;
+ border: none;
+ background: transparent;
+ color: var(--text-secondary);
+ border-radius: var(--radius-sm);
+ cursor: pointer;
+ transition: var(--transition);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.platform-btn.active {
+ background: var(--primary);
+ color: #FFFFFF;
+ box-shadow: var(--shadow-sm);
+}
+
+.theme-toggle-btn, .print-btn, .github-btn {
+ width: 38px;
+ height: 38px;
+ border-radius: var(--radius-md);
+ border: 1px solid var(--border-subtle);
+ background: var(--bg-subtle);
+ color: var(--text-secondary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: var(--transition);
+}
+
+.theme-toggle-btn:hover, .print-btn:hover, .github-btn:hover {
+ background: var(--bg-surface-elevated);
+ color: var(--text-primary);
+ border-color: var(--border-strong);
+}
+
+/* ── Layout Container ───────────────────────────────────── */
+.doc-layout {
+ display: flex;
+ max-width: 1600px;
+ margin: 0 auto;
+ position: relative;
+}
+
+/* ── Sidebar Navigation ─────────────────────────────────── */
+.doc-sidebar {
+ width: var(--sidebar-w);
+ height: calc(100vh - var(--header-h));
+ position: sticky;
+ top: var(--header-h);
+ overflow-y: auto;
+ padding: 24px 16px;
+ border-right: 1px solid var(--border-subtle);
+ background: var(--bg-surface);
+ flex-shrink: 0;
+}
+
+.sidebar-heading {
+ font-size: 0.72rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--text-muted);
+ margin: 16px 12px 8px;
+}
+
+.sidebar-nav {
+ list-style: none;
+}
+
+.nav-link {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 12px;
+ border-radius: var(--radius-md);
+ font-size: 0.88rem;
+ font-weight: 500;
+ color: var(--text-secondary);
+ text-decoration: none;
+ transition: var(--transition);
+ margin-bottom: 2px;
+}
+
+.nav-link:hover {
+ background: var(--bg-subtle);
+ color: var(--text-primary);
+ text-decoration: none;
+}
+
+.nav-link.active {
+ background: var(--primary-light);
+ color: var(--primary);
+ font-weight: 600;
+}
+
+.nav-link .nav-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ flex-shrink: 0;
+ color: var(--text-muted);
+ transition: var(--transition);
+}
+
+.nav-link:hover .nav-icon,
+.nav-link.active .nav-icon {
+ color: var(--primary);
+}
+
+.nav-link .nav-icon svg {
+ width: 16px;
+ height: 16px;
+}
+
+.nav-badge {
+ margin-left: auto;
+ font-size: 0.68rem;
+ padding: 2px 6px;
+ border-radius: 999px;
+ font-weight: 600;
+ background: var(--bg-subtle);
+ color: var(--text-muted);
+}
+
+.nav-link.active .nav-badge {
+ background: var(--primary);
+ color: #fff;
+}
+
+/* ── Main Content Area ──────────────────────────────────── */
+.doc-main {
+ flex: 1;
+ min-width: 0;
+ padding: 40px 48px 100px;
+}
+
+/* ── Hero Banner ────────────────────────────────────────── */
+.hero-banner {
+ background: linear-gradient(135deg, rgba(0, 176, 116, 0.12) 0%, rgba(37, 99, 235, 0.06) 100%);
+ border: 1px solid rgba(0, 176, 116, 0.2);
+ border-radius: var(--radius-xl);
+ padding: 40px;
+ margin-bottom: 48px;
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ background: var(--primary-light);
+ color: var(--primary);
+ font-size: 0.78rem;
+ font-weight: 700;
+ padding: 4px 10px;
+ border-radius: 999px;
+ margin-bottom: 16px;
+}
+
+.hero-title {
+ font-size: 2.2rem;
+ font-weight: 800;
+ color: var(--text-primary);
+ line-height: 1.2;
+ margin-bottom: 12px;
+ letter-spacing: -0.02em;
+}
+
+.hero-subtitle {
+ font-size: 1.05rem;
+ color: var(--text-secondary);
+ max-width: 800px;
+ margin-bottom: 24px;
+}
+
+.hero-badges {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+}
+
+.badge-item {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ padding: 6px 12px;
+ border-radius: var(--radius-md);
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ box-shadow: var(--shadow-sm);
+}
+
+/* ── Content Sections ───────────────────────────────────── */
+.doc-section {
+ margin-bottom: 64px;
+ scroll-margin-top: calc(var(--header-h) + 20px);
+}
+
+.section-header {
+ border-bottom: 1px solid var(--border-subtle);
+ padding-bottom: 16px;
+ margin-bottom: 24px;
+}
+
+.section-title {
+ font-size: 1.65rem;
+ font-weight: 800;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ letter-spacing: -0.01em;
+}
+
+.section-subtitle {
+ font-size: 0.95rem;
+ color: var(--text-secondary);
+ margin-top: 6px;
+}
+
+/* ── Typography & Cards ─────────────────────────────────── */
+h3.subheading {
+ font-size: 1.25rem;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin: 28px 0 12px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+p.paragraph {
+ font-size: 0.95rem;
+ color: var(--text-secondary);
+ margin-bottom: 16px;
+ line-height: 1.7;
+}
+
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+ gap: 20px;
+ margin: 20px 0;
+}
+
+.feature-card {
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ padding: 24px;
+ box-shadow: var(--shadow-sm);
+ transition: var(--transition);
+}
+
+.feature-card:hover {
+ transform: translateY(-2px);
+ box-shadow: var(--shadow-md);
+ border-color: var(--primary);
+}
+
+.card-icon {
+ width: 40px;
+ height: 40px;
+ border-radius: var(--radius-md);
+ background: var(--primary-light);
+ color: var(--primary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-bottom: 16px;
+ flex-shrink: 0;
+}
+
+.card-icon svg {
+ width: 20px;
+ height: 20px;
+}
+
+.card-title {
+ font-size: 1.02rem;
+ font-weight: 700;
+ margin-bottom: 8px;
+ color: var(--text-primary);
+}
+
+.card-desc {
+ font-size: 0.88rem;
+ color: var(--text-secondary);
+ line-height: 1.6;
+}
+
+/* ── Module Summary Box (Reader-Friendly Key Points) ────── */
+.module-summary {
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-left: 3px solid var(--primary);
+ border-radius: var(--radius-md);
+ padding: 16px 20px;
+ margin: 16px 0 24px;
+}
+
+.module-summary-label {
+ font-size: 0.72rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--primary);
+ margin-bottom: 8px;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.module-summary-list {
+ list-style: none;
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.module-summary-list li {
+ font-size: 0.88rem;
+ color: var(--text-secondary);
+ line-height: 1.5;
+ display: flex;
+ align-items: baseline;
+ gap: 8px;
+}
+
+.module-summary-list li::before {
+ content: "•";
+ color: var(--primary);
+ font-weight: bold;
+}
+
+/* ── Step-by-Step Instructions ─────────────────────────── */
+.steps-container {
+ display: flex;
+ flex-direction: column;
+ gap: 18px;
+ margin: 24px 0;
+}
+
+.step-card {
+ display: flex;
+ gap: 20px;
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ padding: 20px 24px;
+ box-shadow: var(--shadow-sm);
+}
+
+.step-number {
+ flex-shrink: 0;
+ width: 36px;
+ height: 36px;
+ border-radius: 50%;
+ background: var(--primary);
+ color: #fff;
+ font-weight: 800;
+ font-size: 0.95rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 2px 8px var(--primary-glow);
+}
+
+.step-content h4 {
+ font-size: 1rem;
+ font-weight: 700;
+ margin-bottom: 6px;
+ color: var(--text-primary);
+}
+
+.step-content p {
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ line-height: 1.6;
+}
+
+/* ── Callout Alerts ─────────────────────────────────────── */
+.callout {
+ padding: 16px 20px;
+ border-radius: var(--radius-md);
+ margin: 20px 0;
+ display: flex;
+ gap: 14px;
+ align-items: flex-start;
+ font-size: 0.9rem;
+ line-height: 1.6;
+}
+
+.callout.tip {
+ background: rgba(16, 185, 129, 0.08);
+ border-left: 4px solid var(--primary);
+ color: var(--text-primary);
+}
+
+.callout.important {
+ background: rgba(37, 99, 235, 0.08);
+ border-left: 4px solid var(--accent-blue);
+ color: var(--text-primary);
+}
+
+.callout.warning {
+ background: rgba(217, 119, 6, 0.08);
+ border-left: 4px solid var(--accent-amber);
+ color: var(--text-primary);
+}
+
+.callout-icon {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 20px;
+ height: 20px;
+ flex-shrink: 0;
+ margin-top: 2px;
+}
+
+.callout.tip .callout-icon { color: var(--primary); }
+.callout.important .callout-icon { color: var(--accent-blue); }
+.callout.warning .callout-icon { color: var(--accent-amber); }
+
+.callout-icon svg {
+ width: 18px;
+ height: 18px;
+}
+ flex-shrink: 0;
+}
+
+/* ── Screenshot Mockup Showcase ─────────────────────────── */
+.mockup-container {
+ margin: 28px 0;
+ background: var(--bg-subtle);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-xl);
+ padding: 24px;
+ position: relative;
+}
+
+.mockup-tabs {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 16px;
+}
+
+.mockup-tab-btn {
+ padding: 6px 14px;
+ border-radius: var(--radius-md);
+ font-size: 0.82rem;
+ font-weight: 600;
+ border: 1px solid var(--border-subtle);
+ background: var(--bg-surface);
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: var(--transition);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.mockup-tab-btn.active {
+ background: var(--primary);
+ color: #FFFFFF;
+ border-color: var(--primary);
+}
+
+/* Windows Window Chrome */
+.window-chrome {
+ background: var(--bg-surface);
+ border: 1px solid var(--border-strong);
+ border-radius: var(--radius-md);
+ overflow: hidden;
+ box-shadow: var(--shadow-xl);
+}
+
+.window-titlebar {
+ height: 32px;
+ background: var(--bg-subtle);
+ border-bottom: 1px solid var(--border-subtle);
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 0 12px;
+ font-size: 0.78rem;
+ color: var(--text-secondary);
+}
+
+.window-dots {
+ display: flex;
+ gap: 6px;
+}
+
+.window-dots span {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: var(--border-strong);
+}
+
+.window-dots span:nth-child(1) { background: #EF4444; }
+.window-dots span:nth-child(2) { background: #F59E0B; }
+.window-dots span:nth-child(3) { background: #10B981; }
+
+.mockup-img-wrap {
+ position: relative;
+ overflow: hidden;
+ background: #000;
+ display: flex;
+ justify-content: center;
+ cursor: zoom-in;
+}
+
+.mockup-img-wrap img {
+ width: 100%;
+ height: auto;
+ max-height: 520px;
+ object-fit: contain;
+ display: block;
+ transition: transform 0.3s ease;
+}
+
+.mockup-img-wrap:hover img {
+ transform: scale(1.015);
+}
+
+.zoom-overlay-hint {
+ position: absolute;
+ bottom: 12px;
+ right: 12px;
+ background: rgba(15, 23, 42, 0.75);
+ color: #fff;
+ padding: 5px 10px;
+ border-radius: var(--radius-sm);
+ font-size: 0.75rem;
+ font-weight: 500;
+ backdrop-filter: blur(4px);
+ pointer-events: none;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.zoom-overlay-hint svg {
+ width: 13px;
+ height: 13px;
+}
+
+/* ── Mobile Phone Chrome ────────────────────────────────── */
+.mobile-chrome {
+ max-width: 360px;
+ margin: 0 auto;
+ background: #1E293B;
+ border: 10px solid #0F172A;
+ border-radius: 36px;
+ overflow: hidden;
+ box-shadow: var(--shadow-xl);
+}
+
+.mobile-notch {
+ height: 24px;
+ background: #0F172A;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.mobile-notch-camera {
+ width: 10px;
+ height: 10px;
+ border-radius: 50%;
+ background: #334155;
+}
+
+.mobile-img-wrap {
+ cursor: zoom-in;
+}
+
+.mobile-img-wrap img {
+ width: 100%;
+ display: block;
+}
+
+/* ── Interactive Video Walkthrough Player ────────────────── */
+.video-showcase-box {
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-xl);
+ overflow: hidden;
+ box-shadow: var(--shadow-lg);
+ margin: 28px 0;
+}
+
+.video-screen {
+ position: relative;
+ width: 100%;
+ height: 480px;
+ background: #0F172A;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.video-slide-item {
+ position: absolute;
+ inset: 0;
+ opacity: 0;
+ transition: opacity 0.5s ease-in-out;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.video-slide-item.active {
+ opacity: 1;
+}
+
+.video-slide-item img {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+}
+
+.video-hud-overlay {
+ position: absolute;
+ top: 16px;
+ left: 16px;
+ background: rgba(15, 23, 42, 0.85);
+ color: #fff;
+ padding: 8px 16px;
+ border-radius: var(--radius-md);
+ font-size: 0.85rem;
+ backdrop-filter: blur(8px);
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.pulse-indicator {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--primary);
+ box-shadow: 0 0 10px var(--primary);
+ animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+ 0% { transform: scale(0.9); opacity: 0.8; }
+ 50% { transform: scale(1.4); opacity: 1; }
+ 100% { transform: scale(0.9); opacity: 0.8; }
+}
+
+.video-controls-bar {
+ background: var(--bg-surface-elevated);
+ border-top: 1px solid var(--border-subtle);
+ padding: 16px 24px;
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+}
+
+.video-play-btn {
+ background: var(--primary);
+ color: #fff;
+ border: none;
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ font-size: 1.1rem;
+ transition: var(--transition);
+ box-shadow: 0 2px 8px var(--primary-glow);
+}
+
+.video-play-btn:hover {
+ transform: scale(1.05);
+ background: var(--primary-hover);
+}
+
+.video-timeline {
+ flex: 1;
+ min-width: 200px;
+ display: flex;
+ gap: 6px;
+}
+
+.timeline-segment {
+ flex: 1;
+ height: 6px;
+ background: var(--border-strong);
+ border-radius: 3px;
+ cursor: pointer;
+ position: relative;
+ overflow: hidden;
+}
+
+.timeline-segment.active .segment-progress {
+ width: 100%;
+ background: var(--primary);
+}
+
+.timeline-segment.played .segment-progress {
+ width: 100%;
+ background: var(--primary);
+}
+
+.segment-progress {
+ height: 100%;
+ width: 0;
+ transition: width 0.3s ease;
+}
+
+.video-chapters {
+ display: flex;
+ gap: 8px;
+ overflow-x: auto;
+ padding: 12px 24px;
+ background: var(--bg-subtle);
+ border-top: 1px solid var(--border-subtle);
+}
+
+.chapter-btn {
+ padding: 6px 12px;
+ border-radius: var(--radius-sm);
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ font-size: 0.8rem;
+ font-weight: 600;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ cursor: pointer;
+ transition: var(--transition);
+}
+
+.chapter-btn:hover, .chapter-btn.active {
+ background: var(--primary);
+ color: #fff;
+ border-color: var(--primary);
+}
+
+/* ── Tables & Data Grids ────────────────────────────────── */
+.table-wrap {
+ overflow-x: auto;
+ margin: 20px 0;
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-sm);
+ background: var(--bg-surface);
+}
+
+table.doc-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.9rem;
+ text-align: left;
+}
+
+table.doc-table th {
+ background: var(--bg-subtle);
+ padding: 12px 18px;
+ font-weight: 700;
+ color: var(--text-primary);
+ border-bottom: 1px solid var(--border-subtle);
+}
+
+table.doc-table td {
+ padding: 12px 18px;
+ border-bottom: 1px solid var(--border-subtle);
+ color: var(--text-secondary);
+}
+
+table.doc-table tr:last-child td {
+ border-bottom: none;
+}
+
+table.doc-table tr:hover td {
+ background: var(--bg-subtle);
+}
+
+.badge {
+ display: inline-block;
+ font-size: 0.72rem;
+ font-weight: 700;
+ padding: 2px 8px;
+ border-radius: 999px;
+ text-transform: uppercase;
+}
+
+.badge.success { background: #DEF7EC; color: #03543F; }
+.badge.info { background: #E1EFFE; color: #1E429F; }
+.badge.warning { background: #FEF08A; color: #854D0E; }
+.badge.danger { background: #FDE8E8; color: #9B1C1C; }
+
+/* ── Lightbox Modal ─────────────────────────────────────── */
+.lightbox-modal {
+ position: fixed;
+ inset: 0;
+ z-index: 1000;
+ background: rgba(0, 0, 0, 0.9);
+ backdrop-filter: blur(8px);
+ display: none;
+ align-items: center;
+ justify-content: center;
+ padding: 24px;
+}
+
+.lightbox-modal.active {
+ display: flex;
+}
+
+.lightbox-img {
+ max-width: 95%;
+ max-height: 90vh;
+ object-fit: contain;
+ border-radius: var(--radius-md);
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.7);
+ animation: zoomIn 0.2s ease-out;
+}
+
+@keyframes zoomIn {
+ from { transform: scale(0.95); opacity: 0; }
+ to { transform: scale(1); opacity: 1; }
+}
+
+.lightbox-close {
+ position: absolute;
+ top: 24px;
+ right: 24px;
+ width: 44px;
+ height: 44px;
+ border-radius: 50%;
+ background: rgba(255, 255, 255, 0.2);
+ color: #fff;
+ border: none;
+ font-size: 1.5rem;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: var(--transition);
+}
+
+.lightbox-close:hover {
+ background: rgba(255, 255, 255, 0.4);
+}
+
+.lightbox-caption {
+ position: absolute;
+ bottom: 24px;
+ background: rgba(15, 23, 42, 0.8);
+ color: #fff;
+ padding: 8px 18px;
+ border-radius: var(--radius-md);
+ font-size: 0.9rem;
+}
+
+/* ── FAQ Accordion Section ─────────────────────────────── */
+.faq-category {
+ margin: 32px 0 16px;
+}
+
+.faq-category:first-of-type {
+ margin-top: 16px;
+}
+
+.faq-category-title {
+ font-size: 1.05rem;
+ font-weight: 700;
+ color: var(--primary);
+ margin-bottom: 14px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.faq-item {
+ background: var(--bg-surface);
+ border: 1px solid var(--border-subtle);
+ border-radius: var(--radius-md);
+ margin-bottom: 12px;
+ overflow: hidden;
+ transition: var(--transition);
+}
+
+.faq-item:hover {
+ border-color: var(--border-strong);
+}
+
+.faq-question {
+ width: 100%;
+ padding: 16px 20px;
+ background: transparent;
+ border: none;
+ text-align: left;
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ font-family: inherit;
+}
+
+.faq-question svg {
+ transition: transform 0.25s ease;
+ flex-shrink: 0;
+ color: var(--text-muted);
+}
+
+.faq-item.active {
+ border-color: var(--primary);
+ box-shadow: 0 2px 8px var(--primary-glow);
+}
+
+.faq-item.active .faq-question svg {
+ transform: rotate(180deg);
+ color: var(--primary);
+}
+
+.faq-answer {
+ display: none;
+ padding: 0 20px 18px;
+ color: var(--text-secondary);
+ font-size: 0.9rem;
+ line-height: 1.65;
+ border-top: 1px solid var(--border-subtle);
+ background: var(--bg-subtle);
+}
+
+.faq-item.active .faq-answer {
+ display: block;
+ animation: slideDown 0.2s ease-out;
+}
+
+.faq-answer p {
+ margin-top: 12px;
+}
+
+.faq-answer p:first-child {
+ margin-top: 14px;
+}
+
+.faq-answer ul {
+ margin: 10px 0 10px 20px;
+}
+
+.faq-answer li {
+ margin-bottom: 6px;
+}
+
+/* ── Footer ─────────────────────────────────────────────── */
+.doc-footer {
+ margin-top: 64px;
+ border-top: 1px solid var(--border-subtle);
+ padding: 32px 0 16px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 16px;
+ font-size: 0.85rem;
+ color: var(--text-muted);
+}
+
+.footer-credits {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.footer-credits img {
+ width: 28px;
+ height: 28px;
+}
+
+/* ── Responsive Adaptations ─────────────────────────────── */
+@keyframes slideDown {
+ from { opacity: 0; transform: translateY(-8px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@media (max-width: 1024px) {
+ :root {
+ --header-h: 62px;
+ }
+
+ .mobile-menu-btn {
+ display: flex !important;
+ }
+
+ .mobile-search-btn {
+ display: flex !important;
+ }
+
+ .print-btn {
+ display: none !important;
+ }
+
+ .header-search {
+ position: absolute;
+ top: var(--header-h);
+ left: 0;
+ right: 0;
+ margin: 0;
+ max-width: none;
+ background: var(--bg-surface);
+ padding: 10px 16px;
+ border-bottom: 1px solid var(--border-subtle);
+ display: none;
+ box-shadow: var(--shadow-md);
+ z-index: 90;
+ }
+
+ .header-search.mobile-open {
+ display: block !important;
+ animation: slideDown 0.2s ease-out;
+ }
+
+ .search-kbd {
+ display: none;
+ }
+
+ .doc-sidebar {
+ position: fixed;
+ top: 0;
+ bottom: 0;
+ left: -320px;
+ height: 100vh;
+ z-index: 100;
+ width: 290px;
+ background: var(--bg-surface);
+ border-right: 1px solid var(--border-subtle);
+ box-shadow: var(--shadow-xl);
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ padding: 20px 16px 40px;
+ }
+
+ .doc-sidebar.open {
+ transform: translateX(320px);
+ }
+
+ .doc-main {
+ padding: 28px 24px 80px;
+ }
+}
+
+@media (max-width: 768px) {
+ :root {
+ --header-h: 60px;
+ }
+
+ .doc-header {
+ padding: 0 14px;
+ }
+
+ .platform-pill-toggle .platform-text {
+ display: none;
+ }
+
+ .platform-btn {
+ padding: 6px 8px;
+ font-size: 0.75rem;
+ }
+
+ .print-btn {
+ display: none;
+ }
+
+ .hero-banner {
+ padding: 24px 18px;
+ margin-bottom: 36px;
+ border-radius: var(--radius-lg);
+ }
+
+ .hero-title {
+ font-size: 1.55rem;
+ }
+
+ .hero-subtitle {
+ font-size: 0.92rem;
+ margin-bottom: 18px;
+ }
+
+ .doc-section {
+ margin-bottom: 48px;
+ }
+
+ .section-title {
+ font-size: 1.35rem;
+ }
+
+ .section-subtitle {
+ font-size: 0.88rem;
+ }
+
+ .video-screen {
+ height: clamp(210px, 54vw, 340px);
+ }
+
+ .video-hud-overlay {
+ font-size: 0.75rem;
+ padding: 5px 10px;
+ top: 10px;
+ left: 10px;
+ max-width: calc(100% - 20px);
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ .video-controls-bar {
+ padding: 10px 14px;
+ gap: 10px;
+ }
+
+ .video-chapters {
+ padding: 8px 12px;
+ gap: 6px;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ .chapter-btn {
+ font-size: 0.75rem;
+ padding: 5px 10px;
+ }
+
+ .feature-grid {
+ grid-template-columns: 1fr;
+ gap: 14px;
+ margin: 16px 0;
+ }
+
+ .mockup-container {
+ padding: 14px 10px;
+ border-radius: var(--radius-lg);
+ margin: 20px 0;
+ }
+
+ .mockup-tabs {
+ overflow-x: auto;
+ flex-wrap: nowrap;
+ -webkit-overflow-scrolling: touch;
+ padding-bottom: 4px;
+ gap: 6px;
+ }
+
+ .mockup-tab-btn {
+ white-space: nowrap;
+ font-size: 0.78rem;
+ padding: 6px 12px;
+ flex-shrink: 0;
+ }
+
+ .step-card {
+ padding: 16px 14px;
+ gap: 14px;
+ }
+
+ .step-number {
+ width: 32px;
+ height: 32px;
+ font-size: 0.85rem;
+ }
+
+ .table-wrap {
+ margin: 16px 0;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ table.doc-table th, table.doc-table td {
+ padding: 10px 12px;
+ font-size: 0.82rem;
+ white-space: nowrap;
+ }
+
+ .table-wrap table.doc-table td:nth-child(1),
+ .table-wrap table.doc-table td:nth-child(2) {
+ white-space: normal;
+ }
+}
+
+@media (max-width: 480px) {
+ .doc-main {
+ padding: 16px 12px 60px;
+ }
+
+ .brand-text span {
+ display: none;
+ }
+
+ .brand-text h1 {
+ font-size: 0.95rem;
+ }
+
+ .header-brand img {
+ width: 32px;
+ height: 32px;
+ }
+
+ .header-actions {
+ gap: 6px;
+ }
+
+ .theme-toggle-btn,
+ .mobile-menu-btn,
+ .mobile-search-btn {
+ width: 34px;
+ height: 34px;
+ }
+
+ .hero-banner {
+ padding: 18px 14px;
+ }
+
+ .hero-tag {
+ font-size: 0.7rem;
+ padding: 3px 8px;
+ margin-bottom: 12px;
+ }
+
+ .hero-title {
+ font-size: 1.32rem;
+ }
+
+ .hero-subtitle {
+ font-size: 0.85rem;
+ margin-bottom: 14px;
+ }
+
+ .badge-item {
+ font-size: 0.7rem;
+ padding: 4px 8px;
+ }
+
+ .section-title {
+ font-size: 1.18rem;
+ }
+
+ .module-summary {
+ padding: 12px 14px;
+ margin: 12px 0 20px;
+ }
+
+ .module-summary-list li {
+ font-size: 0.82rem;
+ }
+
+ .mobile-chrome {
+ border-width: 6px;
+ border-radius: 20px;
+ }
+
+ .window-titlebar {
+ height: 26px;
+ font-size: 0.7rem;
+ padding: 0 8px;
+ }
+
+ .callout {
+ padding: 12px 14px;
+ gap: 10px;
+ font-size: 0.84rem;
+ }
+
+ .doc-footer {
+ padding: 24px 0 16px;
+ font-size: 0.78rem;
+ }
+}
+
+/* Print Styles */
+@media print {
+ .doc-header, .doc-sidebar, .mockup-tabs, .video-controls-bar, .video-chapters, .theme-toggle-btn, .print-btn {
+ display: none !important;
+ }
+ .doc-main {
+ padding: 0;
+ max-width: 100%;
+ }
+ .doc-section {
+ page-break-inside: avoid;
+ }
+}
diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md
index 4f29cc3..2dac6fd 100644
--- a/frontend/AGENTS.md
+++ b/frontend/AGENTS.md
@@ -930,7 +930,7 @@ Files backup/restore - mirror mode or zip -->
- Clicking the "Enrolled" tab automatically clears the search field and restores the active year enrolled filter.
- Also cleared view-tab filters when search is triggered from the mobile search dialog in android_bottom_nav_layout.dart. -->
-# ANDROID APP
+
- [DONE] make the about, support, system information in settings responsive
- System/Client Description Header now adapts using `LayoutBuilder` (<460px): renders icon + title in a row and full-width description below on mobile screens, avoiding horizontal squishing.
- System Information items (`_buildAboutInfoRow`) dynamically switch from a 2-column fixed-label layout to stacked label & selectable value on compact viewports (<460px), preventing text compression.
- - Developer Contact container adapts on viewports <500px: stacks label and email with a clean indentation to prevent horizontal overflows. Action buttons seamlessly wrap with `Wrap`.
+ - Developer Contact container adapts on viewports <500px: stacks label and email with a clean indentation to prevent horizontal overflows. Action buttons seamlessly wrap with `Wrap`. -->
+
+
+
+
+
+
+
+
+# HISTORY SCREEN
+- PRINTED_HISTORY card has a overflowed for date and time
+
+# Documentation website
+- fix FAQs make sure its on the current system features
+- connect the documentation based on the current system features
+- dont speficy the android is for tablet just android
+- redesign the entire documentation make it minimalist no extra design just make it normal documentation
---
diff --git a/frontend/android/app/src/main/AndroidManifest.xml b/frontend/android/app/src/main/AndroidManifest.xml
index 6747a8a..02d49e2 100644
--- a/frontend/android/app/src/main/AndroidManifest.xml
+++ b/frontend/android/app/src/main/AndroidManifest.xml
@@ -93,5 +93,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/lib/domain/repositories/document_repository.dart b/frontend/lib/domain/repositories/document_repository.dart
index 695c97b..c068f21 100644
--- a/frontend/lib/domain/repositories/document_repository.dart
+++ b/frontend/lib/domain/repositories/document_repository.dart
@@ -253,6 +253,24 @@ class DocumentRepository {
}
}
+ Future>> getDocumentTypes() async {
+ try {
+ final options = await _getAuthOptions();
+ final response = await _dio.get('/documents/types', options: options);
+ final data = response.data as Map;
+ return {
+ 'jhs': (data['jhs'] as List? ?? []).map((e) => e.toString()).toList(),
+ 'shs': (data['shs'] as List? ?? []).map((e) => e.toString()).toList(),
+ 'general':
+ (data['general'] as List? ?? []).map((e) => e.toString()).toList(),
+ };
+ } on DioException catch (e) {
+ final msg =
+ e.response?.data?['message'] ?? 'Failed to fetch document types.';
+ throw Exception(msg);
+ }
+ }
+
Future getDocuments({
String search = '',
int page = 1,
diff --git a/frontend/lib/ui/layouts/android_bottom_nav_layout.dart b/frontend/lib/ui/layouts/android_bottom_nav_layout.dart
index ec96dd7..041c79e 100644
--- a/frontend/lib/ui/layouts/android_bottom_nav_layout.dart
+++ b/frontend/lib/ui/layouts/android_bottom_nav_layout.dart
@@ -28,7 +28,6 @@ import '../providers/navigation_provider.dart';
import '../providers/connected_users_provider.dart';
import '../screens/capstone_members/capstone_members_screen.dart';
import '../shared/dialogs/disconnected_dialog.dart';
-import '../screens/students/widgets/student_filter_dialog.dart';
import '../shared/inputs/app_search_bar.dart';
import '../shared/menus/profile_dropdown_menu.dart';
import '../shared/widgets/notification_icon_button.dart';
@@ -748,32 +747,6 @@ class _AndroidBottomNavLayoutState extends ConsumerState
);
},
),
- // 2. Filter Icon with Badge
- Consumer(
- builder: (context, ref, _) {
- final activeCount = ref.watch(studentActiveFilterCountProvider);
- final query = ref.watch(studentQueryProvider);
- return Tooltip(
- message: 'Filter Students',
- child: IconButton(
- onPressed: () =>
- StudentFilterDialog.show(
- context,
- query: query,
- isEnrolledTab: ref.read(studentViewTabProvider) == StudentViewTab.enrolled,
- ),
- icon: Badge(
- isLabelVisible: activeCount > 0,
- label: Text(activeCount.toString()),
- child: const Icon(
- Icons.tune_rounded,
- size: 22,
- ),
- ),
- ),
- );
- },
- ),
// 3. Add Student Icon (High-Attraction Elevated Primary CTA)
if (widget.userRole != 'teacher')
Tooltip(
diff --git a/frontend/lib/ui/providers/document_provider.dart b/frontend/lib/ui/providers/document_provider.dart
index 0701dd2..97da39c 100644
--- a/frontend/lib/ui/providers/document_provider.dart
+++ b/frontend/lib/ui/providers/document_provider.dart
@@ -151,11 +151,20 @@ final studentDocumentsProvider = FutureProvider.family
final studentFoldersProvider = FutureProvider.autoDispose>((
ref,
) async {
+ final query = ref.watch(documentQueryProvider);
final repo = ref.read(documentRepositoryProvider);
- // Fetch top-level folders (no parentId filter = all root folders)
- return repo.getFolders();
+ return repo.getFolders(search: query.search);
});
+// ============================================================
+// Realtime document types provider — live from backend database
+// ============================================================
+final realtimeDocumentTypesProvider =
+ FutureProvider.autoDispose>>((ref) async {
+ final repo = ref.read(documentRepositoryProvider);
+ return repo.getDocumentTypes();
+ });
+
// ============================================================
// Document statuses provider — fetches distinct statuses from backend
// ============================================================
diff --git a/frontend/lib/ui/screens/archives/archives_screen.dart b/frontend/lib/ui/screens/archives/archives_screen.dart
index 537e7f4..de10089 100644
--- a/frontend/lib/ui/screens/archives/archives_screen.dart
+++ b/frontend/lib/ui/screens/archives/archives_screen.dart
@@ -745,6 +745,9 @@ class _ArchivesScreenState extends ConsumerState
const SingleActivator(LogicalKeyboardKey.keyF, control: true): () {
_showSearchDialog(context);
},
+ const SingleActivator(LogicalKeyboardKey.keyP, control: true): () {
+ PrintQueueModal.show(context);
+ },
},
child: Focus(
focusNode: _shortcutFocusNode,
@@ -1196,6 +1199,10 @@ class _ArchivesScreenState extends ConsumerState
tooltip: 'Filter by Document Type',
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
+ onOpened: () {
+ ref.invalidate(realtimeDocumentTypesProvider);
+ ref.invalidate(documentRequirementsProvider);
+ },
icon: Badge(
isLabelVisible: _getActiveFilterCount() > 0,
label: Text(_getActiveFilterCount().toString()),
@@ -1216,25 +1223,30 @@ class _ArchivesScreenState extends ConsumerState
_applyFilters();
},
itemBuilder: (context) {
- final requirementsAsync = ref.read(documentRequirementsProvider);
- final jhsReqs = requirementsAsync.maybeWhen(
- data: (reqs) => reqs
+ final realtimeData = ref.read(realtimeDocumentTypesProvider).asData?.value;
+ final requirementsData = ref.read(documentRequirementsProvider).asData?.value ?? [];
+ List jhsReqs = [];
+ List shsReqs = [];
+ List generalReqs = [];
+
+ if (realtimeData != null) {
+ jhsReqs = List.from(realtimeData['jhs'] ?? []);
+ shsReqs = List.from(realtimeData['shs'] ?? []);
+ generalReqs = List.from(realtimeData['general'] ?? []);
+ } else {
+ jhsReqs = requirementsData
.where((r) => r.category == 'JHS')
- .map((r) => r.name)
+ .map((r) => r.name.replaceFirst(RegExp(r'^(JHS|SHS)\s*-\s*', caseSensitive: false), ''))
.toSet()
.toList()
- ..sort(),
- orElse: () => [],
- );
- final shsReqs = requirementsAsync.maybeWhen(
- data: (reqs) => reqs
+ ..sort();
+ shsReqs = requirementsData
.where((r) => r.category == 'SHS')
- .map((r) => r.name)
+ .map((r) => r.name.replaceFirst(RegExp(r'^(JHS|SHS)\s*-\s*', caseSensitive: false), ''))
.toSet()
.toList()
- ..sort(),
- orElse: () => [],
- );
+ ..sort();
+ }
// Update cached lists for _applyFilters
if (jhsReqs.isNotEmpty) _jhsItems = jhsReqs;
@@ -1317,6 +1329,18 @@ class _ArchivesScreenState extends ConsumerState
}
}
+ if (generalReqs.isNotEmpty) {
+ items.add(const PopupMenuDivider(height: 8));
+ items.add(PopupMenuItem(
+ enabled: false,
+ height: 28,
+ child: buildHeader('OTHER DOCUMENTS'),
+ ));
+ for (final doc in generalReqs) {
+ items.add(buildItem(doc, doc, isSubItem: true));
+ }
+ }
+
return items;
},
),
diff --git a/frontend/lib/ui/screens/documents/documents_screen.dart b/frontend/lib/ui/screens/documents/documents_screen.dart
index 18263bf..62bf5e3 100644
--- a/frontend/lib/ui/screens/documents/documents_screen.dart
+++ b/frontend/lib/ui/screens/documents/documents_screen.dart
@@ -700,6 +700,17 @@ class _DocumentsScreenState extends ConsumerState
const SingleActivator(LogicalKeyboardKey.keyF, control: true): () {
_showSearchDialog(context);
},
+ const SingleActivator(LogicalKeyboardKey.keyU, control: true): () {
+ UploadOcrModal.show(
+ context,
+ prefilledStudentId:
+ _openedFolderStudentId ?? widget.initialStudentId,
+ prefilledLrn: ref.read(openedFolderProvider)?.lrn,
+ );
+ },
+ const SingleActivator(LogicalKeyboardKey.keyP, control: true): () {
+ PrintQueueModal.show(context);
+ },
},
child: Focus(
focusNode: _shortcutFocusNode,
@@ -1229,6 +1240,10 @@ class _DocumentsScreenState extends ConsumerState
tooltip: 'Filter by Document Type',
padding: const EdgeInsets.all(8),
constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
+ onOpened: () {
+ ref.invalidate(realtimeDocumentTypesProvider);
+ ref.invalidate(documentRequirementsProvider);
+ },
icon: Badge(
isLabelVisible: _getActiveFilterCount() > 0,
label: Text(_getActiveFilterCount().toString()),
@@ -1247,24 +1262,30 @@ class _DocumentsScreenState extends ConsumerState
_applyFilters();
},
itemBuilder: (context) {
- final jhsReqs = requirementsAsync.maybeWhen(
- data: (reqs) => reqs
+ final realtimeData = ref.read(realtimeDocumentTypesProvider).asData?.value;
+ final requirementsData = ref.read(documentRequirementsProvider).asData?.value ?? [];
+ List jhsReqs = [];
+ List shsReqs = [];
+ List generalReqs = [];
+
+ if (realtimeData != null) {
+ jhsReqs = List.from(realtimeData['jhs'] ?? []);
+ shsReqs = List.from(realtimeData['shs'] ?? []);
+ generalReqs = List.from(realtimeData['general'] ?? []);
+ } else {
+ jhsReqs = requirementsData
.where((r) => r.category == 'JHS')
- .map((r) => r.name as String)
+ .map((r) => r.name.replaceFirst(RegExp(r'^(JHS|SHS)\s*-\s*', caseSensitive: false), ''))
.toSet()
.toList()
- ..sort(),
- orElse: () => [],
- );
- final shsReqs = requirementsAsync.maybeWhen(
- data: (reqs) => reqs
+ ..sort();
+ shsReqs = requirementsData
.where((r) => r.category == 'SHS')
- .map((r) => r.name as String)
+ .map((r) => r.name.replaceFirst(RegExp(r'^(JHS|SHS)\s*-\s*', caseSensitive: false), ''))
.toSet()
.toList()
- ..sort(),
- orElse: () => [],
- );
+ ..sort();
+ }
// Update cached lists for _applyFilters
if (jhsReqs.isNotEmpty) _jhsItems = jhsReqs;
@@ -1347,6 +1368,18 @@ class _DocumentsScreenState extends ConsumerState
}
}
+ if (generalReqs.isNotEmpty) {
+ items.add(const PopupMenuDivider(height: 8));
+ items.add(PopupMenuItem(
+ enabled: false,
+ height: 28,
+ child: buildHeader('OTHER DOCUMENTS'),
+ ));
+ for (final doc in generalReqs) {
+ items.add(buildItem(doc, doc, isSubItem: true));
+ }
+ }
+
return items;
},
),
diff --git a/frontend/lib/ui/screens/settings/settings_screen.dart b/frontend/lib/ui/screens/settings/settings_screen.dart
index c34e41d..f661d9f 100644
--- a/frontend/lib/ui/screens/settings/settings_screen.dart
+++ b/frontend/lib/ui/screens/settings/settings_screen.dart
@@ -2317,9 +2317,25 @@ class _SettingsScreenState extends ConsumerState {
),
OutlinedButton.icon(
onPressed: () async {
- final uri = Uri.parse('https://github.com/ssbg04/TIS_RMS/issues');
- if (await canLaunchUrl(uri)) {
- await launchUrl(uri, mode: LaunchMode.externalApplication);
+ const url = 'https://github.com/ssbg04/TIS_RMS/issues';
+ final uri = Uri.parse(url);
+ bool launched = false;
+ try {
+ launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
+ if (!launched) {
+ launched = await launchUrl(uri, mode: LaunchMode.platformDefault);
+ }
+ } catch (_) {}
+ if (!launched) {
+ await Clipboard.setData(const ClipboardData(text: url));
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('Issue tracker link copied to clipboard: https://github.com/ssbg04/TIS_RMS/issues'),
+ backgroundColor: AppColors.primaryGreen,
+ ),
+ );
+ }
}
},
icon: const Icon(Icons.bug_report_outlined, size: 15),
@@ -2334,9 +2350,25 @@ class _SettingsScreenState extends ConsumerState {
),
OutlinedButton.icon(
onPressed: () async {
- final uri = Uri.parse('https://github.com/ssbg04/TIS_RMS');
- if (await canLaunchUrl(uri)) {
- await launchUrl(uri, mode: LaunchMode.externalApplication);
+ const url = 'https://github.com/ssbg04/TIS_RMS';
+ final uri = Uri.parse(url);
+ bool launched = false;
+ try {
+ launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
+ if (!launched) {
+ launched = await launchUrl(uri, mode: LaunchMode.platformDefault);
+ }
+ } catch (_) {}
+ if (!launched) {
+ await Clipboard.setData(const ClipboardData(text: url));
+ if (context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(
+ content: Text('GitHub repository link copied to clipboard: https://github.com/ssbg04/TIS_RMS'),
+ backgroundColor: AppColors.primaryGreen,
+ ),
+ );
+ }
}
},
icon: const Icon(Icons.code_rounded, size: 15),
diff --git a/frontend/lib/ui/screens/students/students_screen.dart b/frontend/lib/ui/screens/students/students_screen.dart
index 3b6c826..c6caf52 100644
--- a/frontend/lib/ui/screens/students/students_screen.dart
+++ b/frontend/lib/ui/screens/students/students_screen.dart
@@ -1066,6 +1066,9 @@ class _StudentsScreenState extends ConsumerState {
const SingleActivator(LogicalKeyboardKey.keyF, control: true): () {
_showSearchDialog(context);
},
+ const SingleActivator(LogicalKeyboardKey.keyN, control: true): () {
+ _openModal();
+ },
},
child: Focus(
focusNode: _shortcutFocusNode,
@@ -1232,7 +1235,7 @@ class _StudentsScreenState extends ConsumerState {
height: (MediaQuery.of(context).size.width < 700 ||
defaultTargetPlatform ==
TargetPlatform.android)
- ? 76
+ ? 0
: 16,
),
orElse: () => const SizedBox.shrink(),
@@ -1340,6 +1343,30 @@ class _StudentsScreenState extends ConsumerState {
const Spacer(),
+ // Right: Filter Button for Android in header tabs
+ if (isAndroid) ...[
+ IconButton(
+ onPressed: () => StudentFilterDialog.show(
+ context,
+ query: query,
+ isEnrolledTab: _viewTab == StudentViewTab.enrolled,
+ ),
+ padding: const EdgeInsets.all(8),
+ constraints: const BoxConstraints(minWidth: 36, minHeight: 36),
+ icon: Badge(
+ isLabelVisible: activeCount > 0,
+ label: Text(activeCount.toString()),
+ child: Icon(
+ Icons.tune_rounded,
+ size: 20,
+ color: activeCount > 0
+ ? AppColors.primaryGreen
+ : (isDark ? AppColors.darkTextPrimary : Colors.black87),
+ ),
+ ),
+ ),
+ ],
+
// Right: Action buttons (desktop / non-android)
if (!isAndroid) ...[
// Search IconButton
@@ -1677,7 +1704,7 @@ class _StudentsScreenState extends ConsumerState {
physics: const AlwaysScrollableScrollPhysics(),
padding: EdgeInsets.only(
top: isDesktop ? 10 : 14,
- bottom: isDesktop ? 20 : 24,
+ bottom: isDesktop ? 20 : (defaultTargetPlatform == TargetPlatform.android ? 8 : 16),
),
itemCount: students.length,
separatorBuilder: (ctx, index) => SizedBox(
diff --git a/frontend/lib/ui/screens/students/widgets/add_student_modal.dart b/frontend/lib/ui/screens/students/widgets/add_student_modal.dart
index 43ac39d..5ebc1c7 100644
--- a/frontend/lib/ui/screens/students/widgets/add_student_modal.dart
+++ b/frontend/lib/ui/screens/students/widgets/add_student_modal.dart
@@ -10,7 +10,6 @@ import '../../../../domain/repositories/student_repository.dart';
import '../../../providers/setup_provider.dart';
import '../../../providers/ocr_provider.dart';
import '../../../providers/student_provider.dart';
-import '../../../providers/document_provider.dart';
import '../../documents/widgets/document_preview_modal.dart';
import '../../../shared/inputs/document_source_picker.dart';
import 'ocr_enrollment_validation_modal.dart';
@@ -59,8 +58,6 @@ class _AddStudentModalState extends ConsumerState {
final List _ocrSavedEnrollments = [];
File? _ocrScannedFile;
- String _ocrScannedFileName = '';
- String? _selectedOcrDocType;
static const _extSuggestions = [
'JR.',
@@ -297,7 +294,6 @@ class _AddStudentModalState extends ConsumerState {
setState(() {
_errorMessage = null;
_ocrScannedFile = file;
- _ocrScannedFileName = fileName;
});
String? docType = _detectDocType(fileName);
@@ -308,7 +304,6 @@ class _AddStudentModalState extends ConsumerState {
if (docType == null || !mounted) return;
setState(() {
- _selectedOcrDocType = docType;
_isLoading = true;
});
@@ -334,7 +329,6 @@ class _AddStudentModalState extends ConsumerState {
_selectedDob = _parseFlexibleDob(ocrResult.dob);
}
_ocrScannedFile = file;
- _selectedOcrDocType = docType;
_currentStep = 1;
});
@@ -425,7 +419,7 @@ class _AddStudentModalState extends ConsumerState {
setState(() => _isLoading = true);
try {
final notifier = ref.read(studentMutationProvider.notifier);
- final studentId = await notifier.createStudent(
+ await notifier.createStudent(
lrn: _lrnController.text.trim(),
firstName: _firstNameController.text.trim(),
middleName: _middleNameController.text.trim().isEmpty
@@ -444,56 +438,9 @@ class _AddStudentModalState extends ConsumerState {
is4ps: _is4ps,
);
- // After the student's directory is created, upload the scanned
- // SF9/SF10 document into it.
- String? uploadError;
- if (_ocrScannedFile != null) {
- try {
- final bytes = await _ocrScannedFile!.readAsBytes();
- final docType = _selectedOcrDocType ?? 'SF9';
- final originalName = _ocrScannedFileName.isEmpty
- ? _ocrScannedFile!.path.split(RegExp(r'[\\/]')).last
- : _ocrScannedFileName;
- final ext = originalName.contains('.')
- ? originalName.split('.').last
- : 'pdf';
- final lrn = _lrnController.text.trim();
- final renamedFileName = '${docType}_$lrn.$ext';
-
- await ref.read(documentRepositoryProvider).uploadDocumentBytes(
- studentId: studentId,
- documentType: docType,
- fileName: renamedFileName,
- bytes: bytes,
- );
- } catch (e) {
- final raw = e.toString();
- uploadError = raw.startsWith('Exception: ') ? raw.substring(11) : raw;
- }
- }
-
if (!mounted) return;
ref.invalidate(studentPageProvider);
if (mounted) setState(() => _isLoading = false);
- if (uploadError != null) {
- await showDialog(
- context: context,
- builder: (ctx) => AlertDialog(
- title: const Text('Student Created - Upload Failed'),
- content: Text(
- 'The student was created successfully, but the scanned '
- 'document could not be uploaded to their folder.\n\n'
- '$uploadError',
- ),
- actions: [
- TextButton(
- onPressed: () => Navigator.of(ctx).pop(),
- child: const Text('OK'),
- ),
- ],
- ),
- );
- }
if (!mounted) return;
Navigator.of(context).pop(true);
} catch (e) {
@@ -810,8 +757,6 @@ class _AddStudentModalState extends ConsumerState {
onPressed: () => setState(() {
_errorMessage = null;
_ocrScannedFile = null;
- _ocrScannedFileName = '';
- _selectedOcrDocType = null;
_ocrSavedEnrollments.clear();
_lrnController.text = '308035';
_firstNameController.clear();
diff --git a/frontend/lib/ui/screens/students/widgets/bulk_ocr_import_dialog.dart b/frontend/lib/ui/screens/students/widgets/bulk_ocr_import_dialog.dart
index 55c481f..870f979 100644
--- a/frontend/lib/ui/screens/students/widgets/bulk_ocr_import_dialog.dart
+++ b/frontend/lib/ui/screens/students/widgets/bulk_ocr_import_dialog.dart
@@ -9,7 +9,6 @@ import 'package:desktop_drop/desktop_drop.dart';
import '../../../../core/constants/app_colors.dart';
import '../../../../domain/entities/setup_models.dart';
-import '../../../providers/document_provider.dart';
import '../../../providers/ocr_provider.dart';
import '../../../providers/student_provider.dart';
import '../../../providers/setup_provider.dart';
@@ -773,49 +772,7 @@ class _BulkOcrImportDialogState extends ConsumerState {
.read(studentMutationProvider.notifier)
.bulkCreateStudents(payload);
- // Auto-upload scanned documents for newly created students
- final rawResults = result['results'];
- if (rawResults is List) {
- for (final r in rawResults) {
- if (r is Map && r['status'] == 'created' && r['id'] != null) {
- final studentId = r['id'] as int;
- final studentLrn = (r['lrn'] ?? '').toString();
- // Find corresponding _OcrItem with local scanned file
- final matchingItem = validItems.cast<_OcrItem?>().firstWhere(
- (item) =>
- item != null &&
- item.lrn.trim() == studentLrn.trim() &&
- !item.filePath.startsWith('csv_row_'),
- orElse: () => null,
- );
- if (matchingItem != null) {
- final localFile = File(matchingItem.filePath);
- if (await localFile.exists()) {
- try {
- final bytes = await localFile.readAsBytes();
- final docType = matchingItem.docType ?? 'SF9';
- final originalName = matchingItem.fileName;
- final ext = originalName.contains('.')
- ? originalName.split('.').last
- : 'pdf';
- // Renaming convention according to docType: e.g. SF9_308035123456.pdf or SF10_308035123456.pdf
- final renamedFileName = '${docType}_${matchingItem.lrn}.$ext';
-
- await ref.read(documentRepositoryProvider).uploadDocumentBytes(
- studentId: studentId,
- documentType: docType,
- fileName: renamedFileName,
- bytes: bytes,
- );
- } catch (upErr) {
- debugPrint('Failed to auto-upload OCR doc for student $studentId: $upErr');
- }
- }
- }
- }
- }
- }
setState(() {
_importResult = result;
diff --git a/frontend/lib/ui/screens/users/users_screen.dart b/frontend/lib/ui/screens/users/users_screen.dart
index 2189385..506755e 100644
--- a/frontend/lib/ui/screens/users/users_screen.dart
+++ b/frontend/lib/ui/screens/users/users_screen.dart
@@ -2006,7 +2006,7 @@ class _AddUserModalContentState extends ConsumerState<_AddUserModalContent> {
}
final username = _usernameCtrl.text.trim();
- await ref.read(usersProvider.notifier).createUser(
+ final tempPassword = await ref.read(usersProvider.notifier).createUser(
username: username,
firstName: _firstNameCtrl.text.trim(),
middleName: _middleNameCtrl.text.trim(),
@@ -2022,6 +2022,7 @@ class _AddUserModalContentState extends ConsumerState<_AddUserModalContent> {
_showUserCreatedSuccessDialog(
context,
username: username,
+ temporaryPassword: tempPassword,
email: _emailCtrl.text.trim(),
);
} catch (e) {
@@ -2401,6 +2402,7 @@ Widget _buildEmailDomainSuggestions(
void _showUserCreatedSuccessDialog(
BuildContext ctx, {
required String username,
+ required String temporaryPassword,
String? email,
}) {
final isDark = Theme.of(ctx).brightness == Brightness.dark;
@@ -2426,6 +2428,13 @@ void _showUserCreatedSuccessDialog(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_credentialRow(dialogCtx, 'Username', username),
+ const SizedBox(height: 10),
+ _credentialRow(
+ dialogCtx,
+ 'Temporary Password',
+ temporaryPassword,
+ highlight: true,
+ ),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
diff --git a/frontend/pubspec.yaml b/frontend/pubspec.yaml
index 52494a4..9e7ef3d 100644
--- a/frontend/pubspec.yaml
+++ b/frontend/pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
-version: 1.0.28+28
+version: 1.0.31+31
environment:
sdk: ^3.11.5