Files
fin65-mobile/lib/screens/sync_screen.dart
T

240 lines
7.4 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import '../models/pending_operation.dart';
import '../services/sync_service.dart';
class SyncScreen extends StatefulWidget {
const SyncScreen({
super.key,
required this.syncService,
});
final SyncService syncService;
@override
State<SyncScreen> createState() => _SyncScreenState();
}
class _SyncScreenState extends State<SyncScreen> {
final _date = DateFormat('dd/MM/yyyy HH:mm', 'fr_FR');
final _money = NumberFormat.currency(locale: 'fr_FR', symbol: 'MAD ', decimalDigits: 2);
bool _loading = true;
bool _syncing = false;
bool _isOnline = false;
String? _apiUrl;
String? _message;
DateTime? _lastSyncAt;
List<PendingOperation> _pending = [];
@override
void initState() {
super.initState();
_load();
}
Future<void> _load() async {
setState(() {
_loading = true;
_message = null;
});
try {
final apiUrl = await widget.syncService.getApiUrl();
final isOnline = await widget.syncService.testBackend();
final snapshot = await widget.syncService.loadLocalSnapshot(
isOnline: isOnline,
message: null,
);
final pending = await widget.syncService.loadPendingOperations();
if (!mounted) return;
setState(() {
_apiUrl = apiUrl;
_isOnline = isOnline;
_lastSyncAt = snapshot.lastSyncAt;
_pending = pending;
});
} catch (e) {
if (!mounted) return;
setState(() => _message = 'Erreur locale : $e');
} finally {
if (mounted) setState(() => _loading = false);
}
}
Future<void> _syncNow() async {
if (_syncing) return;
setState(() {
_syncing = true;
_message = 'Synchronisation en cours...';
});
try {
final snapshot = await widget.syncService.synchronize();
final pending = await widget.syncService.loadPendingOperations();
final apiUrl = await widget.syncService.getApiUrl();
if (!mounted) return;
setState(() {
_apiUrl = apiUrl;
_isOnline = snapshot.isOnline;
_lastSyncAt = snapshot.lastSyncAt;
_pending = pending;
_message = snapshot.isOnline
? 'Synchronisation terminée.'
: 'Backend indisponible. Les opérations restent en attente.';
});
} catch (e) {
if (!mounted) return;
setState(() => _message = 'Erreur de synchronisation : $e');
} finally {
if (mounted) setState(() => _syncing = false);
}
}
@override
Widget build(BuildContext context) {
final pendingCount = _pending.length;
return Scaffold(
appBar: AppBar(
title: const Text('Synchronisation'),
actions: [
IconButton(
onPressed: _loading || _syncing ? null : _load,
icon: const Icon(Icons.refresh),
),
],
),
bottomNavigationBar: SafeArea(
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 12),
child: FilledButton.icon(
onPressed: _syncing ? null : _syncNow,
icon: _syncing
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.sync),
label: Text(_syncing ? 'Synchronisation...' : 'Synchroniser maintenant'),
),
),
body: _loading
? const Center(child: CircularProgressIndicator())
: ListView(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 110),
children: [
Card(
child: ListTile(
leading: Icon(_isOnline ? Icons.cloud_done : Icons.cloud_off),
title: Text(_isOnline ? 'Backend joignable' : 'Backend indisponible'),
subtitle: Text(_apiUrl ?? 'URL API inconnue'),
),
),
Card(
child: ListTile(
leading: const Icon(Icons.schedule),
title: const Text('Dernière synchronisation réussie'),
subtitle: Text(
_lastSyncAt == null
? 'Aucune synchronisation réussie'
: _date.format(_lastSyncAt!.toLocal()),
),
),
),
Card(
child: ListTile(
leading: Icon(pendingCount == 0 ? Icons.check_circle : Icons.pending_actions),
title: Text('$pendingCount opération(s) en attente'),
subtitle: Text(
pendingCount == 0
? 'Aucune opération locale à envoyer.'
: 'Ces opérations seront envoyées quand le backend sera disponible.',
),
),
),
if (_message != null && _message!.isNotEmpty) ...[
const SizedBox(height: 8),
Card(
child: Padding(
padding: const EdgeInsets.all(12),
child: Text(_message!),
),
),
],
const SizedBox(height: 16),
Text('File d’attente', style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
if (_pending.isEmpty)
const Card(
child: ListTile(
leading: Icon(Icons.inbox),
title: Text('Aucune opération en attente'),
),
)
else
..._pending.map((operation) => _PendingOperationTile(
operation: operation,
date: _date,
money: _money,
)),
],
),
);
}
}
class _PendingOperationTile extends StatelessWidget {
const _PendingOperationTile({
required this.operation,
required this.date,
required this.money,
});
final PendingOperation operation;
final DateFormat date;
final NumberFormat money;
@override
Widget build(BuildContext context) {
final payload = operation.payload;
final label = payload['label']?.toString() ?? 'Opération mobile';
final amount = (payload['amount'] as num? ?? 0).toDouble();
final type = payload['type']?.toString() ?? operation.operationType;
final accountId = payload['account_id']?.toString() ?? '?';
final targetAccountId = payload['target_account_id']?.toString();
final typeLabel = switch (type) {
'expense' => 'Dépense',
'income' => 'Recette',
'transfer' => 'Transfert',
_ => type,
};
final accountLabel = type == 'transfer' && targetAccountId != null
? 'Compte $accountId → compte $targetAccountId'
: 'Compte $accountId';
return Card(
child: ListTile(
leading: const Icon(Icons.schedule_send),
title: Text(label),
subtitle: Text(
'$typeLabel • $accountLabel\nCréée le ${date.format(operation.createdAt.toLocal())}'
'${operation.error == null ? '' : '\nDernière erreur : ${operation.error}'}',
),
isThreeLine: true,
trailing: Text(
money.format(amount),
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
);
}
}