240 lines
7.4 KiB
Dart
240 lines
7.4 KiB
Dart
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),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|