feat: scope dashboard KPIs to teacher students and restrict teacher actions on documents screen
This commit is contained in:
1 parent
b57bcffa41
commit
57e0c201f8
5 files changed
+460
-189
No files matched your search
@@ -254,37 +254,375 @@ exports.getKpis = (req, res) => {
|
||||
const isTeacher = req.user?.role?.toLowerCase() === 'teacher';
|
||||
const userId = req.user.id;
|
||||
|
||||
// ── 1. Digitalization — JHS/SHS donut ───────────────────────────
|
||||
// Total docs uploaded per category vs. students with at least one doc
|
||||
const jhsTotal = db.prepare(`
|
||||
SELECT COUNT(DISTINCT s.id) as count
|
||||
FROM students s
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.grade_level <= 10
|
||||
`).get().count;
|
||||
let jhsTotal, shsTotal, jhsDigitized, shsDigitized;
|
||||
let activityByDay;
|
||||
let statusDistribution;
|
||||
let topStudents, bottomStudents;
|
||||
let docTypeBreakdown;
|
||||
let uploadTrend;
|
||||
let fileRows, recentRows, olderRows;
|
||||
|
||||
const shsTotal = db.prepare(`
|
||||
SELECT COUNT(DISTINCT s.id) as count
|
||||
FROM students s
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.grade_level > 10
|
||||
`).get().count;
|
||||
if (isTeacher) {
|
||||
// ── 1. Digitalization (Teacher's Students) ───────────────────────────
|
||||
jhsTotal = db.prepare(`
|
||||
SELECT COUNT(DISTINCT s.id) as count
|
||||
FROM students s
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.id = (
|
||||
SELECT e2.id FROM enrollments e2
|
||||
JOIN academic_years ay ON e2.academic_year_id = ay.id
|
||||
WHERE e2.student_id = s.id
|
||||
ORDER BY ay.year_range DESC, e2.grade_level DESC, e2.id DESC LIMIT 1
|
||||
)
|
||||
AND e.section_id IN (
|
||||
SELECT section_id FROM teacher_sections WHERE teacher_id = ?
|
||||
)
|
||||
AND e.grade_level <= 10
|
||||
`).get(userId).count;
|
||||
|
||||
const jhsDigitized = db.prepare(`
|
||||
SELECT COUNT(DISTINCT s.id) as count
|
||||
FROM students s
|
||||
JOIN documents d ON d.student_id = s.id
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.grade_level <= 10 AND d.deleted_at IS NULL
|
||||
`).get().count;
|
||||
shsTotal = db.prepare(`
|
||||
SELECT COUNT(DISTINCT s.id) as count
|
||||
FROM students s
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.id = (
|
||||
SELECT e2.id FROM enrollments e2
|
||||
JOIN academic_years ay ON e2.academic_year_id = ay.id
|
||||
WHERE e2.student_id = s.id
|
||||
ORDER BY ay.year_range DESC, e2.grade_level DESC, e2.id DESC LIMIT 1
|
||||
)
|
||||
AND e.section_id IN (
|
||||
SELECT section_id FROM teacher_sections WHERE teacher_id = ?
|
||||
)
|
||||
AND e.grade_level > 10
|
||||
`).get(userId).count;
|
||||
|
||||
const shsDigitized = db.prepare(`
|
||||
SELECT COUNT(DISTINCT s.id) as count
|
||||
FROM students s
|
||||
JOIN documents d ON d.student_id = s.id
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.grade_level > 10 AND d.deleted_at IS NULL
|
||||
`).get().count;
|
||||
jhsDigitized = db.prepare(`
|
||||
SELECT COUNT(DISTINCT s.id) as count
|
||||
FROM students s
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
JOIN documents d ON d.student_id = s.id AND d.deleted_at IS NULL
|
||||
WHERE e.id = (
|
||||
SELECT e2.id FROM enrollments e2
|
||||
JOIN academic_years ay ON e2.academic_year_id = ay.id
|
||||
WHERE e2.student_id = s.id
|
||||
ORDER BY ay.year_range DESC, e2.grade_level DESC, e2.id DESC LIMIT 1
|
||||
)
|
||||
AND e.section_id IN (
|
||||
SELECT section_id FROM teacher_sections WHERE teacher_id = ?
|
||||
)
|
||||
AND e.grade_level <= 10
|
||||
`).get(userId).count;
|
||||
|
||||
shsDigitized = db.prepare(`
|
||||
SELECT COUNT(DISTINCT s.id) as count
|
||||
FROM students s
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
JOIN documents d ON d.student_id = s.id AND d.deleted_at IS NULL
|
||||
WHERE e.id = (
|
||||
SELECT e2.id FROM enrollments e2
|
||||
JOIN academic_years ay ON e2.academic_year_id = ay.id
|
||||
WHERE e2.student_id = s.id
|
||||
ORDER BY ay.year_range DESC, e2.grade_level DESC, e2.id DESC LIMIT 1
|
||||
)
|
||||
AND e.section_id IN (
|
||||
SELECT section_id FROM teacher_sections WHERE teacher_id = ?
|
||||
)
|
||||
AND e.grade_level > 10
|
||||
`).get(userId).count;
|
||||
|
||||
// ── 2. Activity by day (Teacher's Activities / Students) ────────────
|
||||
activityByDay = db.prepare(`
|
||||
SELECT
|
||||
DATE(a.created_at) as day,
|
||||
a.action,
|
||||
a.entity_type,
|
||||
COUNT(*) as count
|
||||
FROM activity_log a
|
||||
WHERE DATE(a.created_at) >= DATE('now', '-6 days')
|
||||
AND (
|
||||
a.user_id = ?
|
||||
OR (a.entity_type = 'student' AND a.entity_id IN (
|
||||
SELECT s.id FROM students s
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
JOIN teacher_sections ts ON e.section_id = ts.section_id
|
||||
WHERE ts.teacher_id = ?
|
||||
))
|
||||
OR (a.entity_type = 'document' AND a.entity_id IN (
|
||||
SELECT d.id FROM documents d
|
||||
JOIN enrollments e ON d.student_id = e.student_id
|
||||
JOIN teacher_sections ts ON e.section_id = ts.section_id
|
||||
WHERE ts.teacher_id = ?
|
||||
))
|
||||
)
|
||||
GROUP BY day, a.action, a.entity_type
|
||||
ORDER BY day ASC
|
||||
`).all(userId, userId, userId);
|
||||
|
||||
// ── 3. Document status distribution (Teacher's Students) ─────────────
|
||||
const statusRows = db.prepare(`
|
||||
SELECT d.status, COUNT(*) as count
|
||||
FROM documents d
|
||||
JOIN students s ON d.student_id = s.id
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.id = (
|
||||
SELECT e2.id FROM enrollments e2
|
||||
JOIN academic_years ay ON e2.academic_year_id = ay.id
|
||||
WHERE e2.student_id = s.id
|
||||
ORDER BY ay.year_range DESC, e2.grade_level DESC, e2.id DESC LIMIT 1
|
||||
)
|
||||
AND e.section_id IN (
|
||||
SELECT section_id FROM teacher_sections WHERE teacher_id = ?
|
||||
)
|
||||
AND d.deleted_at IS NULL
|
||||
GROUP BY d.status
|
||||
`).all(userId);
|
||||
statusDistribution = statusRows.map(r => ({ status: r.status, count: r.count }));
|
||||
|
||||
// ── 4. Top & bottom students (Teacher's Students) ──────────────────
|
||||
const studentDocCounts = db.prepare(`
|
||||
SELECT
|
||||
s.id,
|
||||
s.first_name || ' ' || s.last_name as name,
|
||||
COUNT(d.id) as doc_count
|
||||
FROM students s
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
LEFT JOIN documents d ON d.student_id = s.id AND d.deleted_at IS NULL
|
||||
WHERE e.id = (
|
||||
SELECT e2.id FROM enrollments e2
|
||||
JOIN academic_years ay ON e2.academic_year_id = ay.id
|
||||
WHERE e2.student_id = s.id
|
||||
ORDER BY ay.year_range DESC, e2.grade_level DESC, e2.id DESC LIMIT 1
|
||||
)
|
||||
AND e.section_id IN (
|
||||
SELECT section_id FROM teacher_sections WHERE teacher_id = ?
|
||||
)
|
||||
GROUP BY s.id
|
||||
ORDER BY doc_count DESC
|
||||
`).all(userId);
|
||||
|
||||
topStudents = studentDocCounts.slice(0, 5);
|
||||
bottomStudents = [...studentDocCounts].sort((a, b) => a.doc_count - b.doc_count).slice(0, 5);
|
||||
|
||||
// ── 5. Document type breakdown (Teacher's Students) ────────────────
|
||||
const typeRows = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(dr.name, d.document_type, 'Uncategorized') as type_name,
|
||||
COUNT(*) as count
|
||||
FROM documents d
|
||||
JOIN students s ON d.student_id = s.id
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
LEFT JOIN document_requirements dr ON d.requirement_id = dr.id
|
||||
WHERE e.id = (
|
||||
SELECT e2.id FROM enrollments e2
|
||||
JOIN academic_years ay ON e2.academic_year_id = ay.id
|
||||
WHERE e2.student_id = s.id
|
||||
ORDER BY ay.year_range DESC, e2.grade_level DESC, e2.id DESC LIMIT 1
|
||||
)
|
||||
AND e.section_id IN (
|
||||
SELECT section_id FROM teacher_sections WHERE teacher_id = ?
|
||||
)
|
||||
AND d.deleted_at IS NULL
|
||||
GROUP BY type_name
|
||||
ORDER BY count DESC
|
||||
LIMIT 10
|
||||
`).all(userId);
|
||||
docTypeBreakdown = typeRows.map(r => ({ name: r.type_name, count: r.count }));
|
||||
|
||||
// ── 6. Upload trend (Teacher's Students) ───────────────────────────
|
||||
uploadTrend = db.prepare(`
|
||||
SELECT
|
||||
DATE(d.created_at) as day,
|
||||
COUNT(*) as count
|
||||
FROM documents d
|
||||
JOIN students s ON d.student_id = s.id
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.id = (
|
||||
SELECT e2.id FROM enrollments e2
|
||||
JOIN academic_years ay ON e2.academic_year_id = ay.id
|
||||
WHERE e2.student_id = s.id
|
||||
ORDER BY ay.year_range DESC, e2.grade_level DESC, e2.id DESC LIMIT 1
|
||||
)
|
||||
AND e.section_id IN (
|
||||
SELECT section_id FROM teacher_sections WHERE teacher_id = ?
|
||||
)
|
||||
AND DATE(d.created_at) >= DATE('now', '-29 days')
|
||||
AND d.deleted_at IS NULL
|
||||
GROUP BY day
|
||||
ORDER BY day ASC
|
||||
`).all(userId);
|
||||
|
||||
// ── 7. Storage analytics (Teacher's Students) ──────────────────────
|
||||
fileRows = db.prepare(`
|
||||
SELECT d.file_path, d.document_type,
|
||||
COALESCE(dr.name, d.document_type, 'Uncategorized') as type_name
|
||||
FROM documents d
|
||||
JOIN students s ON d.student_id = s.id
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
LEFT JOIN document_requirements dr ON d.requirement_id = dr.id
|
||||
WHERE e.id = (
|
||||
SELECT e2.id FROM enrollments e2
|
||||
JOIN academic_years ay ON e2.academic_year_id = ay.id
|
||||
WHERE e2.student_id = s.id
|
||||
ORDER BY ay.year_range DESC, e2.grade_level DESC, e2.id DESC LIMIT 1
|
||||
)
|
||||
AND e.section_id IN (
|
||||
SELECT section_id FROM teacher_sections WHERE teacher_id = ?
|
||||
)
|
||||
AND d.deleted_at IS NULL
|
||||
`).all(userId);
|
||||
|
||||
recentRows = db.prepare(`
|
||||
SELECT d.file_path FROM documents d
|
||||
JOIN students s ON d.student_id = s.id
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.id = (
|
||||
SELECT e2.id FROM enrollments e2
|
||||
JOIN academic_years ay ON e2.academic_year_id = ay.id
|
||||
WHERE e2.student_id = s.id
|
||||
ORDER BY ay.year_range DESC, e2.grade_level DESC, e2.id DESC LIMIT 1
|
||||
)
|
||||
AND e.section_id IN (
|
||||
SELECT section_id FROM teacher_sections WHERE teacher_id = ?
|
||||
)
|
||||
AND d.deleted_at IS NULL
|
||||
AND d.created_at >= datetime('now', '-30 days')
|
||||
`).all(userId);
|
||||
|
||||
olderRows = db.prepare(`
|
||||
SELECT d.file_path FROM documents d
|
||||
JOIN students s ON d.student_id = s.id
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.id = (
|
||||
SELECT e2.id FROM enrollments e2
|
||||
JOIN academic_years ay ON e2.academic_year_id = ay.id
|
||||
WHERE e2.student_id = s.id
|
||||
ORDER BY ay.year_range DESC, e2.grade_level DESC, e2.id DESC LIMIT 1
|
||||
)
|
||||
AND e.section_id IN (
|
||||
SELECT section_id FROM teacher_sections WHERE teacher_id = ?
|
||||
)
|
||||
AND d.deleted_at IS NULL
|
||||
AND d.created_at >= datetime('now', '-60 days')
|
||||
AND d.created_at < datetime('now', '-30 days')
|
||||
`).all(userId);
|
||||
} else {
|
||||
// Admin: All students across the school
|
||||
// ── 1. Digitalization — JHS/SHS donut ───────────────────────────
|
||||
jhsTotal = db.prepare(`
|
||||
SELECT COUNT(DISTINCT s.id) as count
|
||||
FROM students s
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.grade_level <= 10
|
||||
`).get().count;
|
||||
|
||||
shsTotal = db.prepare(`
|
||||
SELECT COUNT(DISTINCT s.id) as count
|
||||
FROM students s
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.grade_level > 10
|
||||
`).get().count;
|
||||
|
||||
jhsDigitized = db.prepare(`
|
||||
SELECT COUNT(DISTINCT s.id) as count
|
||||
FROM students s
|
||||
JOIN documents d ON d.student_id = s.id
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.grade_level <= 10 AND d.deleted_at IS NULL
|
||||
`).get().count;
|
||||
|
||||
shsDigitized = db.prepare(`
|
||||
SELECT COUNT(DISTINCT s.id) as count
|
||||
FROM students s
|
||||
JOIN documents d ON d.student_id = s.id
|
||||
JOIN enrollments e ON s.id = e.student_id
|
||||
WHERE e.grade_level > 10 AND d.deleted_at IS NULL
|
||||
`).get().count;
|
||||
|
||||
// ── 2. Activity by day — last 7 days bar chart ──────────────────
|
||||
activityByDay = db.prepare(`
|
||||
SELECT
|
||||
DATE(created_at) as day,
|
||||
action,
|
||||
entity_type,
|
||||
COUNT(*) as count
|
||||
FROM activity_log
|
||||
WHERE DATE(created_at) >= DATE('now', '-6 days')
|
||||
GROUP BY day, action, entity_type
|
||||
ORDER BY day ASC
|
||||
`).all();
|
||||
|
||||
// ── 3. Document status distribution ─────────────────────────────
|
||||
const statusRows = db.prepare(`
|
||||
SELECT status, COUNT(*) as count
|
||||
FROM documents
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY status
|
||||
`).all();
|
||||
statusDistribution = statusRows.map(r => ({ status: r.status, count: r.count }));
|
||||
|
||||
// ── 4. Top & bottom students by document count ──────────────────
|
||||
const studentDocCounts = db.prepare(`
|
||||
SELECT
|
||||
s.id,
|
||||
s.first_name || ' ' || s.last_name as name,
|
||||
COUNT(d.id) as doc_count
|
||||
FROM students s
|
||||
LEFT JOIN documents d ON d.student_id = s.id AND d.deleted_at IS NULL
|
||||
GROUP BY s.id
|
||||
ORDER BY doc_count DESC
|
||||
`).all();
|
||||
|
||||
topStudents = studentDocCounts.slice(0, 5);
|
||||
bottomStudents = [...studentDocCounts].sort((a, b) => a.doc_count - b.doc_count).slice(0, 5);
|
||||
|
||||
// ── 5. Document type breakdown pie ──────────────────────────────
|
||||
const typeRows = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(dr.name, d.document_type, 'Uncategorized') as type_name,
|
||||
COUNT(*) as count
|
||||
FROM documents d
|
||||
LEFT JOIN document_requirements dr ON d.requirement_id = dr.id
|
||||
WHERE d.deleted_at IS NULL
|
||||
GROUP BY type_name
|
||||
ORDER BY count DESC
|
||||
LIMIT 10
|
||||
`).all();
|
||||
docTypeBreakdown = typeRows.map(r => ({ name: r.type_name, count: r.count }));
|
||||
|
||||
// ── 6. Upload trend — last 30 days line chart ───────────────────
|
||||
uploadTrend = db.prepare(`
|
||||
SELECT
|
||||
DATE(created_at) as day,
|
||||
COUNT(*) as count
|
||||
FROM documents
|
||||
WHERE DATE(created_at) >= DATE('now', '-29 days')
|
||||
AND deleted_at IS NULL
|
||||
GROUP BY day
|
||||
ORDER BY day ASC
|
||||
`).all();
|
||||
|
||||
// ── 7. Storage analytics — compute sizes from disk ──────────────
|
||||
fileRows = db.prepare(`
|
||||
SELECT file_path, document_type,
|
||||
COALESCE(dr.name, document_type, 'Uncategorized') as type_name
|
||||
FROM documents d
|
||||
LEFT JOIN document_requirements dr ON d.requirement_id = dr.id
|
||||
WHERE deleted_at IS NULL
|
||||
`).all();
|
||||
|
||||
recentRows = db.prepare(`
|
||||
SELECT file_path FROM documents
|
||||
WHERE deleted_at IS NULL
|
||||
AND created_at >= datetime('now', '-30 days')
|
||||
`).all();
|
||||
|
||||
olderRows = db.prepare(`
|
||||
SELECT file_path FROM documents
|
||||
WHERE deleted_at IS NULL
|
||||
AND created_at >= datetime('now', '-60 days')
|
||||
AND created_at < datetime('now', '-30 days')
|
||||
`).all();
|
||||
}
|
||||
|
||||
const digitalization = {
|
||||
jhs: { total: jhsTotal, digitized: jhsDigitized },
|
||||
@@ -295,78 +633,6 @@ exports.getKpis = (req, res) => {
|
||||
},
|
||||
};
|
||||
|
||||
// ── 2. Activity by day — last 7 days bar chart ──────────────────
|
||||
const activityByDay = db.prepare(`
|
||||
SELECT
|
||||
DATE(created_at) as day,
|
||||
action,
|
||||
entity_type,
|
||||
COUNT(*) as count
|
||||
FROM activity_log
|
||||
WHERE DATE(created_at) >= DATE('now', '-6 days')
|
||||
GROUP BY day, action, entity_type
|
||||
ORDER BY day ASC
|
||||
`).all();
|
||||
|
||||
// ── 3. Document status distribution ─────────────────────────────
|
||||
const statusRows = db.prepare(`
|
||||
SELECT status, COUNT(*) as count
|
||||
FROM documents
|
||||
WHERE deleted_at IS NULL
|
||||
GROUP BY status
|
||||
`).all();
|
||||
const statusDistribution = statusRows.map(r => ({ status: r.status, count: r.count }));
|
||||
|
||||
// ── 4. Top & bottom students by document count ──────────────────
|
||||
const studentDocCounts = db.prepare(`
|
||||
SELECT
|
||||
s.id,
|
||||
s.first_name || ' ' || s.last_name as name,
|
||||
COUNT(d.id) as doc_count
|
||||
FROM students s
|
||||
LEFT JOIN documents d ON d.student_id = s.id AND d.deleted_at IS NULL
|
||||
GROUP BY s.id
|
||||
ORDER BY doc_count DESC
|
||||
`).all();
|
||||
|
||||
const topStudents = studentDocCounts.slice(0, 5);
|
||||
const bottomStudents = [...studentDocCounts].sort((a, b) => a.doc_count - b.doc_count).slice(0, 5);
|
||||
|
||||
// ── 5. Document type breakdown pie ──────────────────────────────
|
||||
const typeRows = db.prepare(`
|
||||
SELECT
|
||||
COALESCE(dr.name, d.document_type, 'Uncategorized') as type_name,
|
||||
COUNT(*) as count
|
||||
FROM documents d
|
||||
LEFT JOIN document_requirements dr ON d.requirement_id = dr.id
|
||||
WHERE d.deleted_at IS NULL
|
||||
GROUP BY type_name
|
||||
ORDER BY count DESC
|
||||
LIMIT 10
|
||||
`).all();
|
||||
const docTypeBreakdown = typeRows.map(r => ({ name: r.type_name, count: r.count }));
|
||||
|
||||
// ── 6. Upload trend — last 30 days line chart ───────────────────
|
||||
const uploadTrend = db.prepare(`
|
||||
SELECT
|
||||
DATE(created_at) as day,
|
||||
COUNT(*) as count
|
||||
FROM documents
|
||||
WHERE DATE(created_at) >= DATE('now', '-29 days')
|
||||
AND deleted_at IS NULL
|
||||
GROUP BY day
|
||||
ORDER BY day ASC
|
||||
`).all();
|
||||
|
||||
// ── 7. Storage analytics — compute sizes from disk ──────────────
|
||||
const fileRows = db.prepare(`
|
||||
SELECT file_path, document_type,
|
||||
COALESCE(dr.name, document_type, 'Uncategorized') as type_name
|
||||
FROM documents d
|
||||
LEFT JOIN document_requirements dr ON d.requirement_id = dr.id
|
||||
WHERE deleted_at IS NULL
|
||||
`).all();
|
||||
|
||||
let totalBytes = 0;
|
||||
const byType = {};
|
||||
let filesWithSize = 0;
|
||||
@@ -384,18 +650,6 @@ exports.getKpis = (req, res) => {
|
||||
}
|
||||
|
||||
// Growth rate: bytes added in last 30 days vs prior 30 days
|
||||
const recentRows = db.prepare(`
|
||||
SELECT file_path FROM documents
|
||||
WHERE deleted_at IS NULL
|
||||
AND created_at >= datetime('now', '-30 days')
|
||||
`).all();
|
||||
const olderRows = db.prepare(`
|
||||
SELECT file_path FROM documents
|
||||
WHERE deleted_at IS NULL
|
||||
AND created_at >= datetime('now', '-60 days')
|
||||
AND created_at < datetime('now', '-30 days')
|
||||
`).all();
|
||||
|
||||
const sumSize = (rows) => rows.reduce((acc, r) => {
|
||||
try {
|
||||
if (r.file_path && fs.existsSync(r.file_path)) {
|
||||
|
||||
@@ -8,15 +8,15 @@ router.get('/requirements', authenticateToken, documentController.getRequirement
|
||||
router.get('/statuses', authenticateToken, documentController.getStatuses);
|
||||
|
||||
// Print Queue routes — must be BEFORE /:id routes to avoid conflict
|
||||
router.get('/print-queue', authenticateToken, documentController.getPrintQueue);
|
||||
router.get('/print-history', authenticateToken, documentController.getPrintHistory);
|
||||
router.delete('/print-history/clear', authenticateToken, documentController.clearPrintHistory);
|
||||
router.post('/print-queue', authenticateToken, documentController.addToPrintQueue);
|
||||
router.post('/print-queue/print', authenticateToken, documentController.executePrintQueue);
|
||||
router.delete('/print-queue/clear', authenticateToken, documentController.clearPrintQueue);
|
||||
router.delete('/print-queue/:queueId', authenticateToken, documentController.removeFromPrintQueue);
|
||||
router.get('/print-queue', authenticateToken, authorizeRoles('admin'), documentController.getPrintQueue);
|
||||
router.get('/print-history', authenticateToken, authorizeRoles('admin'), documentController.getPrintHistory);
|
||||
router.delete('/print-history/clear', authenticateToken, authorizeRoles('admin'), documentController.clearPrintHistory);
|
||||
router.post('/print-queue', authenticateToken, authorizeRoles('admin'), documentController.addToPrintQueue);
|
||||
router.post('/print-queue/print', authenticateToken, authorizeRoles('admin'), documentController.executePrintQueue);
|
||||
router.delete('/print-queue/clear', authenticateToken, authorizeRoles('admin'), documentController.clearPrintQueue);
|
||||
router.delete('/print-queue/:queueId', authenticateToken, authorizeRoles('admin'), documentController.removeFromPrintQueue);
|
||||
|
||||
router.post('/upload', authenticateToken, documentController.uploadMiddleware, documentController.uploadDocument);
|
||||
router.post('/upload', authenticateToken, authorizeRoles('admin'), documentController.uploadMiddleware, documentController.uploadDocument);
|
||||
router.get('/student/:studentId', authenticateToken, documentController.getDocumentsByStudent);
|
||||
|
||||
// Recycle Bin / Trash routes — must be BEFORE /:id routes
|
||||
@@ -27,11 +27,11 @@ router.post('/bulk-permanent-delete', authenticateToken, authorizeRoles('admin')
|
||||
// Bulk routes — must be BEFORE /:id routes to avoid conflict
|
||||
router.post('/bulk-delete', authenticateToken, authorizeRoles('admin'), documentController.bulkDelete);
|
||||
router.post('/bulk-status', authenticateToken, authorizeRoles('admin'), documentController.bulkStatus);
|
||||
router.post('/bulk-print', authenticateToken, documentController.bulkAddToPrintQueue);
|
||||
router.post('/bulk-copy', authenticateToken, documentController.bulkCopy);
|
||||
router.post('/bulk-print', authenticateToken, authorizeRoles('admin'), documentController.bulkAddToPrintQueue);
|
||||
router.post('/bulk-copy', authenticateToken, authorizeRoles('admin'), documentController.bulkCopy);
|
||||
|
||||
router.get('/:id/view', authenticateToken, documentController.viewDocument);
|
||||
router.post('/:id/copy', authenticateToken, documentController.copyDocument);
|
||||
router.post('/:id/copy', authenticateToken, authorizeRoles('admin'), documentController.copyDocument);
|
||||
router.patch('/:id/status', authenticateToken, authorizeRoles('admin'), documentController.updateStatus);
|
||||
router.post('/:id/restore', authenticateToken, authorizeRoles('admin'), documentController.restoreDocument);
|
||||
router.delete('/:id/permanent', authenticateToken, authorizeRoles('admin'), documentController.permanentDeleteDocument);
|
||||
|
||||
+13
-8
@@ -147,15 +147,20 @@ Files backup/restore - mirror mode or zip -->
|
||||
- SF9 is equal to report card, Form 138, School Form 9, Student Report Card, SF1 for JHS/SF9 for SHS
|
||||
- SF10 is equal to School Form 10, Form 137, Form 137-A, Student Permanent Record -->
|
||||
|
||||
# STUDENT SCREEN
|
||||
- edit student screen -> enrollments tab -> add/edit enrollement modal
|
||||
- add more enrollment confirmation to add, yes and no
|
||||
- in the add more confirmation if no, clears section and grade level and reset academic year, and grade level to default 7.
|
||||
# DASHBOARD SCREEN (Teachers side)
|
||||
- Analytics an KPIs
|
||||
- make fetch only their students, not all students in the database
|
||||
- digitalization rate of their students
|
||||
- document status distribution of their students
|
||||
- document type breakdown of their students
|
||||
- top students by documents of their student
|
||||
- needs attention of their students
|
||||
|
||||
# DOCUMENT SCREEN (Teachers side)
|
||||
- hide or unauthorize teacher role to print and upload documents
|
||||
- hide or disable hold and right click, multi select, copy, just hide the show menu and hold/right click functions for teachers role
|
||||
|
||||
|
||||
- change the 4ps color to #2e3192
|
||||
1. student profile modal
|
||||
2. edit student screen
|
||||
3. add student screen
|
||||
---
|
||||
|
||||
# TIS_RMS Server Manager (dont do)
|
||||
|
||||
@@ -668,8 +668,8 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
// Left FAB: Print List (Only for Android/mobile app)
|
||||
if (isMobile)
|
||||
// Left FAB: Print List (Only for Android/mobile app, admin only)
|
||||
if (isMobile && widget.userRole != 'teacher')
|
||||
Badge(
|
||||
label: Text(
|
||||
'${ref.watch(printQueueProvider).value?.length ?? 0}',
|
||||
@@ -691,8 +691,8 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
)
|
||||
else
|
||||
const SizedBox.shrink(),
|
||||
// Right FAB: Upload Document
|
||||
if (isMobile)
|
||||
// Right FAB: Upload Document (admin only)
|
||||
if (isMobile && widget.userRole != 'teacher')
|
||||
if (_tabController.index == 1 || isFolderOpened)
|
||||
FloatingActionButton(
|
||||
heroTag: 'fab-upload-doc',
|
||||
@@ -1091,8 +1091,8 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
|
||||
// Desktop action buttons (Upload available to all roles)
|
||||
if (!isMobile && _tabController.index != 2) ...[
|
||||
// Desktop action buttons (Upload available to admins only)
|
||||
if (!isMobile && _tabController.index != 2 && widget.userRole != 'teacher') ...[
|
||||
SizedBox(
|
||||
height: 36,
|
||||
child: ElevatedButton.icon(
|
||||
@@ -1122,7 +1122,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
|
||||
if (defaultTargetPlatform != TargetPlatform.android && !isMobile) ...[
|
||||
if (defaultTargetPlatform != TargetPlatform.android && !isMobile && widget.userRole != 'teacher') ...[
|
||||
SizedBox(
|
||||
height: 36,
|
||||
child: _buildPrintQueueButton(compact: false),
|
||||
@@ -1186,7 +1186,7 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
if (_tabController.index == 1 || _openedFolderStudentId != null) ...[
|
||||
if (widget.userRole != 'teacher' && (_tabController.index == 1 || _openedFolderStudentId != null)) ...[
|
||||
PopupMenuItem(
|
||||
value: 'multi_select',
|
||||
child: Row(
|
||||
@@ -1937,18 +1937,20 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
itemBuilder: (ctx, i) {
|
||||
final folder = paginatedFolders[i];
|
||||
return GestureDetector(
|
||||
onSecondaryTapDown: (details) =>
|
||||
_showFolderContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
folder,
|
||||
),
|
||||
onLongPressStart: (details) =>
|
||||
_showFolderContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
folder,
|
||||
),
|
||||
onSecondaryTapDown: widget.userRole == 'teacher'
|
||||
? null
|
||||
: (details) => _showFolderContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
folder,
|
||||
),
|
||||
onLongPressStart: widget.userRole == 'teacher'
|
||||
? null
|
||||
: (details) => _showFolderContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
folder,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (folder.studentId != null) {
|
||||
@@ -2086,16 +2088,20 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
itemBuilder: (ctx, i) {
|
||||
final folder = paginatedFolders[i];
|
||||
return GestureDetector(
|
||||
onSecondaryTapDown: (details) => _showFolderContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
folder,
|
||||
),
|
||||
onLongPressStart: (details) => _showFolderContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
folder,
|
||||
),
|
||||
onSecondaryTapDown: widget.userRole == 'teacher'
|
||||
? null
|
||||
: (details) => _showFolderContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
folder,
|
||||
),
|
||||
onLongPressStart: widget.userRole == 'teacher'
|
||||
? null
|
||||
: (details) => _showFolderContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
folder,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (folder.studentId != null) {
|
||||
@@ -2507,16 +2513,20 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
final isSelected = _selectedDocumentIds.contains(doc.id);
|
||||
|
||||
return GestureDetector(
|
||||
onSecondaryTapDown: (details) => _showDocumentContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
doc as DocumentModel,
|
||||
),
|
||||
onLongPressStart: (details) => _showDocumentContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
doc as DocumentModel,
|
||||
),
|
||||
onSecondaryTapDown: widget.userRole == 'teacher'
|
||||
? null
|
||||
: (details) => _showDocumentContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
doc as DocumentModel,
|
||||
),
|
||||
onLongPressStart: widget.userRole == 'teacher'
|
||||
? null
|
||||
: (details) => _showDocumentContextMenu(
|
||||
context,
|
||||
details.globalPosition,
|
||||
doc as DocumentModel,
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (_isMultiSelectMode) {
|
||||
@@ -2628,8 +2638,8 @@ class _DocumentsScreenState extends ConsumerState<DocumentsScreen>
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
|
||||
// Actions menu
|
||||
if (!_isMultiSelectMode)
|
||||
// Actions menu (hidden for teachers)
|
||||
if (!_isMultiSelectMode && widget.userRole != 'teacher')
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: PopupMenuButton<String>(
|
||||
|
||||
@@ -180,13 +180,14 @@ class _FileFolderCardState extends State<FileFolderCard> {
|
||||
// ════════════════════════════════════════
|
||||
Widget _buildGridCard(BuildContext context) {
|
||||
return GestureDetector(
|
||||
// Desktop: right-click opens context menu
|
||||
onSecondaryTapDown: (details) =>
|
||||
_showContextMenu(context, details.globalPosition),
|
||||
// Mobile: long press opens context menu
|
||||
onLongPressStart: _isMobile
|
||||
? (details) => _showContextMenu(context, details.globalPosition)
|
||||
: null,
|
||||
// Desktop: right-click opens context menu (disabled for teachers)
|
||||
onSecondaryTapDown: widget.userRole == 'teacher'
|
||||
? null
|
||||
: (details) => _showContextMenu(context, details.globalPosition),
|
||||
// Mobile: long press opens context menu (disabled for teachers)
|
||||
onLongPressStart: (widget.userRole == 'teacher' || !_isMobile)
|
||||
? null
|
||||
: (details) => _showContextMenu(context, details.globalPosition),
|
||||
child: InkWell(
|
||||
onTap: widget.isMultiSelectMode
|
||||
? () => widget.onSelectedChanged?.call(!widget.isSelected)
|
||||
@@ -261,8 +262,8 @@ class _FileFolderCardState extends State<FileFolderCard> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// ⋮ button only on desktop — on mobile, long press triggers the menu
|
||||
if (!widget.isMultiSelectMode && !_isMobile)
|
||||
// ⋮ button only on desktop and not for teachers — on mobile, long press triggers the menu
|
||||
if (!widget.isMultiSelectMode && !_isMobile && widget.userRole != 'teacher')
|
||||
Positioned(
|
||||
top: 2,
|
||||
right: 2,
|
||||
@@ -288,11 +289,12 @@ class _FileFolderCardState extends State<FileFolderCard> {
|
||||
// ════════════════════════════════════════
|
||||
Widget _buildListRow(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onSecondaryTapDown: (details) =>
|
||||
_showContextMenu(context, details.globalPosition),
|
||||
onLongPressStart: _isMobile
|
||||
? (details) => _showContextMenu(context, details.globalPosition)
|
||||
: null,
|
||||
onSecondaryTapDown: widget.userRole == 'teacher'
|
||||
? null
|
||||
: (details) => _showContextMenu(context, details.globalPosition),
|
||||
onLongPressStart: (widget.userRole == 'teacher' || !_isMobile)
|
||||
? null
|
||||
: (details) => _showContextMenu(context, details.globalPosition),
|
||||
child: InkWell(
|
||||
onTap: widget.isMultiSelectMode
|
||||
? () => widget.onSelectedChanged?.call(!widget.isSelected)
|
||||
@@ -376,10 +378,10 @@ class _FileFolderCardState extends State<FileFolderCard> {
|
||||
// Status
|
||||
Expanded(child: _buildStatusBadge()),
|
||||
|
||||
// ⋮ Actions — desktop only; on mobile use long press
|
||||
// ⋮ Actions — desktop only; on mobile use long press; hidden for teachers
|
||||
SizedBox(
|
||||
width: 40,
|
||||
child: (widget.isMultiSelectMode || _isMobile)
|
||||
child: (widget.isMultiSelectMode || _isMobile || widget.userRole == 'teacher')
|
||||
? const SizedBox.shrink()
|
||||
: PopupMenuButton<String>(
|
||||
icon: Icon(
|
||||
|
||||
Reference in new issue
Block a user