import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:file_picker/file_picker.dart'; import 'package:share_plus/share_plus.dart'; import '../../../../shared/design_system/tokens/color_tokens.dart'; import '../../../../shared/design_system/tokens/spacing_tokens.dart'; import '../../../../core/di/injection_container.dart'; import '../../../../core/utils/logger.dart'; import '../../data/models/backup_model.dart'; import '../../data/models/backup_config_model.dart'; import '../../data/repositories/backup_repository.dart'; import '../bloc/backup_bloc.dart'; /// Page Sauvegarde & Restauration - UnionFlow Mobile /// /// Page complète de gestion des sauvegardes avec création, restauration, /// planification et monitoring des sauvegardes système. class BackupPage extends StatefulWidget { const BackupPage({super.key}); @override State createState() => _BackupPageState(); } class _BackupPageState extends State with TickerProviderStateMixin { late TabController _tabController; bool _autoBackupEnabled = true; String _selectedFrequency = 'Quotidien'; String _selectedRetention = '30 jours'; List? _cachedBackups; BackupConfigModel? _cachedConfig; final List _frequencies = ['Horaire', 'Quotidien', 'Hebdomadaire']; final List _retentions = ['7 jours', '30 jours', '90 jours', '1 an']; @override void initState() { super.initState(); _tabController = TabController(length: 3, vsync: this); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return BlocProvider( create: (_) => sl() ..add(LoadBackups()) ..add(LoadBackupConfig()), child: BlocConsumer( listener: (context, state) { if (state is BackupsLoaded) { _cachedBackups = state.backups; } else if (state is BackupConfigLoaded) { _cachedConfig = state.config; } if (state is BackupSuccess) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), backgroundColor: const Color(0xFF00B894), behavior: SnackBarBehavior.floating, ), ); } else if (state is BackupError) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.error), backgroundColor: const Color(0xFFD63031), behavior: SnackBarBehavior.floating, ), ); } }, builder: (context, state) { return Scaffold( backgroundColor: ColorTokens.background, body: Column( children: [ _buildHeader(), _buildTabBar(), Expanded( child: TabBarView( controller: _tabController, children: [ _buildBackupsTab(state), _buildScheduleTab(), _buildRestoreTab(), ], ), ), ], ), ); }, ), ); } /// Header harmonisé Widget _buildHeader() { return Container( margin: const EdgeInsets.all(SpacingTokens.lg), padding: const EdgeInsets.all(SpacingTokens.xl), decoration: BoxDecoration( gradient: const LinearGradient( colors: ColorTokens.primaryGradient, begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(SpacingTokens.radiusXl), boxShadow: [ BoxShadow( color: ColorTokens.primary.withOpacity(0.3), blurRadius: 20, offset: const Offset(0, 8), ), ], ), child: Column( children: [ Row( children: [ Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(12), ), child: const Icon( Icons.backup, color: Colors.white, size: 24, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Sauvegarde & Restauration', style: TextStyle( fontSize: 20, fontWeight: FontWeight.bold, color: Colors.white, ), ), Text( 'Gestion des sauvegardes système', style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.8), ), ), ], ), ), Container( decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(8), ), child: IconButton( onPressed: () => _createBackupNow(), icon: const Icon( Icons.save, color: Colors.white, ), tooltip: 'Sauvegarde immédiate', ), ), ], ), const SizedBox(height: 16), Row( children: [ Expanded( child: _buildStatCard( 'Dernière sauvegarde', _lastBackupDisplay(), Icons.schedule, ), ), const SizedBox(width: 12), Expanded( child: _buildStatCard( 'Taille totale', _totalSizeDisplay(), Icons.storage, ), ), const SizedBox(width: 12), Expanded( child: _buildStatCard( 'Statut', _statusDisplay(), Icons.check_circle, ), ), ], ), ], ), ); } String _lastBackupDisplay() { if (_cachedConfig?.lastBackup != null) { final d = _cachedConfig!.lastBackup!; final diff = DateTime.now().difference(d); if (diff.inMinutes < 60) return '${diff.inMinutes} min'; if (diff.inHours < 24) return '${diff.inHours}h'; if (diff.inDays < 7) return '${diff.inDays} j'; return '${d.day}/${d.month}/${d.year}'; } if (_cachedBackups != null && _cachedBackups!.isNotEmpty) { final sorted = List.from(_cachedBackups!) ..sort((a, b) => (b.createdAt ?? DateTime(0)).compareTo(a.createdAt ?? DateTime(0))); final d = sorted.first.createdAt; if (d != null) { final diff = DateTime.now().difference(d); if (diff.inMinutes < 60) return '${diff.inMinutes} min'; if (diff.inHours < 24) return '${diff.inHours}h'; return '${diff.inDays} j'; } } return '—'; } String _totalSizeDisplay() { if (_cachedConfig?.totalSizeFormatted != null && _cachedConfig!.totalSizeFormatted!.isNotEmpty) { return _cachedConfig!.totalSizeFormatted!; } if (_cachedBackups != null && _cachedBackups!.isNotEmpty) { int total = 0; for (final b in _cachedBackups!) { total += b.sizeBytes ?? 0; } if (total >= 1024 * 1024 * 1024) return '${(total / (1024 * 1024 * 1024)).toStringAsFixed(1)} GB'; if (total >= 1024 * 1024) return '${(total / (1024 * 1024)).toStringAsFixed(1)} MB'; if (total >= 1024) return '${(total / 1024).toStringAsFixed(0)} KB'; return '$total B'; } return '0 B'; } String _statusDisplay() { if (_cachedBackups != null && _cachedBackups!.isNotEmpty) { final sorted = List.from(_cachedBackups!) ..sort((a, b) => (b.createdAt ?? DateTime(0)).compareTo(a.createdAt ?? DateTime(0))); final s = sorted.first.status; if (s == 'COMPLETED') return 'OK'; if (s == 'FAILED') return 'Erreur'; if (s == 'IN_PROGRESS') return 'En cours'; } return 'OK'; } /// Carte de statistique Widget _buildStatCard(String label, String value, IconData icon) { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.15), borderRadius: BorderRadius.circular(12), ), child: Column( children: [ Icon(icon, color: Colors.white, size: 20), const SizedBox(height: 4), Text( value, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white, ), ), Text( label, style: TextStyle( fontSize: 10, color: Colors.white.withOpacity(0.8), ), textAlign: TextAlign.center, ), ], ), ); } /// Barre d'onglets Widget _buildTabBar() { return Container( margin: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: TabBar( controller: _tabController, labelColor: const Color(0xFF6C5CE7), unselectedLabelColor: Colors.grey[600], indicatorColor: const Color(0xFF6C5CE7), indicatorWeight: 3, labelStyle: const TextStyle(fontWeight: FontWeight.w600, fontSize: 12), tabs: const [ Tab(icon: Icon(Icons.folder, size: 18), text: 'Sauvegardes'), Tab(icon: Icon(Icons.schedule, size: 18), text: 'Planification'), Tab(icon: Icon(Icons.restore, size: 18), text: 'Restauration'), ], ), ); } /// Onglet sauvegardes Widget _buildBackupsTab(BackupState state) { return SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( children: [ const SizedBox(height: 16), state is BackupLoading ? const Center(child: CircularProgressIndicator()) : _buildBackupsList(state is BackupsLoaded ? state.backups : (_cachedBackups ?? [])), const SizedBox(height: 80), ], ), ); } /// Liste des sauvegardes Widget _buildBackupsList(List backupsData) { final backups = backupsData.map((backup) => { 'id': backup.id?.toString() ?? '', 'name': backup.name ?? 'Sans nom', 'date': backup.createdAt?.toString() ?? '', 'size': backup.sizeFormatted ?? '0 B', 'type': backup.type ?? 'Manual', }).toList(); return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.folder, color: Color(0xFF6C5CE7), size: 20), const SizedBox(width: 8), Text( 'Sauvegardes disponibles', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.grey[800], ), ), ], ), const SizedBox(height: 16), ...backups.map((backup) => _buildBackupItem(backup)), ], ), ); } /// Élément de sauvegarde Widget _buildBackupItem(Map backup) { return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon( backup['type'] == 'Auto' ? Icons.schedule : Icons.touch_app, color: backup['type'] == 'Auto' ? Colors.blue : Colors.green, size: 20, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( backup['name']!, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFF1F2937), ), ), Text( '${backup['date']} • ${backup['size']}', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), ], ), ), PopupMenuButton( onSelected: (action) => _handleBackupAction(backup, action), itemBuilder: (context) => [ const PopupMenuItem(value: 'restore', child: Text('Restaurer')), const PopupMenuItem(value: 'download', child: Text('Télécharger')), const PopupMenuItem(value: 'delete', child: Text('Supprimer')), ], child: const Icon(Icons.more_vert, color: Colors.grey), ), ], ), ); } /// Onglet planification Widget _buildScheduleTab() { return SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( children: [ const SizedBox(height: 16), _buildScheduleSettings(), const SizedBox(height: 80), ], ), ); } /// Paramètres de planification Widget _buildScheduleSettings() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.schedule, color: Color(0xFF6C5CE7), size: 20), const SizedBox(width: 8), Text( 'Configuration automatique', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.grey[800], ), ), ], ), const SizedBox(height: 16), _buildSwitchSetting( 'Sauvegarde automatique', 'Activer les sauvegardes programmées', _autoBackupEnabled, (value) => setState(() => _autoBackupEnabled = value), ), const SizedBox(height: 12), _buildDropdownSetting( 'Fréquence', _selectedFrequency, _frequencies, (value) => setState(() => _selectedFrequency = value!), ), const SizedBox(height: 12), _buildDropdownSetting( 'Rétention', _selectedRetention, _retentions, (value) => setState(() => _selectedRetention = value!), ), ], ), ); } /// Onglet restauration Widget _buildRestoreTab() { return SingleChildScrollView( padding: const EdgeInsets.all(12), child: Column( children: [ const SizedBox(height: 16), _buildRestoreOptions(), const SizedBox(height: 80), ], ), ); } /// Options de restauration Widget _buildRestoreOptions() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.05), blurRadius: 10, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ const Icon(Icons.restore, color: Color(0xFF6C5CE7), size: 20), const SizedBox(width: 8), Text( 'Options de restauration', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.grey[800], ), ), ], ), const SizedBox(height: 16), _buildActionButton( 'Restaurer depuis un fichier', 'Importer une sauvegarde externe', Icons.file_upload, const Color(0xFF0984E3), () => _restoreFromFile(), ), const SizedBox(height: 12), _buildActionButton( 'Restauration sélective', 'Restaurer uniquement certaines données', Icons.checklist, const Color(0xFF00B894), () => _selectiveRestore(), ), const SizedBox(height: 12), _buildActionButton( 'Point de restauration', 'Créer un point de restauration avant modification', Icons.bookmark, const Color(0xFFE17055), () => _createRestorePoint(), ), ], ), ); } // Méthodes de construction des composants Widget _buildSwitchSetting(String title, String subtitle, bool value, Function(bool) onChanged) { return Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), Text(subtitle, style: TextStyle(fontSize: 12, color: Colors.grey[600])), ], ), ), Switch(value: value, onChanged: onChanged, activeColor: const Color(0xFF6C5CE7)), ], ); } Widget _buildDropdownSetting(String title, String value, List options, Function(String?) onChanged) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600)), const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[300]!), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: value, isExpanded: true, onChanged: onChanged, items: options.map((option) => DropdownMenuItem(value: option, child: Text(option))).toList(), ), ), ), ], ); } Widget _buildActionButton(String title, String subtitle, IconData icon, Color color, VoidCallback onTap) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: color.withOpacity(0.05), borderRadius: BorderRadius.circular(12), border: Border.all(color: color.withOpacity(0.1)), ), child: Row( children: [ Icon(icon, color: color, size: 20), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: color)), Text(subtitle, style: TextStyle(fontSize: 12, color: Colors.grey[600])), ], ), ), Icon(Icons.arrow_forward_ios, color: Colors.grey[400], size: 16), ], ), ), ); } // Méthodes d'action void _createBackupNow() { context.read().add(CreateBackup('Sauvegarde manuelle', description: 'Créée depuis l\'application mobile')); } void _handleBackupAction(Map backup, String action) { final backupId = backup['id']; if (backupId == null) return; if (action == 'restore') { context.read().add(RestoreBackup(backupId)); } else if (action == 'delete') { context.read().add(DeleteBackup(backupId)); } else if (action == 'download') { _downloadBackup(backupId); } else { _showSuccessSnackBar('Action "$action" exécutée'); } } Future _downloadBackup(String backupId) async { try { final repo = sl(); final b = await repo.getById(backupId); if (b.filePath != null && b.filePath!.isNotEmpty) { try { await Share.share( b.filePath!, subject: 'Sauvegarde ${b.name ?? backupId}', ); _showSuccessSnackBar('Partage du lien de téléchargement'); } catch (e, st) { AppLogger.error('BackupPage: partage échoué', error: e, stackTrace: st); _showSuccessSnackBar('Téléchargement: configurez l\'URL de téléchargement côté backend'); } } else { _showSuccessSnackBar('Téléchargement: l\'API ne fournit pas encore de lien (filePath).'); } } catch (e, st) { AppLogger.error('BackupPage: téléchargement échoué', error: e, stackTrace: st); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Impossible de récupérer la sauvegarde.'), backgroundColor: Color(0xFFD63031)), ); } } } Future _restoreFromFile() async { try { final result = await FilePicker.platform.pickFiles( type: FileType.any, allowMultiple: false, ); if (result == null || result.files.isEmpty) return; final path = result.files.single.path; if (path != null && path.isNotEmpty) { _showSuccessSnackBar('Fichier sélectionné. Restauration depuis fichier à brancher côté API.'); } else { _showSuccessSnackBar('Restauration depuis fichier à brancher côté API.'); } } catch (e, st) { AppLogger.error('BackupPage: restauration depuis fichier', error: e, stackTrace: st); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Sélection de fichier impossible.'), backgroundColor: Color(0xFFD63031)), ); } } } Future _selectiveRestore() async { try { final result = await FilePicker.platform.pickFiles( type: FileType.any, allowMultiple: true, ); if (result == null || result.files.isEmpty) { _showSuccessSnackBar('Restauration sélective: sélectionnez un ou plusieurs fichiers.'); return; } final paths = result.files.map((f) => f.path).whereType().toList(); if (paths.isNotEmpty) { _showSuccessSnackBar('Restauration sélective: ${paths.length} fichier(s) (API à brancher).'); } } catch (e, st) { AppLogger.error('BackupPage: restauration sélective', error: e, stackTrace: st); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Sélection impossible.'), backgroundColor: Color(0xFFD63031)), ); } } } void _createRestorePoint() { context.read().add(CreateBackup('Point de restauration', description: 'Point de restauration')); } void _showSuccessSnackBar(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(message), backgroundColor: const Color(0xFF00B894), behavior: SnackBarBehavior.floating), ); } }