import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart' show kIsWeb; import '../theme/m3_motion.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; // showcaseview removed due to null-safety incompatibility; onboarding shown via dialog import '../providers/auth_provider.dart'; import '../providers/notifications_provider.dart'; import '../providers/profile_provider.dart'; import 'app_breakpoints.dart'; import 'profile_avatar.dart'; import 'pass_slip_countdown_banner.dart'; import 'shift_countdown_banner.dart'; final GlobalKey notificationBellKey = GlobalKey(); class AppScaffold extends ConsumerWidget { const AppScaffold({super.key, required this.child}); final Widget child; @override Widget build(BuildContext context, WidgetRef ref) { final profileAsync = ref.watch(currentProfileProvider); final role = profileAsync.maybeWhen( data: (profile) => profile?.role ?? 'standard', orElse: () => 'standard', ); final displayName = profileAsync.maybeWhen( data: (profile) { final name = profile?.fullName.trim() ?? ''; return name.isNotEmpty ? name : 'User'; }, orElse: () => 'User', ); final avatarUrl = profileAsync.maybeWhen( data: (profile) => profile?.avatarUrl, orElse: () => null, ); final isStandard = role == 'standard'; final location = GoRouterState.of(context).uri.toString(); final sections = _buildSections(role); final width = MediaQuery.of(context).size.width; final showRail = width >= AppBreakpoints.tablet; final isExtended = width >= AppBreakpoints.desktop; final railItems = _flattenSections( sections, ).where((item) => !item.isLogout).toList(); final primaryItems = _primaryItemsForRole(role); final mobilePrimary = _mobilePrimaryItems(primaryItems); final overflowItems = _overflowItems(railItems, mobilePrimary); return Scaffold( appBar: AppBar( title: Row( children: [ Image.asset('assets/tasq_ico.png', width: 28, height: 28), const SizedBox(width: 8), Text('TasQ'), ], ), actions: [ if (isStandard) PopupMenuButton( tooltip: 'Account', onSelected: (value) { if (value == 0) { context.go('/profile'); } else if (value == 1) { ref.read(authControllerProvider).signOut(); } }, itemBuilder: (context) => const [ PopupMenuItem(value: 0, child: Text('My profile')), PopupMenuItem(value: 1, child: Text('Sign out')), ], child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( children: [ ProfileAvatar( fullName: displayName, avatarUrl: avatarUrl, radius: 16, heroTag: 'profile-avatar', ), const SizedBox(width: 8), Text(displayName), const SizedBox(width: 4), const Icon(Icons.expand_more), ], ), ), ) else Row( mainAxisSize: MainAxisSize.min, children: [ IconButton( tooltip: 'Profile', onPressed: () => context.go('/profile'), icon: ProfileAvatar( fullName: displayName, avatarUrl: avatarUrl, radius: 16, heroTag: 'profile-avatar', ), ), IconButton( tooltip: 'Sign out', onPressed: () => ref.read(authControllerProvider).signOut(), icon: const Icon(Icons.logout), ), ], ), const _NotificationBell(), ], ), bottomNavigationBar: showRail ? null : AppBottomNav( location: location, items: _mobileNavItems(mobilePrimary, overflowItems), onShowMore: overflowItems.isEmpty ? null : () => _showOverflowSheet( context, overflowItems, () => ref.read(authControllerProvider).signOut(), ), ), body: LayoutBuilder( builder: (context, constraints) { final railWidth = showRail ? (isExtended ? 256.0 : 80.0) : 0.0; return Stack( children: [ Positioned.fill( left: railWidth, child: _ShellBackground(child: child), ), if (showRail) Positioned( left: 0, top: 0, bottom: 0, width: railWidth, child: AppNavigationRail( items: railItems, location: location, extended: isExtended, displayName: displayName, onLogout: () => ref.read(authControllerProvider).signOut(), ), ), ], ); }, ), ); } } class AppNavigationRail extends StatelessWidget { const AppNavigationRail({ super.key, required this.items, required this.location, required this.extended, required this.displayName, required this.onLogout, }); final List items; final String location; final bool extended; final String displayName; final VoidCallback onLogout; @override Widget build(BuildContext context) { final currentIndex = _currentIndex(location, items); final cs = Theme.of(context).colorScheme; // M3 Expressive: tonal surface container instead of a hard border divider. return Container( decoration: BoxDecoration(color: cs.surfaceContainerLow), child: NavigationRail( backgroundColor: Colors.transparent, extended: extended, selectedIndex: currentIndex, onDestinationSelected: (value) { items[value].onTap(context, onLogout: onLogout); }, leading: Padding( padding: EdgeInsets.symmetric( vertical: extended ? 12 : 8, horizontal: extended ? 16 : 0, ), child: Center( child: Image.asset( 'assets/tasq_ico.png', width: extended ? 48 : 40, height: extended ? 48 : 40, ), ), ), trailing: Expanded( child: Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 16), child: IconButton( tooltip: 'Sign out', onPressed: onLogout, icon: const Icon(Icons.logout), ), ), ), ), destinations: [ for (final item in items) NavigationRailDestination( icon: Icon(item.icon), selectedIcon: Icon(item.selectedIcon ?? item.icon), label: Text(item.label), ), ], ), ); } } class AppBottomNav extends StatelessWidget { const AppBottomNav({ super.key, required this.location, required this.items, this.onShowMore, }); final String location; final List items; final VoidCallback? onShowMore; @override Widget build(BuildContext context) { final index = _currentIndex(location, items); return NavigationBar( selectedIndex: index, onDestinationSelected: (value) { final item = items[value]; if (item.isOverflow) { onShowMore?.call(); return; } item.onTap(context); }, destinations: [ for (final item in items) NavigationDestination( icon: Icon(item.icon), selectedIcon: Icon(item.selectedIcon ?? item.icon), label: item.label, ), ], ); } } class _NotificationBell extends ConsumerWidget { const _NotificationBell(); @override Widget build(BuildContext context, WidgetRef ref) { final unreadCount = ref.watch(unreadNotificationsCountProvider); final cs = Theme.of(context).colorScheme; return IconButton( tooltip: 'Notifications', onPressed: () => context.go('/notifications'), icon: Stack( clipBehavior: Clip.none, children: [ const Icon(Icons.notifications_outlined), AnimatedSwitcher( duration: const Duration(milliseconds: 200), switchInCurve: Curves.easeOutBack, switchOutCurve: Curves.easeInCubic, transitionBuilder: (child, animation) => ScaleTransition( scale: animation, child: child, ), child: unreadCount > 0 ? Positioned( key: const ValueKey('badge'), right: -3, top: -3, child: Container( width: 10, height: 10, decoration: BoxDecoration( color: cs.error, shape: BoxShape.circle, border: Border.all( color: cs.surface, width: 1.5, ), ), ), ) : const SizedBox.shrink(key: ValueKey('no-badge')), ), ], ), ); } } /// M3 Expressive shell background — uses a subtle tonal surface tint /// rather than a gradient to create the organic, seed-colored feel. class _ShellBackground extends StatelessWidget { const _ShellBackground({required this.child}); final Widget child; @override Widget build(BuildContext context) { return ColoredBox( color: Theme.of(context).scaffoldBackgroundColor, child: ShiftCountdownBanner( child: PassSlipCountdownBanner(child: child), ), ); } } class NavItem { NavItem({ required this.label, required this.route, required this.icon, this.selectedIcon, this.isLogout = false, this.isOverflow = false, }); final String label; final String route; final IconData icon; final IconData? selectedIcon; final bool isLogout; final bool isOverflow; void onTap(BuildContext context, {VoidCallback? onLogout}) { if (isLogout) { onLogout?.call(); return; } if (route.isNotEmpty) { context.go(route); } } } class NavSection { NavSection({this.label, required this.items}); final String? label; final List items; } List _buildSections(String role) { final isStandard = role == 'standard'; final mainItems = [ NavItem( label: 'Dashboard', route: '/dashboard', icon: Icons.grid_view, selectedIcon: Icons.grid_view_rounded, ), if (!isStandard) NavItem( label: 'Attendance', route: '/attendance', icon: Icons.fact_check_outlined, selectedIcon: Icons.fact_check, ), if (!isStandard) NavItem( label: 'Whereabouts', route: '/whereabouts', icon: Icons.share_location_outlined, selectedIcon: Icons.share_location, ), NavItem( label: 'Tickets', route: '/tickets', icon: Icons.support_agent_outlined, selectedIcon: Icons.support_agent, ), NavItem( label: 'Tasks', route: '/tasks', icon: Icons.task_outlined, selectedIcon: Icons.task, ), NavItem( label: 'IT Service Requests', route: '/it-service-requests', icon: Icons.miscellaneous_services_outlined, selectedIcon: Icons.miscellaneous_services, ), NavItem( label: 'Announcement', route: '/announcements', icon: Icons.campaign_outlined, selectedIcon: Icons.campaign, ), NavItem( label: 'Workforce', route: '/workforce', icon: Icons.groups_outlined, selectedIcon: Icons.groups, ), NavItem( label: 'Reports', route: '/reports', icon: Icons.analytics_outlined, selectedIcon: Icons.analytics, ), ]; if (role == 'admin' || role == 'programmer' || role == 'dispatcher') { return [ NavSection(label: 'Operations', items: mainItems), NavSection( label: 'Settings', items: [ NavItem( label: 'User Management', route: '/settings/users', icon: Icons.admin_panel_settings_outlined, selectedIcon: Icons.admin_panel_settings, ), NavItem( label: 'Office Management', route: '/settings/offices', icon: Icons.apartment_outlined, selectedIcon: Icons.apartment, ), NavItem( label: 'IT Staff Teams', route: '/settings/teams', icon: Icons.groups_2_outlined, selectedIcon: Icons.groups_2, ), if (kIsWeb) ...[ NavItem( label: 'App Update', route: '/settings/app-update', icon: Icons.system_update_alt, selectedIcon: Icons.system_update, ), ], NavItem( label: 'Logout', route: '', icon: Icons.logout, isLogout: true, ), ], ), ]; } return [ NavSection(label: 'Operations', items: mainItems), NavSection( label: 'Settings', items: [ NavItem( label: 'Logout', route: '', icon: Icons.logout, isLogout: true, ), ], ), ]; } List _standardNavItems() { return [ NavItem( label: 'Dashboard', route: '/dashboard', icon: Icons.grid_view, selectedIcon: Icons.grid_view_rounded, ), NavItem( label: 'Tickets', route: '/tickets', icon: Icons.support_agent_outlined, selectedIcon: Icons.support_agent, ), NavItem( label: 'Tasks', route: '/tasks', icon: Icons.task_outlined, selectedIcon: Icons.task, ), NavItem( label: 'IT Service Requests', route: '/it-service-requests', icon: Icons.miscellaneous_services_outlined, selectedIcon: Icons.miscellaneous_services, ), ]; } List _primaryItemsForRole(String role) { if (role == 'admin' || role == 'programmer') { return [ NavItem( label: 'Dashboard', route: '/dashboard', icon: Icons.grid_view_outlined, selectedIcon: Icons.grid_view_rounded, ), NavItem( label: 'Tickets', route: '/tickets', icon: Icons.support_agent_outlined, selectedIcon: Icons.support_agent, ), NavItem( label: 'Tasks', route: '/tasks', icon: Icons.task_outlined, selectedIcon: Icons.task, ), NavItem( label: 'Workforce', route: '/workforce', icon: Icons.groups_outlined, selectedIcon: Icons.groups, ), NavItem( label: 'Reports', route: '/reports', icon: Icons.analytics_outlined, selectedIcon: Icons.analytics, ), ]; } return _standardNavItems(); } List _mobileNavItems(List primary, List overflow) { if (overflow.isEmpty) { return primary; } return [ ...primary, NavItem(label: 'More', route: '', icon: Icons.more_horiz, isOverflow: true), ]; } List _mobilePrimaryItems(List primary) { if (primary.length <= 4) { return primary; } return primary.take(4).toList(); } List _flattenSections(List sections) { return [for (final section in sections) ...section.items]; } List _overflowItems(List all, List primary) { final primaryRoutes = primary.map((item) => item.route).toSet(); return all .where( (item) => !item.isLogout && item.route.isNotEmpty && !primaryRoutes.contains(item.route), ) .toList(); } Future _showOverflowSheet( BuildContext context, List items, VoidCallback onLogout, ) async { await m3ShowBottomSheet( context: context, builder: (context) { return SafeArea( child: ListView( padding: const EdgeInsets.all(16), children: [ for (final item in items) ListTile( leading: Icon(item.icon), title: Text(item.label), onTap: () { Navigator.of(context).pop(); item.onTap(context, onLogout: onLogout); }, ), ], ), ); }, ); } bool _isSelected(String location, String route) { if (route.isEmpty) return false; if (location == route) return true; return location.startsWith('$route/'); } int _currentIndex(String location, List items) { final index = items.indexWhere((item) => _isSelected(location, item.route)); if (index != -1) return index; final overflowIndex = items.indexWhere((item) => item.isOverflow); return overflowIndex == -1 ? 0 : overflowIndex; }