feat: scope dashboard KPIs to teacher students and restrict teacher actions on documents screen

This commit is contained in:
ssbg04 committed 2026-08-16 02:05:01 +08:00
1 parent b57bcffa41
commit 57e0c201f8
5 files changed
+460 -189

No files matched your search

+366 -112
View File
@@ -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)) {
+11 -11
View File
@@ -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
View File
@@ -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(