diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 4a69c4b..fdeca46 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -604,7 +604,7 @@ Files backup/restore - mirror mode or zip --> - print list modal - print list history, print email to the student: email (optional), add available date to pickup, message to email can be manual message typed by registrar/staff or preset message (optional) --> -# DASHBORD SCREEN + - F:\SumbrerongBato\tis_rms_server\screenshots\screenshot1.png -> align tabs section to the list layout, decrease space bottom list between the pagination buttons, in android app - remove the multi select icon, in android app - in android app, screen header move the print list icon, upload icon -- in file preview screen add icon button of all dropdown menu from the more menu +- in file preview screen add icon button of all dropdown menu from the more menu --> +# STUDENT SCREEN +- in android app, screen header icon add icon button for "add student" make it more attractive + +# DOCUMENT SCREEN +- file preview screen -> in android app make it more menu icon only delete and copy, make it responsive in android app base on screen size for default icons are 4 exclude the copy and delete, always inside are copy and delete in more icon if screen size small put all in more icon +- in filter documents only add document type, make it dropdown not modal + +# ARCHIVE SCREEN +- do the same from document screen design from this chat today + --- diff --git a/frontend/lib/ui/layouts/android_bottom_nav_layout.dart b/frontend/lib/ui/layouts/android_bottom_nav_layout.dart index e2ccf62..e563247 100644 --- a/frontend/lib/ui/layouts/android_bottom_nav_layout.dart +++ b/frontend/lib/ui/layouts/android_bottom_nav_layout.dart @@ -33,6 +33,7 @@ import '../shared/widgets/notification_icon_button.dart'; import '../screens/students/widgets/student_bulk_actions.dart'; import '../screens/documents/widgets/print_queue_modal.dart'; import '../screens/documents/widgets/upload_ocr_modal.dart'; +import '../screens/students/widgets/add_student_modal.dart'; // Dummy screen for placeholders class PlaceholderScreen extends StatelessWidget { @@ -183,6 +184,53 @@ class _AndroidBottomNavLayoutState extends ConsumerState ); } + void _showArchiveSearchDialog( + BuildContext context, + WidgetRef ref, + ) { + final currentSearch = ref.read(archiveDocumentQueryProvider).search; + final searchController = TextEditingController(text: currentSearch); + final searchFocusNode = FocusNode(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + searchFocusNode.requestFocus(); + }); + + showDialog( + context: context, + barrierColor: Colors.black54, + builder: (ctx) { + final isDark = Theme.of(ctx).brightness == Brightness.dark; + return Align( + alignment: Alignment.topCenter, + child: Padding( + padding: const EdgeInsets.fromLTRB(16, kToolbarHeight + 24, 16, 0), + child: Material( + color: isDark ? AppColors.darkSurfaceCard : Colors.white, + elevation: 4, + borderRadius: BorderRadius.circular(12), + child: AppSearchBar( + controller: searchController, + focusNode: searchFocusNode, + collapsible: false, + hint: 'Search archives by name, LRN, file…', + maxWidth: 600, + onSubmitted: (value) { + Navigator.of(ctx).pop(); + ref + .read(archiveDocumentQueryProvider.notifier) + .setSearch(value); + ref.invalidate(archiveStudentFoldersProvider); + ref.invalidate(archiveDocumentPageProvider); + }, + ), + ), + ), + ); + }, + ); + } + void _showUserSearchDialog( BuildContext context, WidgetRef ref, @@ -680,6 +728,48 @@ class _AndroidBottomNavLayoutState extends ConsumerState ); }, ), + // 3. Add Student Icon (High-Attraction Elevated Primary CTA) + if (widget.userRole != 'teacher') + Tooltip( + message: 'Add Student', + child: Container( + width: 36, + height: 36, + margin: const EdgeInsets.only(left: 4, right: 8), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [ + Color(0xFF00B074), + AppColors.primaryGreen, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(10), + boxShadow: [ + BoxShadow( + color: AppColors.primaryGreen.withValues(alpha: 0.4), + blurRadius: 6, + offset: const Offset(0, 2), + ), + ], + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(10), + onTap: () => AddStudentModal.show(context), + child: const Center( + child: Icon( + Icons.person_add_rounded, + size: 20, + color: Colors.white, + ), + ), + ), + ), + ), + ), const SizedBox(width: 4), ], if (activeTab == 'Documents') ...[ @@ -770,6 +860,44 @@ class _AndroidBottomNavLayoutState extends ConsumerState ), const SizedBox(width: 4), ], + if (activeTab == 'Archives') ...[ + // 1. Search Icon + Consumer( + builder: (context, ref, _) { + return Tooltip( + message: 'Search Archives', + child: IconButton( + icon: const Icon(Icons.search, size: 22), + onPressed: () => + _showArchiveSearchDialog(context, ref), + ), + ); + }, + ), + // 2. Print List Icon (for non-teachers) + if (widget.userRole != 'teacher') + Consumer( + builder: (context, ref, _) { + final queueAsync = ref.watch(printQueueProvider); + final count = queueAsync.maybeWhen( + data: (items) => items.length, + orElse: () => 0, + ); + return Tooltip( + message: 'Print List', + child: IconButton( + icon: Badge( + isLabelVisible: count > 0, + label: Text(count.toString()), + child: const Icon(Icons.print_rounded, size: 22), + ), + onPressed: () => PrintQueueModal.show(context), + ), + ); + }, + ), + const SizedBox(width: 4), + ], if (activeTab == 'Users') ...[ // 1. Search Icon Consumer( diff --git a/frontend/lib/ui/screens/archives/archives_screen.dart b/frontend/lib/ui/screens/archives/archives_screen.dart index 7d1626d..2ddca04 100644 --- a/frontend/lib/ui/screens/archives/archives_screen.dart +++ b/frontend/lib/ui/screens/archives/archives_screen.dart @@ -12,10 +12,8 @@ import '../../../domain/repositories/document_repository.dart' import '../../shared/inputs/app_search_bar.dart'; import '../../shared/widgets/app_pagination.dart'; import '../../shared/widgets/app_error_state.dart'; -import '../../shared/widgets/horizontal_expandable_fab.dart'; import '../../providers/archives_provider.dart'; import '../../providers/document_provider.dart'; -import '../../providers/student_provider.dart'; import '../../shared/dialogs/success_dialog.dart'; import '../../shared/dialogs/error_dialog.dart'; import '../../providers/auth_provider.dart'; @@ -66,16 +64,9 @@ class _ArchivesScreenState extends ConsumerState String _selectedGradeLevel = 'All Grades'; String _selectedSchoolYear = 'All Years'; - String _pendingStatus = 'All Statuses'; - String _pendingDocumentType = 'All Types'; - String _pendingGradeLevel = 'All Grades'; - String _pendingSchoolYear = 'All Years'; - int _getActiveFilterCount() { int count = 0; if (_selectedDocumentType != 'All Types') count++; - if (_selectedGradeLevel != 'All Grades') count++; - if (_selectedSchoolYear != 'All Years') count++; return count; } @@ -89,6 +80,7 @@ class _ArchivesScreenState extends ConsumerState ProviderSubscription? _tabListener; ProviderSubscription? _folderListener; + ProviderSubscription? _searchListener; bool get _isAdmin => widget.userRole == 'admin'; @@ -170,6 +162,16 @@ class _ArchivesScreenState extends ConsumerState _shortcutFocusNode.requestFocus(); } + _searchListener = ref.listenManual( + archiveDocumentQueryProvider.select((q) => q.search), + (previous, current) { + if (!mounted) return; + if (_searchController.text != current) { + _searchController.text = current; + } + }, + ); + _tabListener = ref.listenManual(activeTabProvider, ( previous, next, @@ -201,6 +203,7 @@ class _ArchivesScreenState extends ConsumerState @override void dispose() { + _searchListener?.close(); _folderListener?.close(); _tabListener?.close(); _pollingTimer?.cancel(); @@ -802,37 +805,7 @@ class _ArchivesScreenState extends ConsumerState child: Scaffold( resizeToAvoidBottomInset: false, backgroundColor: Colors.transparent, - floatingActionButtonLocation: FloatingActionButtonLocation.endFloat, - floatingActionButton: (isMobile && - widget.userRole != 'teacher' && - !_isMultiSelectMode && - !_searchFocusNode.hasFocus) - ? HorizontalExpandableFab( - heroTag: 'fab_archives_menu', - items: [ - // Print List Action - FabActionItem( - icon: Icons.print_rounded, - tooltip: 'Print List', - badgeCount: ref.watch(printQueueProvider).value?.length ?? 0, - heroTag: 'fab_archives_print_list', - onPressed: () => PrintQueueModal.show(context), - ), - // Multi-Select Action (available on All Archived Docs tab or in opened folder) - if (_tabController.index == 1 || isFolderOpened) - FabActionItem( - icon: Icons.checklist_rounded, - tooltip: 'Select Multiple', - heroTag: 'fab_archives_multi_select', - onPressed: () { - setState(() { - _isMultiSelectMode = true; - }); - }, - ), - ], - ) - : null, + floatingActionButton: null, body: SafeArea( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1060,7 +1033,7 @@ class _ArchivesScreenState extends ConsumerState child: Container( height: 52, margin: EdgeInsets.symmetric( - horizontal: isMobile ? 12 : 20, + horizontal: isMobile ? 12 : 16, vertical: 8, ), padding: EdgeInsets.symmetric(horizontal: isMobile ? 8 : 12), @@ -1121,8 +1094,8 @@ class _ArchivesScreenState extends ConsumerState fontSize: 14, fontWeight: FontWeight.w500, color: isDark - ? AppColors.darkTextSecondary - : AppColors.textSecondary, + ? AppColors.darkTextSecondary + : AppColors.textSecondary, ), ), ], @@ -1148,8 +1121,8 @@ class _ArchivesScreenState extends ConsumerState const Spacer(), ], - // Action buttons - if (!isFolderOpened) ...[ + // Action buttons (Hide search on mobile as it is moved to Android AppBar) + if (!isMobile && !isFolderOpened) ...[ Tooltip( richMessage: (_searchController.text.isNotEmpty || query.search.isNotEmpty) @@ -1233,29 +1206,135 @@ class _ArchivesScreenState extends ConsumerState const SizedBox(width: 2), ], - // Filter button (Icon only, between Search and Print) + // Filter button (Dropdown only for Document Type, between Search and Print) if (_tabController.index == 1 || isFolderOpened) ...[ - Tooltip( - message: 'Filter Documents', - child: IconButton( - padding: const EdgeInsets.all(8), - constraints: - const BoxConstraints(minWidth: 36, minHeight: 36), - onPressed: _openFilterDialog, - icon: Badge( - isLabelVisible: _getActiveFilterCount() > 0, - label: Text(_getActiveFilterCount().toString()), - child: Icon( - Icons.tune_rounded, - size: 20, - color: _getActiveFilterCount() > 0 - ? AppColors.primaryGreen - : (isDark - ? AppColors.darkTextPrimary - : Colors.black87), - ), + PopupMenuButton( + tooltip: 'Filter by Document Type', + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minWidth: 36, minHeight: 36), + icon: Badge( + isLabelVisible: _getActiveFilterCount() > 0, + label: Text(_getActiveFilterCount().toString()), + child: Icon( + Icons.tune_rounded, + size: 20, + color: _getActiveFilterCount() > 0 + ? AppColors.primaryGreen + : (isDark + ? AppColors.darkTextPrimary + : Colors.black87), ), ), + onSelected: (type) { + setState(() { + _selectedDocumentType = type; + }); + _applyFilters(); + }, + itemBuilder: (context) { + final requirementsAsync = ref.read(documentRequirementsProvider); + final jhsReqs = requirementsAsync.maybeWhen( + data: (reqs) => reqs + .where((r) => r.category == 'JHS') + .map((r) => r.name) + .toSet() + .toList() + ..sort(), + orElse: () => [], + ); + final shsReqs = requirementsAsync.maybeWhen( + data: (reqs) => reqs + .where((r) => r.category == 'SHS') + .map((r) => r.name) + .toSet() + .toList() + ..sort(), + orElse: () => [], + ); + + // Update cached lists for _applyFilters + if (jhsReqs.isNotEmpty) _jhsItems = jhsReqs; + if (shsReqs.isNotEmpty) _shsItems = shsReqs; + + final List> items = []; + + Widget buildHeader(String title) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Text( + title, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white54 : Colors.black54, + letterSpacing: 0.5, + ), + ), + ); + } + + PopupMenuItem buildItem(String value, String label, {bool isSubItem = false}) { + final isSelected = _selectedDocumentType == value; + return PopupMenuItem( + value: value, + height: 38, + child: Row( + children: [ + if (isSubItem) const SizedBox(width: 8), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected + ? AppColors.primaryGreen + : (isDark ? Colors.white : Colors.black87), + ), + overflow: TextOverflow.ellipsis, + ), + ), + if (isSelected) + const Icon( + Icons.check_rounded, + size: 16, + color: AppColors.primaryGreen, + ), + ], + ), + ); + } + + items.add(buildItem('All Types', 'All Types')); + items.add(const PopupMenuDivider(height: 8)); + + if (jhsReqs.isNotEmpty) { + items.add(PopupMenuItem( + enabled: false, + height: 28, + child: buildHeader('JUNIOR HIGH SCHOOL'), + )); + items.add(buildItem('All JHS', 'All JHS', isSubItem: true)); + for (final doc in jhsReqs) { + items.add(buildItem(doc, doc, isSubItem: true)); + } + } + + if (shsReqs.isNotEmpty) { + items.add(const PopupMenuDivider(height: 8)); + items.add(PopupMenuItem( + enabled: false, + height: 28, + child: buildHeader('SENIOR HIGH SCHOOL'), + )); + items.add(buildItem('All SHS', 'All SHS', isSubItem: true)); + for (final doc in shsReqs) { + items.add(buildItem(doc, doc, isSubItem: true)); + } + } + + return items; + }, ), const SizedBox(width: 2), ], @@ -1305,490 +1384,6 @@ class _ArchivesScreenState extends ConsumerState ); } - // ════════════════════════════════════════════════════════════════ - // FILTER PANEL - // ════════════════════════════════════════════════════════════════ - void _openFilterDialog() { - setState(() { - _pendingStatus = _selectedStatus; - _pendingDocumentType = _selectedDocumentType; - _pendingGradeLevel = _selectedGradeLevel; - _pendingSchoolYear = _selectedSchoolYear; - }); - - showDialog( - context: context, - barrierColor: Colors.black.withValues(alpha: 0.35), - builder: (ctx) => StatefulBuilder( - builder: (ctx, setDialogState) { - final isSmallScreen = MediaQuery.of(context).size.height < 600; - return Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.symmetric( - horizontal: 20, - vertical: isSmallScreen ? 16 : 36, - ), - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 440, - maxHeight: MediaQuery.of(context).size.height * 0.85, - ), - child: _buildFilterPanelContent( - setDialogState, - () => Navigator.of(ctx).pop(), - ), - ), - ); - }, - ), - ); - } - - Widget _buildFilterPanelContent( - StateSetter setDialogState, - VoidCallback onApply, - ) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final academicYearsAsync = ref.watch(academicYearsProvider); - final requirementsAsync = ref.watch(documentRequirementsProvider); - - final jhsItems = requirementsAsync.when( - data: (reqs) => - reqs - .where((r) => r.category == 'JHS') - .map((r) => r.name) - .toSet() - .toList() - ..sort(), - loading: () => [], - error: (_, _) => [], - ); - - final shsItems = requirementsAsync.when( - data: (reqs) => - reqs - .where((r) => r.category == 'SHS') - .map((r) => r.name) - .toSet() - .toList() - ..sort(), - loading: () => [], - error: (_, _) => [], - ); - - if (jhsItems.isNotEmpty) _jhsItems = jhsItems; - if (shsItems.isNotEmpty) _shsItems = shsItems; - - final docTypes = [ - 'All Types', - 'All JHS', - 'All SHS', - ...jhsItems, - ...shsItems, - ]; - - final years = academicYearsAsync.when( - data: (y) => ['All Years', ...y.map((ay) => ay.yearRange)], - loading: () => const ['All Years'], - error: (_, _) => const ['All Years'], - ); - - - return Container( - decoration: BoxDecoration( - color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isDark ? AppColors.darkBorder : Colors.grey.shade200, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.12), - blurRadius: 24, - offset: const Offset(0, 8), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Header (Fixed) - Padding( - padding: const EdgeInsets.fromLTRB(20, 18, 16, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: AppColors.primaryGreen.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.tune_rounded, - size: 20, - color: AppColors.primaryGreen, - ), - ), - const SizedBox(width: 10), - Text( - 'Filter Archives', - style: TextStyle( - fontSize: 17, - fontWeight: FontWeight.w700, - color: isDark - ? AppColors.darkTextPrimary - : AppColors.textPrimary, - ), - ), - ], - ), - IconButton( - icon: Icon( - Icons.close_rounded, - size: 20, - color: isDark - ? AppColors.darkTextSecondary - : AppColors.textSecondary, - ), - tooltip: 'Close', - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ), - - Divider( - height: 20, - thickness: 1, - color: isDark ? AppColors.darkBorder : Colors.grey.shade200, - ), - - // Scrollable middle section for filter items - Flexible( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - - // Document Type - _buildFilterSection( - label: 'Document Type', - hasActiveFilter: _pendingDocumentType != 'All Types', - onReset: () => - setDialogState(() => _pendingDocumentType = 'All Types'), - child: _buildFilterDropdown( - value: docTypes.contains(_pendingDocumentType) - ? _pendingDocumentType - : 'All Types', - items: docTypes, - onChanged: (v) => - setDialogState(() => _pendingDocumentType = v!), - ), - ), - - _buildDivider(isDark), - - // Grade Level - _buildFilterSection( - label: 'Grade Level', - hasActiveFilter: _pendingGradeLevel != 'All Grades', - onReset: () => - setDialogState(() => _pendingGradeLevel = 'All Grades'), - child: _buildFilterDropdown( - value: const [ - 'All Grades', - '7', - '8', - '9', - '10', - '11', - '12', - ].contains(_pendingGradeLevel) - ? _pendingGradeLevel - : 'All Grades', - items: const ['All Grades', '7', '8', '9', '10', '11', '12'], - labelBuilder: (g) => - (g == 'All Grades' || g.toLowerCase().startsWith('grade')) - ? g - : 'Grade $g', - onChanged: (v) => - setDialogState(() => _pendingGradeLevel = v!), - ), - ), - - _buildDivider(isDark), - - // School Year - _buildFilterSection( - label: 'School Year', - hasActiveFilter: _pendingSchoolYear != 'All Years', - onReset: () => - setDialogState(() => _pendingSchoolYear = 'All Years'), - child: _buildFilterDropdown( - value: years.contains(_pendingSchoolYear) - ? _pendingSchoolYear - : 'All Years', - items: years, - onChanged: (v) => - setDialogState(() => _pendingSchoolYear = v!), - ), - ), - ], - ), - ), - ), - - Divider( - height: 1, - thickness: 1, - color: isDark ? AppColors.darkBorder : Colors.grey.shade200, - ), - - // Footer buttons (Fixed at bottom) - Padding( - padding: const EdgeInsets.fromLTRB(20, 14, 20, 16), - child: Row( - children: [ - // Reset all - Expanded( - child: OutlinedButton( - style: OutlinedButton.styleFrom( - foregroundColor: isDark - ? AppColors.darkTextSecondary - : AppColors.textSecondary, - side: BorderSide( - color: isDark - ? AppColors.darkBorder - : Colors.grey.shade300, - ), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - onPressed: () { - setDialogState(() { - _pendingStatus = 'All Statuses'; - _pendingDocumentType = 'All Types'; - _pendingGradeLevel = 'All Grades'; - _pendingSchoolYear = 'All Years'; - }); - setState(() { - _selectedStatus = 'All Statuses'; - _selectedDocumentType = 'All Types'; - _selectedGradeLevel = 'All Grades'; - _selectedSchoolYear = 'All Years'; - }); - _applyFilters(); - }, - child: const Text( - 'Reset all', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 13.5, - ), - ), - ), - ), - const SizedBox(width: 12), - // Apply - Expanded( - flex: 2, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - elevation: 0, - ), - onPressed: () { - setState(() { - _selectedStatus = _pendingStatus; - _selectedDocumentType = _pendingDocumentType; - _selectedGradeLevel = _pendingGradeLevel; - _selectedSchoolYear = _pendingSchoolYear; - }); - _applyFilters(); - onApply(); - }, - child: const Text( - 'Apply Filters', - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 14, - ), - ), - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildDivider(bool isDark) { - return Divider( - height: 1, - indent: 20, - endIndent: 20, - color: isDark - ? AppColors.darkBorder.withValues(alpha: 0.5) - : Colors.grey.shade100, - ); - } - - - Widget _buildFilterSection({ - required String label, - required VoidCallback onReset, - required Widget child, - bool hasActiveFilter = false, - }) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: isDark - ? AppColors.darkTextPrimary - : AppColors.textPrimary, - ), - ), - if (hasActiveFilter) - GestureDetector( - onTap: onReset, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 2, - ), - child: const Text( - 'Reset', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: AppColors.primaryGreen, - ), - ), - ), - ), - ], - ), - const SizedBox(height: 8), - child, - ], - ), - ); - } - - Widget _buildFilterDropdown({ - required String value, - required List items, - String Function(String)? labelBuilder, - ValueChanged? onChanged, - String? hint, - bool enabled = true, - }) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final safeValue = items.contains(value) ? value : items.firstOrNull; - - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2), - decoration: BoxDecoration( - color: enabled - ? (isDark ? AppColors.darkSurface2 : AppColors.surfaceWhite) - : (isDark ? AppColors.darkSurfaceCard : Colors.grey.shade100), - border: Border.all( - color: enabled - ? (isDark ? AppColors.darkBorder : Colors.grey.shade300) - : (isDark - ? AppColors.darkBorder.withValues(alpha: 0.5) - : Colors.grey.shade200), - ), - borderRadius: BorderRadius.circular(10), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: safeValue, - isExpanded: true, - menuMaxHeight: 260, - borderRadius: BorderRadius.circular(12), - dropdownColor: isDark ? AppColors.darkSurfaceCard : Colors.white, - elevation: 4, - icon: Icon( - Icons.keyboard_arrow_down_rounded, - color: enabled - ? (isDark - ? AppColors.darkTextSecondary - : AppColors.textSecondary) - : (isDark ? AppColors.darkTextMuted : AppColors.textMuted), - size: 22, - ), - hint: hint != null - ? Text( - hint, - style: TextStyle( - fontSize: 13.5, - color: isDark ? AppColors.darkTextMuted : AppColors.textMuted, - ), - ) - : null, - style: TextStyle( - fontSize: 13.5, - fontWeight: FontWeight.w500, - color: enabled - ? (isDark ? AppColors.darkTextPrimary : AppColors.textPrimary) - : (isDark ? AppColors.darkTextMuted : AppColors.textMuted), - ), - items: items.map((item) { - final displayLabel = - labelBuilder != null ? labelBuilder(item) : item; - final isItemActive = item == safeValue; - return DropdownMenuItem( - value: item, - child: Text( - displayLabel, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.5, - fontWeight: - isItemActive ? FontWeight.w600 : FontWeight.w400, - color: isItemActive - ? AppColors.primaryGreen - : (isDark - ? AppColors.darkTextPrimary - : AppColors.textPrimary), - ), - ), - ); - }).toList(), - onChanged: enabled ? onChanged : null, - ), - ), - ); - } // ════════════════════════════════════════════════════════════════ // MORE OPTIONS DROPDOWN (matches Documents screen style) @@ -1984,10 +1579,10 @@ class _ArchivesScreenState extends ConsumerState Widget containerList = Container( margin: EdgeInsets.only( - left: isMobile ? 8 : 16, - right: isMobile ? 8 : 16, + left: isMobile ? 12 : 16, + right: isMobile ? 12 : 16, top: isMobile ? 12 : 16, - bottom: isMobile ? 16 : 16, + bottom: isMobile ? 6 : 16, ), decoration: BoxDecoration( color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, @@ -2139,7 +1734,7 @@ class _ArchivesScreenState extends ConsumerState left: isMobile ? 10 : 16, right: isMobile ? 10 : 16, top: isMobile ? 14 : 16, - bottom: isMobile ? 24 : 16, + bottom: isMobile ? 6 : 16, ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: cols, @@ -2442,10 +2037,10 @@ class _ArchivesScreenState extends ConsumerState Expanded( child: Container( margin: EdgeInsets.only( - left: isMobile ? 8 : 16, - right: isMobile ? 8 : 16, + left: isMobile ? 12 : 16, + right: isMobile ? 12 : 16, top: isMobile ? 12 : 16, - bottom: isMobile ? 16 : 16, + bottom: isMobile ? 6 : 16, ), decoration: BoxDecoration( color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, @@ -2567,7 +2162,7 @@ class _ArchivesScreenState extends ConsumerState left: isMobileGrid ? 10 : 16, right: isMobileGrid ? 10 : 16, top: isMobileGrid ? 12 : 16, - bottom: isMobileGrid ? 24 : 16, + bottom: isMobileGrid ? 6 : 16, ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: cols, diff --git a/frontend/lib/ui/screens/documents/documents_screen.dart b/frontend/lib/ui/screens/documents/documents_screen.dart index 426e48b..5cb38eb 100644 --- a/frontend/lib/ui/screens/documents/documents_screen.dart +++ b/frontend/lib/ui/screens/documents/documents_screen.dart @@ -68,11 +68,6 @@ class _DocumentsScreenState extends ConsumerState String _selectedGradeLevel = 'All Grades'; String _selectedSchoolYear = 'All Years'; - String _pendingStatus = 'All Statuses'; - String _pendingDocumentType = 'All Types'; - String _pendingGradeLevel = 'All Grades'; - String _pendingSchoolYear = 'All Years'; - // Cached doc type lists for filter expansion List _jhsItems = []; List _shsItems = []; @@ -1220,30 +1215,132 @@ class _DocumentsScreenState extends ConsumerState const SizedBox(width: 2), ], - // Filter button (Icon only, between Search and Upload/Bulk Add) + // Filter button (Dropdown only for Document Type, between Search and Upload/Bulk Add) if (_tabController.index == 1 || isFolderOpened) ...[ - Tooltip( - message: 'Filter Documents', - child: IconButton( - padding: const EdgeInsets.all(8), - constraints: const BoxConstraints(minWidth: 36, minHeight: 36), - onPressed: () => _openFilterDialog( - requirementsAsync, - academicYearsAsync, - statusesAsync, - ), - icon: Badge( - isLabelVisible: _getActiveFilterCount() > 0, - label: Text(_getActiveFilterCount().toString()), - child: Icon( - Icons.tune_rounded, - size: 20, - color: _getActiveFilterCount() > 0 - ? AppColors.primaryGreen - : (isDark ? AppColors.darkTextPrimary : Colors.black87), - ), + PopupMenuButton( + tooltip: 'Filter by Document Type', + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minWidth: 36, minHeight: 36), + icon: Badge( + isLabelVisible: _getActiveFilterCount() > 0, + label: Text(_getActiveFilterCount().toString()), + child: Icon( + Icons.tune_rounded, + size: 20, + color: _getActiveFilterCount() > 0 + ? AppColors.primaryGreen + : (isDark ? AppColors.darkTextPrimary : Colors.black87), ), ), + onSelected: (type) { + setState(() { + _selectedDocumentType = type; + }); + _applyFilters(); + }, + itemBuilder: (context) { + final jhsReqs = requirementsAsync.maybeWhen( + data: (reqs) => reqs + .where((r) => r.category == 'JHS') + .map((r) => r.name as String) + .toSet() + .toList() + ..sort(), + orElse: () => [], + ); + final shsReqs = requirementsAsync.maybeWhen( + data: (reqs) => reqs + .where((r) => r.category == 'SHS') + .map((r) => r.name as String) + .toSet() + .toList() + ..sort(), + orElse: () => [], + ); + + // Update cached lists for _applyFilters + if (jhsReqs.isNotEmpty) _jhsItems = jhsReqs; + if (shsReqs.isNotEmpty) _shsItems = shsReqs; + + final List> items = []; + + Widget buildHeader(String title) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + child: Text( + title, + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white54 : Colors.black54, + letterSpacing: 0.5, + ), + ), + ); + } + + PopupMenuItem buildItem(String value, String label, {bool isSubItem = false}) { + final isSelected = _selectedDocumentType == value; + return PopupMenuItem( + value: value, + height: 38, + child: Row( + children: [ + if (isSubItem) const SizedBox(width: 8), + Expanded( + child: Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal, + color: isSelected + ? AppColors.primaryGreen + : (isDark ? Colors.white : Colors.black87), + ), + overflow: TextOverflow.ellipsis, + ), + ), + if (isSelected) + const Icon( + Icons.check_rounded, + size: 16, + color: AppColors.primaryGreen, + ), + ], + ), + ); + } + + items.add(buildItem('All Types', 'All Types')); + items.add(const PopupMenuDivider(height: 8)); + + if (jhsReqs.isNotEmpty) { + items.add(PopupMenuItem( + enabled: false, + height: 28, + child: buildHeader('JUNIOR HIGH SCHOOL'), + )); + items.add(buildItem('All JHS', 'All JHS', isSubItem: true)); + for (final doc in jhsReqs) { + items.add(buildItem(doc, doc, isSubItem: true)); + } + } + + if (shsReqs.isNotEmpty) { + items.add(const PopupMenuDivider(height: 8)); + items.add(PopupMenuItem( + enabled: false, + height: 28, + child: buildHeader('SENIOR HIGH SCHOOL'), + )); + items.add(buildItem('All SHS', 'All SHS', isSubItem: true)); + for (final doc in shsReqs) { + items.add(buildItem(doc, doc, isSubItem: true)); + } + } + + return items; + }, ), const SizedBox(width: 2), ], @@ -1535,494 +1632,6 @@ class _DocumentsScreenState extends ConsumerState ); } - // ══════════════════════════════════════════════════════════════ - // FILTER MODAL - // ══════════════════════════════════════════════════════════════ - void _openFilterDialog( - AsyncValue> requirementsAsync, - AsyncValue> academicYearsAsync, - AsyncValue> statusesAsync, - ) { - setState(() { - _pendingStatus = _selectedStatus; - _pendingDocumentType = _selectedDocumentType; - _pendingGradeLevel = _selectedGradeLevel; - _pendingSchoolYear = _selectedSchoolYear; - }); - - showDialog( - context: context, - barrierColor: Colors.black.withValues(alpha: 0.35), - builder: (ctx) => StatefulBuilder( - builder: (ctx, setDialogState) { - final isSmallScreen = MediaQuery.of(context).size.height < 600; - return Dialog( - backgroundColor: Colors.transparent, - insetPadding: EdgeInsets.symmetric( - horizontal: 20, - vertical: isSmallScreen ? 16 : 36, - ), - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: 440, - maxHeight: MediaQuery.of(context).size.height * 0.85, - ), - child: _buildFilterPanelContent( - requirementsAsync, - academicYearsAsync, - statusesAsync, - setDialogState, - () { - setState(() { - _selectedStatus = _pendingStatus; - _selectedDocumentType = _pendingDocumentType; - _selectedGradeLevel = _pendingGradeLevel; - _selectedSchoolYear = _pendingSchoolYear; - }); - _applyFilters(); - Navigator.of(ctx).pop(); - }, - ), - ), - ); - }, - ), - ); - } - - Widget _buildFilterPanelContent( - AsyncValue> requirementsAsync, - AsyncValue> academicYearsAsync, - AsyncValue> statusesAsync, - StateSetter setDialogState, - VoidCallback onApply, - ) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final jhsReqs = requirementsAsync.when( - data: (reqs) => [ - 'All JHS', - ...reqs - .where((r) => r.category == 'JHS') - .map((r) => r.name as String) - .toSet() - .toList() - ..sort(), - ], - loading: () => ['All JHS'], - error: (err, stack) => ['All JHS'], - ); - final shsReqs = requirementsAsync.when( - data: (reqs) => [ - 'All SHS', - ...reqs - .where((r) => r.category == 'SHS') - .map((r) => r.name as String) - .toSet() - .toList() - ..sort(), - ], - loading: () => ['All SHS'], - error: (err, stack) => ['All SHS'], - ); - final years = academicYearsAsync.when( - data: (y) => ['All Years', ...y.map((ay) => ay.yearRange as String)], - loading: () => ['All Years'], - error: (err, stack) => ['All Years'], - ); - - final jhsItems = jhsReqs.where((e) => e != 'All JHS').toList(); - final shsItems = shsReqs.where((e) => e != 'All SHS').toList(); - - // Cache for use in _applyFilters - if (jhsItems.isNotEmpty) _jhsItems = jhsItems; - if (shsItems.isNotEmpty) _shsItems = shsItems; - - final docTypeOptions = [ - 'All Types', - 'All JHS', - 'All SHS', - ...jhsItems, - ...shsItems, - ]; - - return Container( - decoration: BoxDecoration( - color: isDark ? AppColors.darkSurfaceCard : AppColors.surfaceWhite, - borderRadius: BorderRadius.circular(16), - border: Border.all( - color: isDark ? AppColors.darkBorder : Colors.grey.shade200, - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.12), - blurRadius: 24, - offset: const Offset(0, 8), - ), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Header (Fixed) - Padding( - padding: const EdgeInsets.fromLTRB(20, 18, 16, 0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - color: AppColors.primaryGreen.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.tune_rounded, - size: 20, - color: AppColors.primaryGreen, - ), - ), - const SizedBox(width: 10), - Text( - 'Filter Documents', - style: TextStyle( - fontSize: 17, - fontWeight: FontWeight.w700, - color: isDark - ? AppColors.darkTextPrimary - : AppColors.textPrimary, - ), - ), - ], - ), - IconButton( - icon: Icon( - Icons.close_rounded, - size: 20, - color: isDark - ? AppColors.darkTextSecondary - : AppColors.textSecondary, - ), - tooltip: 'Close', - onPressed: () => Navigator.of(context).pop(), - ), - ], - ), - ), - - Divider( - height: 20, - thickness: 1, - color: isDark ? AppColors.darkBorder : Colors.grey.shade200, - ), - - // Scrollable middle section for filter items - Flexible( - child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - // Document Type - _buildFilterSection( - label: 'Document Type', - hasActiveFilter: _pendingDocumentType != 'All Types', - onReset: () => - setDialogState(() => _pendingDocumentType = 'All Types'), - child: _buildFilterDropdown( - value: docTypeOptions.contains(_pendingDocumentType) - ? _pendingDocumentType - : 'All Types', - items: docTypeOptions, - onChanged: (v) => - setDialogState(() => _pendingDocumentType = v!), - ), - ), - - _buildDivider(isDark), - - // Grade Level - _buildFilterSection( - label: 'Grade Level', - hasActiveFilter: _pendingGradeLevel != 'All Grades', - onReset: () => - setDialogState(() => _pendingGradeLevel = 'All Grades'), - child: _buildFilterDropdown( - value: const [ - 'All Grades', - '7', - '8', - '9', - '10', - '11', - '12', - ].contains(_pendingGradeLevel) - ? _pendingGradeLevel - : 'All Grades', - items: const ['All Grades', '7', '8', '9', '10', '11', '12'], - labelBuilder: (g) => - (g == 'All Grades' || g.toLowerCase().startsWith('grade')) - ? g - : 'Grade $g', - onChanged: (v) => - setDialogState(() => _pendingGradeLevel = v!), - ), - ), - - _buildDivider(isDark), - - // School Year - _buildFilterSection( - label: 'School Year', - hasActiveFilter: _pendingSchoolYear != 'All Years', - onReset: () => - setDialogState(() => _pendingSchoolYear = 'All Years'), - child: _buildFilterDropdown( - value: years.contains(_pendingSchoolYear) - ? _pendingSchoolYear - : 'All Years', - items: years, - onChanged: (v) => - setDialogState(() => _pendingSchoolYear = v!), - ), - ), - ], - ), - ), - ), - - Divider( - height: 1, - thickness: 1, - color: isDark ? AppColors.darkBorder : Colors.grey.shade200, - ), - - // Footer buttons (Fixed at bottom) - Padding( - padding: const EdgeInsets.fromLTRB(20, 14, 20, 16), - child: Row( - children: [ - // Reset all - Expanded( - child: OutlinedButton( - style: OutlinedButton.styleFrom( - foregroundColor: isDark - ? AppColors.darkTextSecondary - : AppColors.textSecondary, - side: BorderSide( - color: isDark - ? AppColors.darkBorder - : Colors.grey.shade300, - ), - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - ), - onPressed: () { - setDialogState(() { - _pendingStatus = 'All Statuses'; - _pendingDocumentType = 'All Types'; - _pendingGradeLevel = 'All Grades'; - _pendingSchoolYear = 'All Years'; - }); - }, - child: const Text( - 'Reset all', - style: TextStyle( - fontWeight: FontWeight.w600, - fontSize: 13.5, - ), - ), - ), - ), - const SizedBox(width: 12), - // Apply - Expanded( - flex: 2, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryGreen, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - elevation: 0, - ), - onPressed: onApply, - child: const Text( - 'Apply Filters', - style: TextStyle( - fontWeight: FontWeight.w700, - fontSize: 14, - ), - ), - ), - ), - ], - ), - ), - ], - ), - ); - } - - Widget _buildDivider(bool isDark) { - return Divider( - height: 1, - indent: 20, - endIndent: 20, - color: isDark - ? AppColors.darkBorder.withValues(alpha: 0.5) - : Colors.grey.shade100, - ); - } - - Widget _buildFilterSection({ - required String label, - required VoidCallback onReset, - required Widget child, - bool hasActiveFilter = false, - }) { - final isDark = Theme.of(context).brightness == Brightness.dark; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: isDark - ? AppColors.darkTextPrimary - : AppColors.textPrimary, - ), - ), - if (hasActiveFilter) - GestureDetector( - onTap: onReset, - behavior: HitTestBehavior.opaque, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 2, - ), - child: const Text( - 'Reset', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: AppColors.primaryGreen, - ), - ), - ), - ), - ], - ), - const SizedBox(height: 8), - child, - ], - ), - ); - } - - Widget _buildFilterDropdown({ - required String value, - required List items, - String Function(String)? labelBuilder, - ValueChanged? onChanged, - String? hint, - bool enabled = true, - }) { - final isDark = Theme.of(context).brightness == Brightness.dark; - final selectedValue = items.contains(value) ? value : items.firstOrNull; - - return Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2), - decoration: BoxDecoration( - color: enabled - ? (isDark ? AppColors.darkSurface2 : AppColors.surfaceWhite) - : (isDark ? AppColors.darkSurfaceCard : Colors.grey.shade100), - border: Border.all( - color: enabled - ? (isDark ? AppColors.darkBorder : Colors.grey.shade300) - : (isDark - ? AppColors.darkBorder.withValues(alpha: 0.5) - : Colors.grey.shade200), - ), - borderRadius: BorderRadius.circular(10), - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - value: selectedValue, - isExpanded: true, - menuMaxHeight: 260, - borderRadius: BorderRadius.circular(12), - dropdownColor: isDark ? AppColors.darkSurfaceCard : Colors.white, - elevation: 4, - icon: Icon( - Icons.keyboard_arrow_down_rounded, - color: enabled - ? (isDark - ? AppColors.darkTextSecondary - : AppColors.textSecondary) - : (isDark ? AppColors.darkTextMuted : AppColors.textMuted), - size: 22, - ), - hint: hint != null - ? Text( - hint, - style: TextStyle( - fontSize: 13.5, - color: isDark ? AppColors.darkTextMuted : AppColors.textMuted, - ), - ) - : null, - style: TextStyle( - fontSize: 13.5, - fontWeight: FontWeight.w500, - color: enabled - ? (isDark ? AppColors.darkTextPrimary : AppColors.textPrimary) - : (isDark ? AppColors.darkTextMuted : AppColors.textMuted), - ), - items: items.map((item) { - final displayLabel = - labelBuilder != null ? labelBuilder(item) : item; - final isItemActive = item == selectedValue; - return DropdownMenuItem( - value: item, - child: Text( - displayLabel, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13.5, - fontWeight: - isItemActive ? FontWeight.w600 : FontWeight.w400, - color: isItemActive - ? AppColors.primaryGreen - : (isDark - ? AppColors.darkTextPrimary - : AppColors.textPrimary), - ), - ), - ); - }).toList(), - onChanged: enabled ? onChanged : null, - ), - ), - ); - } - // ══════════════════════════════════════════════════════════════ // FOLDERS TAB // ══════════════════════════════════════════════════════════════ diff --git a/frontend/lib/ui/screens/documents/widgets/document_preview_modal.dart b/frontend/lib/ui/screens/documents/widgets/document_preview_modal.dart index eeb22a4..4f1a941 100644 --- a/frontend/lib/ui/screens/documents/widgets/document_preview_modal.dart +++ b/frontend/lib/ui/screens/documents/widgets/document_preview_modal.dart @@ -367,8 +367,89 @@ class _DocumentPreviewDialogState Widget build(BuildContext context) { final screenW = MediaQuery.of(context).size.width; final isMobile = screenW < 700; + final isSmallScreen = screenW < 400; final isDark = Theme.of(context).brightness == Brightness.dark; + final List<_PreviewActionItem> directActions = []; + final List<_PreviewActionItem> menuActions = []; + + if (_isExcel && widget.document != null) { + directActions.add(_PreviewActionItem( + id: 'convert_pdf', + label: 'Convert to PDF', + iconData: Icons.picture_as_pdf_outlined, + onTap: _isConverting ? null : _convertToPdf, + isLoading: _isConverting, + )); + } + if (_isExcel) { + directActions.add(_PreviewActionItem( + id: 'open_external', + label: 'Open in External Viewer', + iconData: Icons.open_in_new, + onTap: _isOpeningExternal ? null : _openExternalExcel, + isLoading: _isOpeningExternal, + )); + } + if (widget.document != null) { + directActions.add(_PreviewActionItem( + id: 'print_list', + label: 'Add to Print List', + iconData: Icons.print_outlined, + onTap: _addToPrintList, + )); + directActions.add(_PreviewActionItem( + id: 'download', + label: 'Download', + iconData: Icons.download_rounded, + onTap: _downloadFile, + )); + directActions.add(_PreviewActionItem( + id: 'properties', + label: 'Properties', + iconData: Icons.info_outline_rounded, + onTap: _showProperties, + )); + if (widget.document!.studentId != null) { + directActions.add(_PreviewActionItem( + id: 'view_student', + label: 'View Student Profile', + iconData: Icons.person_outline_rounded, + onTap: _viewStudentProfile, + )); + } + + // Always inside More menu: Copy and Delete + menuActions.add(_PreviewActionItem( + id: 'copy', + label: 'Copy', + iconData: Icons.copy_rounded, + onTap: _copyDocument, + )); + if (ref.watch(authProvider).value?.role != 'teacher') { + menuActions.add(_PreviewActionItem( + id: 'delete', + label: 'Delete', + iconData: Icons.delete_outline_rounded, + onTap: _deleteDocument, + color: Colors.redAccent, + )); + } + } + + final List<_PreviewActionItem> shownDirectActions = []; + final List<_PreviewActionItem> allMenuActions = []; + + if (isSmallScreen) { + allMenuActions.addAll(directActions); + allMenuActions.addAll(menuActions); + } else { + const int maxDirect = 4; + shownDirectActions.addAll(directActions.take(maxDirect)); + allMenuActions.addAll(directActions.skip(maxDirect)); + allMenuActions.addAll(menuActions); + } + if (widget.document != null && _token == null) { return const Dialog.fullscreen( child: Center(child: CircularProgressIndicator()), @@ -407,9 +488,9 @@ class _DocumentPreviewDialogState ], ), actions: [ - if (_isExcel && widget.document != null) - IconButton( - icon: _isConverting + ...shownDirectActions.map( + (action) => IconButton( + icon: action.isLoading ? const SizedBox( width: 18, height: 18, @@ -418,59 +499,50 @@ class _DocumentPreviewDialogState strokeWidth: 2, ), ) - : const Icon(Icons.picture_as_pdf_outlined, color: Colors.white), - tooltip: 'Convert to PDF', - onPressed: _isConverting ? null : _convertToPdf, + : Icon(action.iconData, color: Colors.white), + tooltip: action.label, + onPressed: action.onTap, ), - if (_isExcel) - IconButton( - icon: _isOpeningExternal - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, + ), + if (allMenuActions.isNotEmpty) + PopupMenuButton( + icon: const Icon(Icons.more_vert_rounded, color: Colors.white), + tooltip: 'More Actions', + onSelected: (id) { + final action = allMenuActions.firstWhere((a) => a.id == id); + action.onTap?.call(); + }, + itemBuilder: (context) => allMenuActions.map((action) { + return PopupMenuItem( + value: action.id, + child: Row( + children: [ + if (action.isLoading) + const SizedBox( + width: 18, + height: 18, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + Icon( + action.iconData, + size: 20, + color: action.color ?? + (isDark ? Colors.white70 : Colors.black87), + ), + const SizedBox(width: 12), + Text( + action.label, + style: TextStyle( + color: action.color, + fontSize: 14, + ), ), - ) - : const Icon(Icons.open_in_new, color: Colors.white), - tooltip: 'Open in External Viewer', - onPressed: _isOpeningExternal ? null : _openExternalExcel, + ], + ), + ); + }).toList(), ), - if (widget.document != null) ...[ - IconButton( - icon: const Icon(Icons.copy_rounded, color: Colors.white), - tooltip: 'Copy', - onPressed: _copyDocument, - ), - IconButton( - icon: const Icon(Icons.print_outlined, color: Colors.white), - tooltip: 'Add to Print List', - onPressed: _addToPrintList, - ), - IconButton( - icon: const Icon(Icons.download_rounded, color: Colors.white), - tooltip: 'Download', - onPressed: _downloadFile, - ), - IconButton( - icon: const Icon(Icons.info_outline_rounded, color: Colors.white), - tooltip: 'Properties', - onPressed: _showProperties, - ), - if (widget.document!.studentId != null) - IconButton( - icon: const Icon(Icons.person_outline_rounded, color: Colors.white), - tooltip: 'View Student Profile', - onPressed: _viewStudentProfile, - ), - if (ref.watch(authProvider).value?.role != 'teacher') - IconButton( - icon: const Icon(Icons.delete_outline_rounded, color: Colors.white), - tooltip: 'Delete', - onPressed: _deleteDocument, - ), - ], const SizedBox(width: 8), ], ), @@ -883,3 +955,22 @@ class _DocumentPreviewDialogState ); } } + +class _PreviewActionItem { + final String id; + final String label; + final IconData iconData; + final VoidCallback? onTap; + final Color? color; + final bool isLoading; + + const _PreviewActionItem({ + required this.id, + required this.label, + required this.iconData, + required this.onTap, + this.color, + this.isLoading = false, + }); +} + 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 59bbed8..a8bbf13 100644 --- a/frontend/lib/ui/screens/students/widgets/add_student_modal.dart +++ b/frontend/lib/ui/screens/students/widgets/add_student_modal.dart @@ -21,6 +21,12 @@ class AddStudentModal extends ConsumerStatefulWidget { final bool isDialog; const AddStudentModal({super.key, this.isDialog = false}); + static Future show(BuildContext context) { + return Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const AddStudentModal()), + ); + } + @override ConsumerState createState() => _AddStudentModalState(); }