import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; import '../models/account.dart'; import '../models/category.dart'; import '../models/dashboard.dart'; import '../models/transaction_item.dart'; import '../services/sync_service.dart'; import 'quick_transaction_screen.dart'; import 'settings_screen.dart'; import 'sync_screen.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key, required this.syncService}); final SyncService syncService; @override State createState() => _HomeScreenState(); } class _HomeScreenState extends State { DashboardData? _dashboard; List _accounts = []; List _categories = []; List _transactions = []; int _pendingCount = 0; DateTime? _lastSyncAt; bool _isOnline = false; bool _loading = true; bool _syncing = false; String? _message; final _money = NumberFormat.currency(locale: 'fr_FR', symbol: 'MAD ', decimalDigits: 2); final _date = DateFormat('dd/MM/yyyy HH:mm', 'fr_FR'); @override void initState() { super.initState(); _bootstrap(); } Future _bootstrap() async { try { final local = await widget.syncService.loadLocalSnapshot( isOnline: false, message: 'Données locales chargées.', ); _applySnapshot(local, loading: false); await _synchronize(); } catch (e) { if (!mounted) return; setState(() { _loading = false; _message = 'Erreur locale : $e'; }); } } void _applySnapshot(SyncSnapshot snapshot, {bool? loading}) { if (!mounted) return; setState(() { _dashboard = snapshot.dashboard; _accounts = snapshot.accounts; _categories = snapshot.categories; _transactions = snapshot.transactions; _pendingCount = snapshot.pendingCount; _lastSyncAt = snapshot.lastSyncAt; _isOnline = snapshot.isOnline; _message = snapshot.message; if (loading != null) _loading = loading; }); } Future _synchronize() async { if (_syncing) return; setState(() { _syncing = true; _message = 'Synchronisation...'; }); try { final snapshot = await widget.syncService.synchronize(); if (!mounted) return; _applySnapshot(snapshot, loading: false); } catch (e) { if (!mounted) return; setState(() { _message = 'Erreur de synchronisation : $e'; _loading = false; }); } finally { if (mounted) { setState(() => _syncing = false); } } } Future _reloadLocalAfterSave() async { try { final snapshot = await widget.syncService.loadLocalSnapshot( isOnline: _isOnline, message: _isOnline ? 'Opération enregistrée.' : 'Opération en attente de synchronisation.', ); _applySnapshot(snapshot, loading: false); } catch (e) { if (!mounted) return; setState(() { _message = 'Opération enregistrée localement, mais rechargement écran échoué : $e'; _loading = false; }); } } Future _openQuickAdd() async { if (_accounts.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Crée d’abord au moins un compte dans la version web, puis synchronise.')), ); return; } await Navigator.of(context).push( MaterialPageRoute( builder: (_) => QuickTransactionScreen( syncService: widget.syncService, accounts: _accounts, categories: _categories, onSaved: _reloadLocalAfterSave, ), ), ); } Future _openSyncScreen() async { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => SyncScreen(syncService: widget.syncService), ), ); final snapshot = await widget.syncService.loadLocalSnapshot( isOnline: _isOnline, message: null, ); _applySnapshot(snapshot, loading: false); } Future _openSettings() async { await Navigator.of(context).push( MaterialPageRoute( builder: (_) => SettingsScreen(apiService: widget.syncService.apiService), ), ); await _synchronize(); } @override Widget build(BuildContext context) { final dashboard = _dashboard; return Scaffold( appBar: AppBar( title: const Text('FIN65'), actions: [ IconButton( tooltip: 'Page synchronisation', onPressed: _openSyncScreen, icon: Badge( isLabelVisible: _pendingCount > 0, label: Text('$_pendingCount'), child: const Icon(Icons.cloud_sync), ), ), IconButton( tooltip: 'Synchroniser', onPressed: _syncing ? null : _synchronize, icon: _syncing ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2), ) : const Icon(Icons.sync), ), IconButton(onPressed: _openSettings, icon: const Icon(Icons.settings)), ], ), floatingActionButton: FloatingActionButton.extended( onPressed: _openQuickAdd, icon: const Icon(Icons.add), label: const Text('Ajouter'), ), body: RefreshIndicator( onRefresh: _synchronize, child: _loading ? const Center(child: CircularProgressIndicator()) : ListView( padding: const EdgeInsets.all(16), children: [ _SyncBanner( isOnline: _isOnline, pendingCount: _pendingCount, lastSyncAt: _lastSyncAt, formatter: _date, message: _message, ), const SizedBox(height: 12), if (dashboard != null) ...[ _DashboardCards( dashboard: dashboard, money: _money, hasPending: _pendingCount > 0, ), const SizedBox(height: 16), ] else const _EmptyCacheCard(), _SectionTitle(title: 'Comptes', action: _pendingCount > 0 ? 'Solde estimé' : 'Solde app'), const SizedBox(height: 8), if (_accounts.isEmpty) const Card(child: ListTile(title: Text('Aucun compte en cache. Synchronise avec le backend.'))) else ..._accounts.map((a) => _AccountTile(account: a, money: _money)), const SizedBox(height: 16), _SectionTitle(title: 'Dernières transactions'), const SizedBox(height: 8), if (_transactions.isEmpty) const Card(child: ListTile(title: Text('Aucune transaction en cache.'))) else ..._transactions.map( (t) => _TransactionTile( transaction: t, accounts: _accounts, categories: _categories, money: _money, ), ), const SizedBox(height: 90), ], ), ), ); } } class _SyncBanner extends StatelessWidget { const _SyncBanner({ required this.isOnline, required this.pendingCount, required this.lastSyncAt, required this.formatter, this.message, }); final bool isOnline; final int pendingCount; final DateTime? lastSyncAt; final DateFormat formatter; final String? message; @override Widget build(BuildContext context) { final title = isOnline ? 'Connecté au backend' : 'Mode hors ligne'; final subtitleParts = []; if (lastSyncAt != null) { subtitleParts.add('Dernière synchro : ${formatter.format(lastSyncAt!.toLocal())}'); } else { subtitleParts.add('Aucune synchronisation réussie'); } if (pendingCount > 0) { subtitleParts.add('$pendingCount opération(s) en attente'); } if (message != null && message!.isNotEmpty) { subtitleParts.add(message!); } return Card( child: ListTile( leading: Icon(isOnline ? Icons.cloud_done : Icons.cloud_off), title: Text(title), subtitle: Text(subtitleParts.join('\n')), ), ); } } class _EmptyCacheCard extends StatelessWidget { const _EmptyCacheCard(); @override Widget build(BuildContext context) { return const Card( child: Padding( padding: EdgeInsets.all(16), child: Text( 'Aucune donnée locale. Lance le backend PC une première fois, configure l’URL API, puis synchronise.', ), ), ); } } class _DashboardCards extends StatelessWidget { const _DashboardCards({ required this.dashboard, required this.money, required this.hasPending, }); final DashboardData dashboard; final NumberFormat money; final bool hasPending; @override Widget build(BuildContext context) { final labelSuffix = hasPending ? ' estimé' : ''; return Column( children: [ _MetricCard( title: 'Disponible réel$labelSuffix', value: money.format(dashboard.availableAfterReserves), icon: Icons.account_balance_wallet, backgroundColor: const Color(0xFFE8F5E9), accentColor: const Color(0xFF2E7D32), ), const SizedBox(height: 8), _MetricCard( title: 'Balance prévisionnelle du mois', value: money.format(dashboard.projectedMonthBalance), icon: Icons.insights, backgroundColor: const Color(0xFFE3F2FD), accentColor: const Color(0xFF1565C0), ), const SizedBox(height: 8), Row( children: [ Expanded( child: _MetricCard( title: 'Solde total$labelSuffix', value: money.format(dashboard.totalBalance), icon: Icons.savings, compact: true, backgroundColor: const Color(0xFFF3E5F5), accentColor: const Color(0xFF6A1B9A), ), ), const SizedBox(width: 8), Expanded( child: _MetricCard( title: 'Balance réelle', value: money.format(dashboard.monthlyBalance), icon: Icons.trending_up, compact: true, backgroundColor: dashboard.monthlyBalance >= 0 ? const Color(0xFFE8F5E9) : const Color(0xFFFFEBEE), accentColor: dashboard.monthlyBalance >= 0 ? const Color(0xFF2E7D32) : const Color(0xFFC62828), ), ), ], ), const SizedBox(height: 8), Row( children: [ Expanded( child: _MetricCard( title: 'Revenus réels', value: money.format(dashboard.monthlyIncome), icon: Icons.arrow_downward, compact: true, backgroundColor: const Color(0xFFE8F5E9), accentColor: const Color(0xFF2E7D32), ), ), const SizedBox(width: 8), Expanded( child: _MetricCard( title: 'Dépenses réelles', value: money.format(dashboard.monthlyExpenses), icon: Icons.arrow_upward, compact: true, backgroundColor: const Color(0xFFFFEBEE), accentColor: const Color(0xFFC62828), ), ), ], ), const SizedBox(height: 8), _PlannedInfoCard(dashboard: dashboard, money: money), ], ); } } class _PlannedInfoCard extends StatelessWidget { const _PlannedInfoCard({ required this.dashboard, required this.money, }); final DashboardData dashboard; final NumberFormat money; @override Widget build(BuildContext context) { final rows = <_InfoRow>[ _InfoRow( 'Dépenses prévues restantes', dashboard.plannedExpensesTotal, Icons.event_busy, const Color(0xFFFFEBEE), const Color(0xFFC62828), ), _InfoRow( 'Revenus prévus échéancier', dashboard.scheduledIncomeRemainingTotal, Icons.event_available, const Color(0xFFE8F5E9), const Color(0xFF2E7D32), ), _InfoRow( 'Dépenses échéancier', dashboard.scheduledExpensesRemainingTotal, Icons.calendar_month, const Color(0xFFFFF3E0), const Color(0xFFEF6C00), ), _InfoRow( 'Transferts prévus', dashboard.plannedTransfersTotal, Icons.swap_horiz, const Color(0xFFE3F2FD), const Color(0xFF1565C0), ), _InfoRow( 'Provisions prévues', dashboard.plannedProvisionsTotal, Icons.inventory_2, const Color(0xFFF3E5F5), const Color(0xFF6A1B9A), ), ]; return Card( color: const Color(0xFFFAFAFA), elevation: 1, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(14), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 38, height: 38, decoration: BoxDecoration( color: const Color(0xFFE3F2FD), borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.event_note, color: Color(0xFF1565C0)), ), const SizedBox(width: 10), Expanded( child: Text( 'Prévisionnel du mois', style: Theme.of(context).textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w800, ), ), ), ], ), const SizedBox(height: 8), Text( 'Inclut les opérations mensuelles et l’échéancier ponctuel du backend.', style: Theme.of(context).textTheme.bodySmall, ), const Divider(height: 20), ...rows.map( (row) => Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 9), decoration: BoxDecoration( color: row.backgroundColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: row.accentColor.withOpacity(0.22)), ), child: Row( children: [ Icon(row.icon, size: 20, color: row.accentColor), const SizedBox(width: 10), Expanded( child: Text( row.label, style: TextStyle( color: row.accentColor, fontWeight: FontWeight.w700, ), ), ), Text( money.format(row.value), style: TextStyle( color: row.accentColor, fontWeight: FontWeight.w900, ), ), ], ), ), ), ], ), ), ); } } class _InfoRow { const _InfoRow( this.label, this.value, this.icon, this.backgroundColor, this.accentColor, ); final String label; final double value; final IconData icon; final Color backgroundColor; final Color accentColor; } class _MetricCard extends StatelessWidget { const _MetricCard({ required this.title, required this.value, required this.icon, required this.backgroundColor, required this.accentColor, this.compact = false, }); final String title; final String value; final IconData icon; final Color backgroundColor; final Color accentColor; final bool compact; @override Widget build(BuildContext context) { return Card( color: backgroundColor, elevation: 1, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: EdgeInsets.all(compact ? 12 : 16), child: Row( children: [ Container( width: compact ? 34 : 42, height: compact ? 34 : 42, decoration: BoxDecoration( color: accentColor.withOpacity(0.12), borderRadius: BorderRadius.circular(12), ), child: Icon(icon, size: compact ? 21 : 27, color: accentColor), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: Theme.of(context).textTheme.bodySmall?.copyWith( color: accentColor, fontWeight: FontWeight.w700, ), ), Text( value, style: (compact ? Theme.of(context).textTheme.titleMedium : Theme.of(context).textTheme.headlineSmall) ?.copyWith( color: accentColor, fontWeight: FontWeight.w900, ), ), ], ), ), ], ), ), ); } } class _SectionTitle extends StatelessWidget { const _SectionTitle({required this.title, this.action}); final String title; final String? action; @override Widget build(BuildContext context) { return Row( children: [ Text(title, style: Theme.of(context).textTheme.titleLarge), const Spacer(), if (action != null) Text(action!, style: Theme.of(context).textTheme.bodySmall), ], ); } } class _AccountTile extends StatelessWidget { const _AccountTile({required this.account, required this.money}); final Account account; final NumberFormat money; @override Widget build(BuildContext context) { return Card( child: ListTile( leading: const Icon(Icons.account_balance), title: Text(account.name), subtitle: Text(account.type), trailing: Text( money.format(account.appBalance), style: const TextStyle(fontWeight: FontWeight.bold), ), ), ); } } class _TransactionTile extends StatelessWidget { const _TransactionTile({ required this.transaction, required this.accounts, required this.categories, required this.money, }); final TransactionItem transaction; final List accounts; final List categories; final NumberFormat money; Account? _findAccount(int? id) { if (id == null) return null; for (final account in accounts) { if (account.id == id) return account; } return null; } Category? _findCategory(int? id) { if (id == null) return null; for (final category in categories) { if (category.id == id) return category; } return null; } @override Widget build(BuildContext context) { final account = _findAccount(transaction.accountId); final target = _findAccount(transaction.targetAccountId); final category = _findCategory(transaction.categoryId); final typeLabel = switch (transaction.type) { 'expense' => 'Dépense', 'income' => 'Recette', 'transfer' => 'Transfert', _ => transaction.type, }; final accountLabel = transaction.type == 'transfer' ? '${account?.name ?? transaction.accountId} → ${target?.name ?? transaction.targetAccountId}' : account?.name ?? 'Compte ${transaction.accountId}'; return Card( child: ListTile( leading: Icon(transaction.isPending ? Icons.schedule_send : Icons.receipt_long), title: Text(transaction.label), subtitle: Text( '${transaction.isPending ? 'EN ATTENTE • ' : ''}$typeLabel • $accountLabel${category == null ? '' : ' • ${category.name}'}', ), trailing: Text( money.format(transaction.amount), style: const TextStyle(fontWeight: FontWeight.bold), ), ), ); } }