import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:relationship_saver/core/config/app_theme.dart'; import 'package:relationship_saver/features/shared/frosted_card.dart'; import 'package:relationship_saver/features/sync/sync_coordinator.dart'; import 'package:relationship_saver/features/sync/sync_queue_repository.dart'; import 'package:relationship_saver/features/sync/sync_state.dart'; import 'package:relationship_saver/integrations/backend/models/backend_models.dart'; class SyncView extends ConsumerStatefulWidget { const SyncView({super.key}); @override ConsumerState createState() => _SyncViewState(); } class _SyncViewState extends ConsumerState { bool _busy = false; String _status = 'Ready. Local-first mode keeps app usable without sync.'; @override Widget build(BuildContext context) { final AsyncValue syncState = ref.watch( syncQueueRepositoryProvider, ); return LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { final bool compact = constraints.maxWidth < 760; return Padding( padding: EdgeInsets.fromLTRB( compact ? 16 : 28, compact ? 16 : 24, compact ? 16 : 28, compact ? 20 : 28, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Sync', style: Theme.of(context).textTheme.headlineMedium), const SizedBox(height: 6), Text( 'Queue local changes, then push and pull when network is available.', style: Theme.of( context, ).textTheme.bodyLarge?.copyWith(color: AppTheme.textSecondary), ), const SizedBox(height: 16), Expanded( child: syncState.when( data: (SyncState state) => _buildBody(context, state, compact), loading: () => const Center(child: CircularProgressIndicator()), error: (Object error, StackTrace stackTrace) { return Center( child: Text( 'Unable to load sync state. $error', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: AppTheme.textSecondary, ), ), ); }, ), ), ], ), ); }, ); } Widget _buildBody(BuildContext context, SyncState state, bool compact) { return ListView( children: [ FrostedCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 10, runSpacing: 10, children: [ FilledButton.icon( onPressed: _busy ? null : _syncNow, icon: const Icon(Icons.sync_rounded), label: const Text('Sync Now'), ), OutlinedButton.icon( onPressed: _busy ? null : _pushPending, icon: const Icon(Icons.upload_rounded), label: const Text('Push Pending'), ), OutlinedButton.icon( onPressed: _busy ? null : _pullChanges, icon: const Icon(Icons.download_rounded), label: const Text('Pull Changes'), ), TextButton.icon( onPressed: _busy ? null : _clearQueue, icon: const Icon(Icons.clear_all_rounded), label: const Text('Clear Queue'), ), ], ), const SizedBox(height: 16), Text( _status, style: Theme.of( context, ).textTheme.bodyLarge?.copyWith(color: AppTheme.textSecondary), ), ], ), ), const SizedBox(height: 16), FrostedCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Queue Status', style: Theme.of(context).textTheme.titleMedium, ), const SizedBox(height: 10), _infoRow( label: 'Pending changes', value: '${state.pendingChanges.length}', compact: compact, ), _infoRow( label: 'Cursor', value: state.cursor ?? 'not set', compact: compact, ), _infoRow( label: 'Last sync', value: _formatDateTime(state.lastSyncAt), compact: compact, ), _infoRow( label: 'Last attempt', value: _formatDateTime(state.lastAttemptAt), compact: compact, ), _infoRow( label: 'Last rejected count', value: '${state.lastRejected.length}', compact: compact, ), _infoRow( label: 'Last error', value: state.lastError ?? 'none', compact: compact, ), ], ), ), const SizedBox(height: 16), if (state.lastRejected.isNotEmpty) ...[ FrostedCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 8, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ Text( 'Rejected Changes', style: Theme.of(context).textTheme.titleMedium, ), TextButton.icon( onPressed: _busy ? null : _dismissRejections, icon: const Icon(Icons.done_all_rounded), label: const Text('Dismiss'), ), ], ), const SizedBox(height: 8), ...state.lastRejected.map( (MutationRejection rejection) => Padding( padding: const EdgeInsets.only(bottom: 10), child: _rejectionTile(rejection), ), ), ], ), ), const SizedBox(height: 16), ], FrostedCard( child: Text( 'Sync policy\n• Core usage never blocks on backend\n• Local changes queue first and remain safe offline\n• Pull applies backend envelopes into local state', style: Theme.of(context).textTheme.bodyLarge, ), ), ], ); } Widget _infoRow({ required String label, required String value, required bool compact, }) { if (compact) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: Theme.of( context, ).textTheme.labelLarge?.copyWith(color: AppTheme.textSecondary), ), const SizedBox(height: 2), Text(value, style: Theme.of(context).textTheme.bodyMedium), ], ), ); } return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox(width: 150, child: Text(label)), Expanded( child: Text( value, style: Theme.of( context, ).textTheme.bodyMedium?.copyWith(color: AppTheme.textSecondary), ), ), ], ), ); } Widget _rejectionTile(MutationRejection rejection) { return Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.72), borderRadius: BorderRadius.circular(12), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 8, runSpacing: 8, crossAxisAlignment: WrapCrossAlignment.center, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: const Color(0xFFEAF7FB), borderRadius: BorderRadius.circular(999), ), child: Text( rejection.code, style: Theme.of( context, ).textTheme.labelMedium?.copyWith(color: AppTheme.primary), ), ), Text( 'Mutation ${rejection.clientMutationId}', style: Theme.of( context, ).textTheme.bodySmall?.copyWith(color: AppTheme.textSecondary), ), ], ), const SizedBox(height: 6), Text( rejection.message, style: Theme.of(context).textTheme.bodyMedium, ), ], ), ); } String _formatDateTime(DateTime? value) { if (value == null) { return 'never'; } final DateTime local = value.toLocal(); final String month = local.month.toString().padLeft(2, '0'); final String day = local.day.toString().padLeft(2, '0'); final String hour = local.hour.toString().padLeft(2, '0'); final String minute = local.minute.toString().padLeft(2, '0'); return '${local.year}-$month-$day $hour:$minute'; } Future _syncNow() async { await _run((SyncCoordinator coordinator) => coordinator.syncNow()); } Future _pushPending() async { await _run((SyncCoordinator coordinator) => coordinator.pushPending()); } Future _pullChanges() async { await _run((SyncCoordinator coordinator) => coordinator.pullRemote()); } Future _clearQueue() async { setState(() { _busy = true; }); try { await ref.read(syncQueueRepositoryProvider.notifier).clearQueue(); setState(() { _status = 'Cleared pending sync queue.'; }); } finally { if (mounted) { setState(() { _busy = false; }); } } } Future _dismissRejections() async { setState(() { _busy = true; }); try { await ref.read(syncQueueRepositoryProvider.notifier).clearRejections(); setState(() { _status = 'Dismissed latest rejected-mutation details.'; }); } finally { if (mounted) { setState(() { _busy = false; }); } } } Future _run( Future Function(SyncCoordinator coordinator) action, ) async { setState(() { _busy = true; _status = 'Running sync operation...'; }); try { final SyncRunResult result = await action( ref.read(syncCoordinatorProvider), ); setState(() { _status = result.message; }); } catch (error) { setState(() { _status = 'Sync operation failed unexpectedly. ($error)'; }); } finally { if (mounted) { setState(() { _busy = false; }); } } } }