diff --git a/backend/src/routes/documents.js b/backend/src/routes/documents.js index cbd774e..fd38e41 100644 --- a/backend/src/routes/documents.js +++ b/backend/src/routes/documents.js @@ -8,34 +8,34 @@ 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, authorizeRoles('admin'), documentController.getPrintQueue); -router.get('/print-history', authenticateToken, authorizeRoles('admin'), documentController.getPrintHistory); +router.get('/print-queue', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.getPrintQueue); +router.get('/print-history', authenticateToken, authorizeRoles('admin', 'teacher'), 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('/print-queue', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.addToPrintQueue); +router.post('/print-queue/print', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.executePrintQueue); +router.delete('/print-queue/clear', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.clearPrintQueue); +router.delete('/print-queue/:queueId', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.removeFromPrintQueue); -router.post('/upload', authenticateToken, authorizeRoles('admin'), documentController.uploadMiddleware, documentController.uploadDocument); +router.post('/upload', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.uploadMiddleware, documentController.uploadDocument); router.get('/student/:studentId', authenticateToken, documentController.getDocumentsByStudent); // Recycle Bin / Trash routes — must be BEFORE /:id routes router.get('/trash', authenticateToken, documentController.getTrashDocuments); -router.post('/bulk-restore', authenticateToken, authorizeRoles('admin'), documentController.bulkRestore); +router.post('/bulk-restore', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.bulkRestore); router.post('/bulk-permanent-delete', authenticateToken, authorizeRoles('admin'), documentController.bulkPermanentDelete); // 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, authorizeRoles('admin'), documentController.bulkAddToPrintQueue); -router.post('/bulk-copy', authenticateToken, authorizeRoles('admin'), documentController.bulkCopy); +router.post('/bulk-delete', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.bulkDelete); +router.post('/bulk-status', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.bulkStatus); +router.post('/bulk-print', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.bulkAddToPrintQueue); +router.post('/bulk-copy', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.bulkCopy); router.get('/:id/view', authenticateToken, documentController.viewDocument); router.post('/:id/convert-to-pdf', authenticateToken, documentController.convertToPdf); -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.post('/:id/copy', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.copyDocument); +router.patch('/:id/status', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.updateStatus); +router.post('/:id/restore', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.restoreDocument); router.delete('/:id/permanent', authenticateToken, authorizeRoles('admin'), documentController.permanentDeleteDocument); -router.delete('/:id', authenticateToken, authorizeRoles('admin'), documentController.deleteDocument); +router.delete('/:id', authenticateToken, authorizeRoles('admin', 'teacher'), documentController.deleteDocument); module.exports = router; diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 849bdd9..75b300a 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -437,15 +437,12 @@ Files backup/restore - mirror mode or zip --> # DOCUMENT REQUIREMENTS SCREEN -- fix some student folders didnt show in the list - -# TEACHERS & ACADEMIC SETUP SCREEN -- manage section -> make the buttons responsive same size -- add academic year -> make the buttons responsive same size -- add section -> -> make the buttons responsive same size +- teachers cannot upload documents # REPORTS SCREEN -- fix all y scroll bar need to touch the graphs to show + + +- fix all horizontal scroll bars always visible not hide -> need to touch the graphs to show --- diff --git a/frontend/lib/ui/screens/reports/reports_screen.dart b/frontend/lib/ui/screens/reports/reports_screen.dart index 98cb73f..856fc97 100644 --- a/frontend/lib/ui/screens/reports/reports_screen.dart +++ b/frontend/lib/ui/screens/reports/reports_screen.dart @@ -516,18 +516,11 @@ class _ReportsScreenState extends ConsumerState { ref.invalidate(storageStatsProvider); ref.invalidate(transparencyBoardProvider); }, - child: Scrollbar( + child: SingleChildScrollView( controller: _scrollController, - thumbVisibility: true, - trackVisibility: true, - interactive: true, - thickness: 8.0, - radius: const Radius.circular(4), - child: SingleChildScrollView( - controller: _scrollController, - physics: const AlwaysScrollableScrollPhysics(), - padding: const EdgeInsets.all(AppSizes.p24), - child: Column( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(AppSizes.p24), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildTitleAndExportActions(context), @@ -624,9 +617,8 @@ class _ReportsScreenState extends ConsumerState { ), ), ), - ), - ); -} + ); + } Widget _buildViewModeToggle() { final isDark = Theme.of(context).brightness == Brightness.dark; @@ -804,7 +796,7 @@ class _ReportsScreenState extends ConsumerState { ), const SizedBox(height: AppSizes.p8), Text( - 'Document Compliance & Statistics Dashboard • Tiaong Integrated School', + 'Document Compliance & Statistics Dashboard Tiaong Integrated School', style: TextStyle( fontSize: 15, color: isDark ? AppColors.darkTextSecondary : Colors.grey.shade600, @@ -1940,23 +1932,15 @@ class _ReportsScreenState extends ConsumerState { ? _emptyWidget( 'No missing document requirements found. Compliance is 100%!', ) - : Scrollbar( + : ListView.separated( controller: _missingDocsScrollController, - thumbVisibility: true, - trackVisibility: true, - interactive: true, - thickness: 6.0, - radius: const Radius.circular(3), - child: ListView.separated( - controller: _missingDocsScrollController, - shrinkWrap: false, - physics: const AlwaysScrollableScrollPhysics(), - itemCount: breakdown.length, - separatorBuilder: (context, index) => - const SizedBox(height: 14), - itemBuilder: (context, index) => - buildMissingDocRow(breakdown[index]), - ), + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: breakdown.length, + separatorBuilder: (context, index) => + const SizedBox(height: 14), + itemBuilder: (context, index) => + buildMissingDocRow(breakdown[index]), ), ) else @@ -4189,14 +4173,19 @@ class _CustomHorizontalScrollBar extends StatefulWidget { }); @override - State<_CustomHorizontalScrollBar> createState() => _CustomHorizontalScrollBarState(); + State<_CustomHorizontalScrollBar> createState() => + _CustomHorizontalScrollBarState(); } -class _CustomHorizontalScrollBarState extends State<_CustomHorizontalScrollBar> { +class _CustomHorizontalScrollBarState + extends State<_CustomHorizontalScrollBar> { @override void initState() { super.initState(); widget.controller.addListener(_onScroll); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() {}); + }); } @override @@ -4205,6 +4194,9 @@ class _CustomHorizontalScrollBarState extends State<_CustomHorizontalScrollBar> if (oldWidget.controller != widget.controller) { oldWidget.controller.removeListener(_onScroll); widget.controller.addListener(_onScroll); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() {}); + }); } } @@ -4223,76 +4215,106 @@ class _CustomHorizontalScrollBarState extends State<_CustomHorizontalScrollBar> return AnimatedBuilder( animation: widget.controller, builder: (context, _) { - if (!widget.controller.hasClients || - !widget.controller.position.hasContentDimensions || - widget.controller.position.maxScrollExtent <= 0) { - return const SizedBox.shrink(); + final hasClients = widget.controller.hasClients && + widget.controller.position.hasContentDimensions; + + if (!hasClients) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() {}); + }); } - final pos = widget.controller.position; - final maxScroll = pos.maxScrollExtent; - final currentScroll = pos.pixels.clamp(0.0, maxScroll); + final pos = hasClients ? widget.controller.position : null; + final maxScroll = pos?.maxScrollExtent ?? 0.0; + final currentScroll = (pos?.pixels ?? 0.0).clamp( + 0.0, + maxScroll > 0 ? maxScroll : 1.0, + ); final progress = maxScroll > 0 ? currentScroll / maxScroll : 0.0; - final viewportFraction = (pos.viewportDimension / - (pos.maxScrollExtent + pos.viewportDimension)) - .clamp(0.15, 0.85); + final viewportFraction = hasClients && maxScroll > 0 + ? (pos!.viewportDimension / + (pos.maxScrollExtent + pos.viewportDimension)) + .clamp(0.15, 0.85) + : 0.35; final trackColor = widget.isDark - ? AppColors.darkBorder.withValues(alpha: 0.5) - : const Color(0xFFE2E8F0); - final thumbColor = widget.isDark - ? const Color(0xFFE2E8F0) - : const Color(0xFF334155); + ? AppColors.darkBorder.withValues(alpha: 0.7) + : const Color(0xFFCBD5E1); + final thumbColor = AppColors.primaryGreen; return Padding( - padding: const EdgeInsets.only(top: 4, bottom: 2), + padding: const EdgeInsets.symmetric(vertical: 4), child: LayoutBuilder( builder: (context, constraints) { final trackWidth = constraints.maxWidth; final thumbWidth = (trackWidth * viewportFraction).clamp(36.0, trackWidth); - final maxThumbOffset = trackWidth - thumbWidth; + final maxThumbOffset = + (trackWidth - thumbWidth).clamp(0.0, trackWidth); final thumbOffset = maxThumbOffset * progress; - return GestureDetector( - behavior: HitTestBehavior.opaque, - onHorizontalDragUpdate: (details) { - if (maxThumbOffset <= 0) return; - final deltaFraction = details.primaryDelta! / maxThumbOffset; - final newScroll = (widget.controller.offset + - deltaFraction * maxScroll) - .clamp(0.0, maxScroll); - widget.controller.jumpTo(newScroll); - }, - child: Container( - height: 10, - width: double.infinity, - alignment: Alignment.centerLeft, - child: Stack( + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onHorizontalDragUpdate: (details) { + if (!hasClients || maxThumbOffset <= 0 || maxScroll <= 0) return; + final deltaFraction = + details.primaryDelta! / maxThumbOffset; + final newScroll = (widget.controller.offset + + deltaFraction * maxScroll) + .clamp(0.0, maxScroll); + widget.controller.jumpTo(newScroll); + }, + onTapDown: (details) { + if (!hasClients || maxThumbOffset <= 0 || maxScroll <= 0) return; + final localX = details.localPosition.dx; + final targetProgress = (localX / trackWidth).clamp(0.0, 1.0); + final newScroll = + (targetProgress * maxScroll).clamp(0.0, maxScroll); + widget.controller.animateTo( + newScroll, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + }, + child: Container( + height: 16, + width: double.infinity, alignment: Alignment.centerLeft, - children: [ - // Thin Track Line - Container( - height: 3, - width: trackWidth, - decoration: BoxDecoration( - color: trackColor, - borderRadius: BorderRadius.circular(1.5), - ), - ), - // Thin Thumb Line - Positioned( - left: thumbOffset, - child: Container( - height: 3, - width: thumbWidth, + child: Stack( + alignment: Alignment.centerLeft, + children: [ + // Track + Container( + height: 4.5, + width: trackWidth, decoration: BoxDecoration( - color: thumbColor, - borderRadius: BorderRadius.circular(1.5), + color: trackColor, + borderRadius: BorderRadius.circular(2.25), ), ), - ), - ], + // Thumb + Positioned( + left: thumbOffset, + child: Container( + height: 4.5, + width: thumbWidth, + decoration: BoxDecoration( + color: thumbColor, + borderRadius: BorderRadius.circular(2.25), + boxShadow: [ + BoxShadow( + color: thumbColor.withValues(alpha: 0.3), + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + ), + ), + ], + ), ), ), ); diff --git a/frontend/lib/ui/screens/reports/widgets/transparency_board_section.dart b/frontend/lib/ui/screens/reports/widgets/transparency_board_section.dart index 8a7e7fc..17a67e1 100644 --- a/frontend/lib/ui/screens/reports/widgets/transparency_board_section.dart +++ b/frontend/lib/ui/screens/reports/widgets/transparency_board_section.dart @@ -1389,14 +1389,19 @@ class _CustomHorizontalScrollBar extends StatefulWidget { }); @override - State<_CustomHorizontalScrollBar> createState() => _CustomHorizontalScrollBarState(); + State<_CustomHorizontalScrollBar> createState() => + _CustomHorizontalScrollBarState(); } -class _CustomHorizontalScrollBarState extends State<_CustomHorizontalScrollBar> { +class _CustomHorizontalScrollBarState + extends State<_CustomHorizontalScrollBar> { @override void initState() { super.initState(); widget.controller.addListener(_onScroll); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() {}); + }); } @override @@ -1405,6 +1410,9 @@ class _CustomHorizontalScrollBarState extends State<_CustomHorizontalScrollBar> if (oldWidget.controller != widget.controller) { oldWidget.controller.removeListener(_onScroll); widget.controller.addListener(_onScroll); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() {}); + }); } } @@ -1423,76 +1431,106 @@ class _CustomHorizontalScrollBarState extends State<_CustomHorizontalScrollBar> return AnimatedBuilder( animation: widget.controller, builder: (context, _) { - if (!widget.controller.hasClients || - !widget.controller.position.hasContentDimensions || - widget.controller.position.maxScrollExtent <= 0) { - return const SizedBox.shrink(); + final hasClients = widget.controller.hasClients && + widget.controller.position.hasContentDimensions; + + if (!hasClients) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) setState(() {}); + }); } - final pos = widget.controller.position; - final maxScroll = pos.maxScrollExtent; - final currentScroll = pos.pixels.clamp(0.0, maxScroll); + final pos = hasClients ? widget.controller.position : null; + final maxScroll = pos?.maxScrollExtent ?? 0.0; + final currentScroll = (pos?.pixels ?? 0.0).clamp( + 0.0, + maxScroll > 0 ? maxScroll : 1.0, + ); final progress = maxScroll > 0 ? currentScroll / maxScroll : 0.0; - final viewportFraction = (pos.viewportDimension / - (pos.maxScrollExtent + pos.viewportDimension)) - .clamp(0.15, 0.85); + final viewportFraction = hasClients && maxScroll > 0 + ? (pos!.viewportDimension / + (pos.maxScrollExtent + pos.viewportDimension)) + .clamp(0.15, 0.85) + : 0.35; final trackColor = widget.isDark - ? AppColors.darkBorder.withValues(alpha: 0.5) - : const Color(0xFFE2E8F0); - final thumbColor = widget.isDark - ? const Color(0xFFE2E8F0) - : const Color(0xFF334155); + ? AppColors.darkBorder.withValues(alpha: 0.7) + : const Color(0xFFCBD5E1); + final thumbColor = AppColors.primaryGreen; return Padding( - padding: const EdgeInsets.only(top: 4, bottom: 2), + padding: const EdgeInsets.symmetric(vertical: 4), child: LayoutBuilder( builder: (context, constraints) { final trackWidth = constraints.maxWidth; final thumbWidth = (trackWidth * viewportFraction).clamp(36.0, trackWidth); - final maxThumbOffset = trackWidth - thumbWidth; + final maxThumbOffset = + (trackWidth - thumbWidth).clamp(0.0, trackWidth); final thumbOffset = maxThumbOffset * progress; - return GestureDetector( - behavior: HitTestBehavior.opaque, - onHorizontalDragUpdate: (details) { - if (maxThumbOffset <= 0) return; - final deltaFraction = details.primaryDelta! / maxThumbOffset; - final newScroll = (widget.controller.offset + - deltaFraction * maxScroll) - .clamp(0.0, maxScroll); - widget.controller.jumpTo(newScroll); - }, - child: Container( - height: 10, - width: double.infinity, - alignment: Alignment.centerLeft, - child: Stack( + return MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onHorizontalDragUpdate: (details) { + if (!hasClients || maxThumbOffset <= 0 || maxScroll <= 0) return; + final deltaFraction = + details.primaryDelta! / maxThumbOffset; + final newScroll = (widget.controller.offset + + deltaFraction * maxScroll) + .clamp(0.0, maxScroll); + widget.controller.jumpTo(newScroll); + }, + onTapDown: (details) { + if (!hasClients || maxThumbOffset <= 0 || maxScroll <= 0) return; + final localX = details.localPosition.dx; + final targetProgress = (localX / trackWidth).clamp(0.0, 1.0); + final newScroll = + (targetProgress * maxScroll).clamp(0.0, maxScroll); + widget.controller.animateTo( + newScroll, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + }, + child: Container( + height: 16, + width: double.infinity, alignment: Alignment.centerLeft, - children: [ - // Thin Track Line - Container( - height: 3, - width: trackWidth, - decoration: BoxDecoration( - color: trackColor, - borderRadius: BorderRadius.circular(1.5), - ), - ), - // Thin Thumb Line - Positioned( - left: thumbOffset, - child: Container( - height: 3, - width: thumbWidth, + child: Stack( + alignment: Alignment.centerLeft, + children: [ + // Track + Container( + height: 4.5, + width: trackWidth, decoration: BoxDecoration( - color: thumbColor, - borderRadius: BorderRadius.circular(1.5), + color: trackColor, + borderRadius: BorderRadius.circular(2.25), ), ), - ), - ], + // Thumb + Positioned( + left: thumbOffset, + child: Container( + height: 4.5, + width: thumbWidth, + decoration: BoxDecoration( + color: thumbColor, + borderRadius: BorderRadius.circular(2.25), + boxShadow: [ + BoxShadow( + color: thumbColor.withValues(alpha: 0.3), + blurRadius: 3, + offset: const Offset(0, 1), + ), + ], + ), + ), + ), + ], + ), ), ), );