import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../brick/cache_warmer.dart'; import '../providers/connectivity_provider.dart'; import '../providers/offline_readiness_provider.dart'; import '../providers/supabase_provider.dart'; import '../providers/sync_queue_provider.dart'; void showOfflineReadinessSheet(BuildContext context) { final container = ProviderScope.containerOf(context); showModalBottomSheet( context: context, isScrollControlled: true, useSafeArea: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (_) => UncontrolledProviderScope( container: container, child: const _OfflineReadinessSheet(), ), ); } class _OfflineReadinessSheet extends ConsumerStatefulWidget { const _OfflineReadinessSheet(); @override ConsumerState<_OfflineReadinessSheet> createState() => _OfflineReadinessSheetState(); } class _OfflineReadinessSheetState extends ConsumerState<_OfflineReadinessSheet> { bool _refreshing = false; Future _refresh() async { if (_refreshing) return; setState(() => _refreshing = true); try { final client = ref.read(supabaseClientProvider); await CacheWarmer.warmAll(client); ref.invalidate(offlineReadinessProvider); } finally { if (mounted) setState(() => _refreshing = false); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final scheme = theme.colorScheme; final readiness = ref.watch(offlineReadinessProvider); final isOnline = ref.watch(isOnlineProvider); return DraggableScrollableSheet( initialChildSize: 0.6, minChildSize: 0.4, maxChildSize: 0.92, expand: false, builder: (context, scrollController) { return Column( children: [ // Handle bar Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Container( width: 40, height: 4, decoration: BoxDecoration( color: scheme.onSurfaceVariant.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(2), ), ), ), // Header Padding( padding: const EdgeInsets.fromLTRB(20, 0, 16, 12), child: Row( children: [ Icon(Icons.offline_bolt_rounded, color: scheme.primary, size: 22), const SizedBox(width: 10), Text( 'Offline Readiness', style: theme.textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600), ), const Spacer(), if (isOnline) _refreshing ? SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: scheme.primary), ) : IconButton( icon: const Icon(Icons.refresh_rounded), tooltip: 'Refresh cache', onPressed: _refresh, ), ], ), ), const Divider(height: 1), // Body Expanded( child: readiness.when( loading: () => const Center(child: CircularProgressIndicator()), error: (e, _) => Center( child: Text('Unable to load status', style: TextStyle(color: scheme.error)), ), data: (state) => _ReadinessBody( state: state, isOnline: isOnline, scrollController: scrollController, onRefresh: _refresh, ), ), ), ], ); }, ); } } class _ReadinessBody extends ConsumerWidget { final OfflineReadinessState state; final bool isOnline; final ScrollController scrollController; final VoidCallback onRefresh; const _ReadinessBody({ required this.state, required this.isOnline, required this.scrollController, required this.onRefresh, }); @override Widget build(BuildContext context, WidgetRef ref) { final theme = Theme.of(context); final scheme = theme.colorScheme; final breakdown = ref.watch(pendingSyncBreakdownProvider); return ListView( controller: scrollController, padding: const EdgeInsets.fromLTRB(16, 12, 16, 32), children: [ // Overall status chip _StatusChip(isReady: state.isReady, isOnline: isOnline), const SizedBox(height: 20), // Cached data section Text( 'Cached Data', style: theme.textTheme.labelLarge?.copyWith( color: scheme.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), _CacheGrid(counts: state.cacheCounts), // Pending sync section — shown whenever any module has pending items if (breakdown.isNotEmpty) ...[ const SizedBox(height: 20), Text( 'Pending Sync', style: theme.textTheme.labelLarge?.copyWith( color: scheme.onSurfaceVariant, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 8), _PendingList(breakdown: breakdown, isOnline: isOnline), ], // Prompt to go online if not ready and not warmed if (!state.hasWarmedOnce && !isOnline) ...[ const SizedBox(height: 20), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: scheme.errorContainer.withValues(alpha: 0.4), borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon(Icons.info_outline_rounded, color: scheme.error, size: 20), const SizedBox(width: 12), Expanded( child: Text( 'Connect to the internet at least once to download offline data.', style: theme.textTheme.bodySmall?.copyWith(color: scheme.onErrorContainer), ), ), ], ), ), ], if (!isOnline && state.hasWarmedOnce) ...[ const SizedBox(height: 20), FilledButton.tonalIcon( onPressed: null, icon: const Icon(Icons.cloud_off_rounded, size: 18), label: const Text('Connect to refresh cache'), ), ] else if (isOnline) ...[ const SizedBox(height: 20), FilledButton.tonalIcon( onPressed: onRefresh, icon: const Icon(Icons.refresh_rounded, size: 18), label: const Text('Refresh Cache Now'), ), ], ], ); } } class _StatusChip extends StatelessWidget { final bool isReady; final bool isOnline; const _StatusChip({required this.isReady, required this.isOnline}); @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final ready = isReady; final bg = ready ? scheme.primaryContainer : scheme.errorContainer; final fg = ready ? scheme.onPrimaryContainer : scheme.onErrorContainer; final icon = ready ? Icons.check_circle_rounded : Icons.warning_amber_rounded; final label = ready ? 'Ready for offline use' : 'Cache incomplete — go online to set up'; return AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: Container( key: ValueKey(ready), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(12)), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: fg, size: 18), const SizedBox(width: 8), Flexible( child: Text(label, style: TextStyle(color: fg, fontWeight: FontWeight.w500)), ), ], ), ), ); } } class _CacheGrid extends StatefulWidget { final Map counts; const _CacheGrid({required this.counts}); @override State<_CacheGrid> createState() => _CacheGridState(); } class _CacheGridState extends State<_CacheGrid> with SingleTickerProviderStateMixin { late final AnimationController _controller; static const _groups = [ ('Tasks', 'tasks', Icons.task_alt_rounded), ('Tickets', 'tickets', Icons.confirmation_number_rounded), ('Announcements', 'announcements', Icons.campaign_rounded), ('Attendance', 'attendance_logs', Icons.fingerprint_rounded), ('Schedules', 'duty_schedules', Icons.calendar_month_rounded), ('Pass Slips', 'pass_slips', Icons.badge_rounded), ('Leaves', 'leave_of_absence', Icons.beach_access_rounded), ('Profiles', 'profiles', Icons.people_rounded), ('Teams', 'teams', Icons.groups_rounded), ('IT Requests', 'it_service_requests', Icons.computer_rounded), ('Notifications', 'notifications', Icons.notifications_rounded), ('Offices', 'offices', Icons.business_rounded), ]; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, )..forward(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; return GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 8, crossAxisSpacing: 8, mainAxisExtent: 100, ), itemCount: _groups.length, itemBuilder: (context, i) { final (label, key, icon) = _groups[i]; final count = widget.counts[key] ?? -1; final cached = count >= 0; final start = i * 0.055; final end = (start + 0.45).clamp(0.0, 1.0); final curve = CurvedAnimation( parent: _controller, curve: Interval(start, end, curve: Curves.easeOutCubic), ); final fadeAnim = Tween(begin: 0.0, end: 1.0).animate(curve); final slideAnim = Tween( begin: const Offset(0, 0.25), end: Offset.zero, ).animate(curve); return FadeTransition( opacity: fadeAnim, child: SlideTransition( position: slideAnim, child: AnimatedSwitcher( duration: const Duration(milliseconds: 400), child: _CacheTile( key: ValueKey('$key-$count'), label: label, icon: icon, count: count, cached: cached, scheme: scheme, ), ), ), ); }, ); } } class _CacheTile extends StatelessWidget { final String label; final IconData icon; final int count; final bool cached; final ColorScheme scheme; const _CacheTile({ super.key, required this.label, required this.icon, required this.count, required this.cached, required this.scheme, }); @override Widget build(BuildContext context) { final bg = cached ? scheme.primaryContainer.withValues(alpha: 0.5) : scheme.surfaceContainerHighest; final fg = cached ? scheme.onPrimaryContainer : scheme.onSurfaceVariant; return Container( decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.all(12), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(icon, color: fg, size: 24), const SizedBox(height: 6), SizedBox( height: 30, child: Center( child: Text( label, style: TextStyle(color: fg, fontSize: 11, fontWeight: FontWeight.w500), textAlign: TextAlign.center, maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ), const SizedBox(height: 2), Text( cached ? '$count items' : 'Not cached', style: TextStyle(color: fg.withValues(alpha: 0.7), fontSize: 10), textAlign: TextAlign.center, ), ], ), ); } } class _PendingList extends StatelessWidget { final Map breakdown; final bool isOnline; const _PendingList({required this.breakdown, required this.isOnline}); static const _icons = { 'tasks': Icons.task_alt_rounded, 'tickets': Icons.confirmation_number_rounded, 'messages': Icons.chat_bubble_rounded, 'assignments': Icons.assignment_ind_rounded, 'activity_logs': Icons.history_rounded, 'check_ins': Icons.login_rounded, 'attendance': Icons.fingerprint_rounded, 'leaves': Icons.beach_access_rounded, 'pass_slips': Icons.badge_rounded, 'announcements': Icons.campaign_rounded, 'it_requests': Icons.computer_rounded, 'notification_reads': Icons.notifications_rounded, 'profile_updates': Icons.person_rounded, 'office_changes': Icons.business_rounded, }; static String _label(String key, int count) { final n = count.toString(); final s = count == 1 ? '' : 's'; switch (key) { case 'tasks': return '$n task$s'; case 'tickets': return '$n ticket$s'; case 'messages': return '$n message$s'; case 'assignments': return '$n assignment$s'; case 'activity_logs': return '$n activity log$s'; case 'check_ins': return '$n check-in$s'; case 'attendance': return '$n attendance update$s'; case 'leaves': return '$n leave$s'; case 'pass_slips': return '$n pass slip$s'; case 'announcements': return '$n announcement$s'; case 'it_requests': return '$n IT request$s'; case 'notification_reads': return '$n notification read$s'; case 'profile_updates': return '$n profile update$s'; case 'office_changes': return '$n office change$s'; default: return '$n $key'; } } @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; final syncing = isOnline && breakdown.isNotEmpty; return Column( children: [ for (int i = 0; i < breakdown.length; i++) ...[ if (i > 0) const SizedBox(height: 6), _PendingTile( icon: _icons[breakdown.keys.elementAt(i)] ?? Icons.sync_rounded, label: _label(breakdown.keys.elementAt(i), breakdown.values.elementAt(i)), syncing: syncing, scheme: scheme, ), ], ], ); } } class _PendingTile extends StatelessWidget { final IconData icon; final String label; final bool syncing; final ColorScheme scheme; const _PendingTile({ required this.icon, required this.label, required this.syncing, required this.scheme, }); @override Widget build(BuildContext context) { final bg = syncing ? scheme.tertiaryContainer : scheme.surfaceContainerHighest; final fg = syncing ? scheme.onTertiaryContainer : scheme.onSurfaceVariant; return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration(color: bg, borderRadius: BorderRadius.circular(10)), child: Row( children: [ syncing ? _SpinningIcon(icon: icon, color: fg) : Icon(icon, color: fg, size: 18), const SizedBox(width: 10), Expanded( child: Text(label, style: TextStyle(color: fg, fontWeight: FontWeight.w500)), ), if (syncing) Text('Syncing…', style: TextStyle(color: fg.withValues(alpha: 0.7), fontSize: 12)), ], ), ); } } class _SpinningIcon extends StatefulWidget { final IconData icon; final Color color; const _SpinningIcon({required this.icon, required this.color}); @override State<_SpinningIcon> createState() => _SpinningIconState(); } class _SpinningIconState extends State<_SpinningIcon> with SingleTickerProviderStateMixin { late final AnimationController _ctrl; @override void initState() { super.initState(); _ctrl = AnimationController(vsync: this, duration: const Duration(seconds: 2))..repeat(); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return RotationTransition( turns: _ctrl, child: Icon(widget.icon, color: widget.color, size: 18), ); } }