ソースを参照

Sincronizzazione - revisione grafica

Fabio Dalle Ave 2 ヶ月 前
親
コミット
9ff026b7fb

+ 18 - 1
lib/business/statistics_service.dart

@@ -10,6 +10,8 @@ class StatisticsService {
   final ClientiDao _clientiDao;
   final AgentiDao _agentiDao;
 
+  final cacheClienti = <int, Cliente>{};
+
   StatisticsService(this._statisticheDao, this._articoliDao, this._clientiDao, this._agentiDao);
 
   Future<List<StatisticheVenditaData>> getStatsForAgent(int idAgente, {String? da, String? a}) async {
@@ -27,7 +29,9 @@ class StatisticsService {
     final stats = await getStatsForAgent(idAgente, da: da, a: a);
     final result = <String, double>{};
     for (final s in stats) {
-      final key = s.idCliente.toString();
+      final cliente = await getCliente(s.idCliente!);
+
+      final key = cliente != null ? '${cliente.codCliente} ${cliente.ragioneSociale}' : s.idCliente.toString();
       result[key] = (result[key] ?? 0) + (s.importo ?? 0);
     }
     return result;
@@ -59,4 +63,17 @@ class StatisticsService {
     }
     return result;
   }
+
+  Future<Cliente?> getCliente(int idCliente) async {
+    if (cacheClienti.containsKey(idCliente)) {
+      return Future.value(cacheClienti[idCliente]);
+    } else {
+      final cliente = await _clientiDao.getById(idCliente);
+      if (cliente != null) {
+        cacheClienti[idCliente] = cliente;
+        return cliente;
+      }
+    }
+    return Future.value(null);
+  }
 }

+ 1 - 1
lib/core/constants.dart

@@ -11,6 +11,6 @@ class AppConstants {
   static const String kAssetsDbPath = 'assets/db/Ristoris.db';
   static const String kDbVersionParam = 'DB_VERSION';
   static const String kUrlParam = 'URL';
-  static const String kLastUpdateParam = 'lastUpdate';
+  static const String kLastUpdateParam = 'LAST_UPDATE';
   static const String kCustomerName = 'Ristoris';
 }

+ 20 - 27
lib/database/daos/ordini_dao.dart

@@ -23,8 +23,7 @@ class OrdiniDao {
     return (db.select(db.ordini)
           ..where((t) => t.idCliente.equals(idCliente))
           ..orderBy([
-            (t) =>
-                OrderingTerm(expression: t.dataOrdine, mode: OrderingMode.desc),
+            (t) => OrderingTerm(expression: t.dataOrdine, mode: OrderingMode.desc),
           ])
           ..limit(200))
         .get();
@@ -34,19 +33,27 @@ class OrdiniDao {
     return (db.select(db.ordini)
           ..where((t) => t.idAgente.equals(idAgente))
           ..orderBy([
-            (t) =>
-                OrderingTerm(expression: t.dataOrdine, mode: OrderingMode.desc),
+            (t) => OrderingTerm(expression: t.dataOrdine, mode: OrderingMode.desc),
           ])
           ..limit(max))
         .get();
   }
 
+  Future<List<VOrdiniData>> customerOrdersWidthData(int idCliente, bool includeReferences) {
+    return (db.select(db.vOrdini)
+          ..where((t) => t.idCliente.equals(idCliente))
+          ..orderBy([
+            (t) => OrderingTerm(expression: t.dataOrdine, mode: OrderingMode.desc),
+          ])
+          ..limit(200))
+        .get();
+  }
+
   Future<List<VOrdiniData>> getOrdersView(int idAgente, {int limit = 200}) {
     return (db.select(db.vOrdini)
           ..where((t) => t.idAgente.equals(idAgente))
           ..orderBy([
-            (t) =>
-                OrderingTerm(expression: t.dataOrdine, mode: OrderingMode.desc),
+            (t) => OrderingTerm(expression: t.dataOrdine, mode: OrderingMode.desc),
           ])
           ..limit(limit))
         .get();
@@ -67,11 +74,7 @@ class OrdiniDao {
 
     for (final p in patterns) {
       query.where(
-        (t) =>
-            t.codCliente.like(p) |
-            t.ragioneSociale.like(p) |
-            t.filiale.like(p) |
-            CustomExpression<String>('CAST(numeroOrdine AS TEXT)').like(p),
+        (t) => t.codCliente.like(p) | t.ragioneSociale.like(p) | t.filiale.like(p) | CustomExpression<String>('CAST(numeroOrdine AS TEXT)').like(p),
       );
     }
 
@@ -102,10 +105,7 @@ class OrdiniDao {
 
     final rows = await (db.select(db.vOrdini)
           ..where(
-            (t) =>
-                t.idAgente.equals(idAgente) &
-                t.dataOrdine.isNotNull() &
-                t.dataOrdine.isBiggerOrEqualValue(dateStr),
+            (t) => t.idAgente.equals(idAgente) & t.dataOrdine.isNotNull() & t.dataOrdine.isBiggerOrEqualValue(dateStr),
           ))
         .get();
     return rows.fold<double>(0, (sum, r) => sum + (r.totale ?? 0));
@@ -153,18 +153,15 @@ class OrdiniDao {
   }
 
   Future<void> markAsSent(int idOrdine, int idAssegnato) {
-    return (db.update(db.ordini)..where((t) => t.idOrdine.equals(idOrdine)))
-        .write(OrdiniCompanion(stato: const Value('T')));
+    return (db.update(db.ordini)..where((t) => t.idOrdine.equals(idOrdine))).write(OrdiniCompanion(stato: const Value('T')));
   }
 
   Future<void> markAsSentStatus(int idOrdine) {
-    return (db.update(db.ordini)..where((t) => t.idOrdine.equals(idOrdine)))
-        .write(OrdiniCompanion(stato: const Value('T')));
+    return (db.update(db.ordini)..where((t) => t.idOrdine.equals(idOrdine))).write(OrdiniCompanion(stato: const Value('T')));
   }
 
   Future<int> insertOrder(Ordine order) {
-    return db.into(db.ordini).insert(
-        order.toCompanion(true).copyWith(idOrdine: const Value.absent()));
+    return db.into(db.ordini).insert(order.toCompanion(true).copyWith(idOrdine: const Value.absent()));
   }
 
   Future<bool> updateOrder(Ordine order) {
@@ -235,9 +232,7 @@ class OrdiniDao {
   Future<double> totalAmountItems(int idOrdine) async {
     final lines = await (db.select(db.righeOrdine)
           ..where(
-            (t) =>
-                t.idOrdine.equals(idOrdine) &
-                (t.rigaPromozione.equals(0) | t.rigaPromozione.isNull()),
+            (t) => t.idOrdine.equals(idOrdine) & (t.rigaPromozione.equals(0) | t.rigaPromozione.isNull()),
           ))
         .get();
     return lines.fold<double>(0.0, (sum, l) => sum + (l.totale ?? 0.0));
@@ -246,9 +241,7 @@ class OrdiniDao {
   Future<double> totalQtyItems(int idOrdine) async {
     final lines = await (db.select(db.righeOrdine)
           ..where(
-            (t) =>
-                t.idOrdine.equals(idOrdine) &
-                (t.rigaPromozione.equals(0) | t.rigaPromozione.isNull()),
+            (t) => t.idOrdine.equals(idOrdine) & (t.rigaPromozione.equals(0) | t.rigaPromozione.isNull()),
           ))
         .get();
     return lines.fold<double>(0.0, (sum, l) => sum + (l.quantita ?? 0.0));

+ 1 - 0
lib/providers/auth_provider.dart

@@ -135,6 +135,7 @@ class AuthNotifier extends Notifier<AuthState> {
       ref.invalidate(orderProvider);
       ref.invalidate(dashboardProvider);
       ref.invalidate(paymentsProvider);
+      ref.invalidate(syncProvider);
     }
   }
 

+ 331 - 48
lib/providers/sync_provider.dart

@@ -1,6 +1,7 @@
+import 'package:drift/drift.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:ristoris/core/constants.dart';
 import 'package:ristoris/database/database.dart';
-import 'package:ristoris/database/tables/agenti.dart';
 import '../services/api_service.dart';
 import '../services/sync/agente_sync_service.dart';
 import '../services/sync/cliente_sync_service.dart';
@@ -72,33 +73,201 @@ final referenceSyncServiceProvider = Provider<ReferenceSyncService>((ref) {
   );
 });
 
+enum SyncPhaseStatus { waiting, downloading, processing, completed }
+
+class SyncPhaseInfo {
+  final String name;
+  final SyncPhaseStatus status;
+  final int? currentRecord;
+  final int? totalRecords;
+
+  const SyncPhaseInfo({
+    required this.name,
+    this.status = SyncPhaseStatus.waiting,
+    this.currentRecord,
+    this.totalRecords,
+  });
+
+  SyncPhaseInfo copyWith({
+    SyncPhaseStatus? status,
+    int? currentRecord,
+    int? totalRecords,
+  }) {
+    return SyncPhaseInfo(
+      name: name,
+      status: status ?? this.status,
+      currentRecord: currentRecord ?? this.currentRecord,
+      totalRecords: totalRecords ?? this.totalRecords,
+    );
+  }
+
+  String get statusText {
+    switch (status) {
+      case SyncPhaseStatus.waiting:
+        return 'IN ATTESA';
+      case SyncPhaseStatus.downloading:
+        return 'IN SCARICAMENTO';
+      case SyncPhaseStatus.processing:
+        if (currentRecord != null && totalRecords != null) {
+          return 'Elaborazione $currentRecord di $totalRecords';
+        }
+        return 'IN ELABORAZIONE';
+      case SyncPhaseStatus.completed:
+        return 'COMPLETATO';
+    }
+  }
+}
+
+const List<String> syncPhaseNames = [
+  'Invio nuovi clienti',
+  'Invio nuove filiali',
+  'Invio nuovi ordini',
+  'Scaricamento tabelle',
+  'Scaricamento pagamenti',
+  'Scaricamento sconti',
+  'Scaricamento articoli',
+  'Scaricamento listini',
+  'Scaricamento listini articolo',
+  'Scaricamento promozioni',
+  'Scaricamento clienti',
+  'Scaricamento filiali',
+  'Scaricamento scadenze',
+  'Scaricamento listini cliente',
+  'Scaricamento sconti cliente',
+  'Scaricamento statistiche',
+];
+
 class SyncState {
   final bool isSyncing;
   final double progress;
   final String message;
   final String? error;
+  final List<SyncPhaseInfo> phases;
+  final DateTime? lastSyncTime;
+  final String? currentAgentCode;
+  final List<String> agentiCodici;
 
   const SyncState({
     this.isSyncing = false,
     this.progress = 0,
     this.message = '',
     this.error,
+    this.phases = const [],
+    this.lastSyncTime,
+    this.currentAgentCode,
+    this.agentiCodici = const [],
   });
 }
 
 class SyncNotifier extends Notifier<SyncState> {
+  DateTime _lastProgressUpdate = DateTime.fromMillisecondsSinceEpoch(0);
+
   @override
-  SyncState build() => const SyncState();
+  SyncState build() {
+    Future.microtask(() async {
+      try {
+        final parametriDao = ref.read(parametriDaoProvider);
+        final lastSyncTimeStr = await parametriDao.read(AppConstants.kLastUpdateParam);
+        if (lastSyncTimeStr != null) {
+          final lastSyncTime = DateTime.tryParse(lastSyncTimeStr);
+          if (lastSyncTime != null) {
+            state = SyncState(
+              phases: state.phases,
+              lastSyncTime: lastSyncTime,
+            );
+          }
+        }
+      } catch (_) {}
+    });
+    return const SyncState();
+  }
+
+  List<SyncPhaseInfo> _initPhases() {
+    return syncPhaseNames.map((name) => SyncPhaseInfo(name: name)).toList();
+  }
+
+  void _setPhase(
+    int index,
+    SyncPhaseStatus status, {
+    int? currentRecord,
+    int? totalRecords,
+  }) {
+    final phases = List<SyncPhaseInfo>.from(state.phases);
+    phases[index] = phases[index].copyWith(
+      status: status,
+      currentRecord: currentRecord,
+      totalRecords: totalRecords,
+    );
+    final progress = (index + 1) / syncPhaseNames.length;
+    state = SyncState(
+      isSyncing: true,
+      progress: progress,
+      phases: phases,
+      lastSyncTime: state.lastSyncTime,
+      currentAgentCode: state.currentAgentCode,
+      agentiCodici: state.agentiCodici,
+    );
+  }
+
+  void _resetAgentPhases() {
+    final phases = List<SyncPhaseInfo>.from(state.phases);
+    for (int i = 10; i <= 14; i++) {
+      phases[i] = SyncPhaseInfo(name: syncPhaseNames[i]);
+    }
+    state = SyncState(
+      isSyncing: true,
+      progress: state.progress,
+      phases: phases,
+      lastSyncTime: state.lastSyncTime,
+      currentAgentCode: state.currentAgentCode,
+      agentiCodici: state.agentiCodici,
+    );
+  }
+
+  void _setCurrentAgent(String codAgente) {
+    state = SyncState(
+      isSyncing: true,
+      progress: state.progress,
+      phases: state.phases,
+      lastSyncTime: state.lastSyncTime,
+      currentAgentCode: codAgente,
+      agentiCodici: state.agentiCodici,
+    );
+  }
+
+  void Function(int current, int total) _throttledProgress(int phaseIndex) {
+    return (int current, int total) {
+      final now = DateTime.now();
+      if (now.difference(_lastProgressUpdate).inMilliseconds >= 500) {
+        _lastProgressUpdate = now;
+        _setPhase(
+          phaseIndex,
+          SyncPhaseStatus.processing,
+          currentRecord: current,
+          totalRecords: total,
+        );
+      }
+    };
+  }
 
   Future<void> startFullSync(
     int idAgente,
     String codAgente, {
     bool forceFullSync = false,
   }) async {
-    state = const SyncState(
+    _lastProgressUpdate = DateTime.fromMillisecondsSinceEpoch(0);
+
+    final parametriDao = ref.read(parametriDaoProvider);
+    final lastSyncTimeStr = await parametriDao.read(AppConstants.kLastUpdateParam);
+    final lastSyncTime = lastSyncTimeStr != null ? DateTime.tryParse(lastSyncTimeStr) : null;
+
+    state = SyncState(
       isSyncing: true,
+      phases: _initPhases(),
       message: 'Avvio sincronizzazione...',
+      lastSyncTime: lastSyncTime,
     );
+
     try {
       final agentiDao = ref.read(agentiDaoProvider);
       final agente = await agentiDao.findByCode(codAgente);
@@ -107,77 +276,195 @@ class SyncNotifier extends Notifier<SyncState> {
         final token = await authService.loginRemote(codAgente, agente!.pwd!);
         ref.read(apiServiceProvider).token = token;
       }
-      _update('Autenticazione riuscita', 0.02);
 
-      final parametriDao = ref.read(parametriDaoProvider);
       final agenteSync = ref.read(agenteSyncServiceProvider);
       final ordineSync = ref.read(ordineSyncServiceProvider);
       final filialeSync = ref.read(filialeSyncServiceProvider);
       final clienteSync = ref.read(clienteSyncServiceProvider);
       final refSync = ref.read(referenceSyncServiceProvider);
 
-      final lastUpdateStr = forceFullSync ? '200001010000' : (await parametriDao.read('lastUpdate') ?? '200001010000');
+      final lastUpdateStr = forceFullSync ? '200001010000' : (await parametriDao.read(AppConstants.kLastUpdateParam) ?? '200001010000');
 
       await agenteSync.downloadAgenti(idAgente, codAgente, lastUpdateStr);
 
-      await clienteSync.uploadNewCustomers(idAgente, codAgente);
-      _update('Clienti inviati', 0.12);
+      // Phase 0: Invio nuovi clienti
+      _setPhase(0, SyncPhaseStatus.processing);
+      await clienteSync.uploadNewCustomers(
+        idAgente,
+        codAgente,
+        onProgress: _throttledProgress(0),
+      );
+      _setPhase(0, SyncPhaseStatus.completed);
+
+      // Phase 1: Invio nuove filiali
+      _setPhase(1, SyncPhaseStatus.processing);
+      await filialeSync.uploadNewDestinations(
+        idAgente,
+        codAgente,
+        onProgress: _throttledProgress(1),
+      );
+      _setPhase(1, SyncPhaseStatus.completed);
+
+      // Phase 2: Invio nuovi ordini
+      _setPhase(2, SyncPhaseStatus.processing);
+      await ordineSync.uploadNewOrders(
+        idAgente,
+        codAgente,
+        onProgress: _throttledProgress(2),
+      );
+      _setPhase(2, SyncPhaseStatus.completed);
 
-      // cicla per tutti gli agenti a db
-      List<Agente> agenti = await agentiDao.listAll();
+      // Phases 3-9: Reference downloads
+      _setPhase(3, SyncPhaseStatus.downloading);
+      await refSync.downloadTabelle(
+        lastUpdateStr,
+        onProgress: _throttledProgress(3),
+      );
+      _setPhase(3, SyncPhaseStatus.completed);
 
-      await filialeSync.uploadNewDestinations(idAgente, codAgente);
-      _update('Destinazioni inviate', 0.18);
-      await ordineSync.uploadNewOrders(idAgente, codAgente);
-      _update('Ordini inviati', 0.22);
+      _setPhase(4, SyncPhaseStatus.downloading);
+      await refSync.downloadPagamenti(
+        lastUpdateStr,
+        onProgress: _throttledProgress(4),
+      );
+      _setPhase(4, SyncPhaseStatus.completed);
 
-      await Future.wait([
-        refSync.downloadTabelle(lastUpdateStr),
-        refSync.downloadPagamenti(lastUpdateStr),
-        refSync.downloadSconti(lastUpdateStr),
-        refSync.downloadArticoli(lastUpdateStr),
-        refSync.downloadListini(lastUpdateStr),
-        refSync.downloadListiniArticolo(lastUpdateStr),
-        refSync.downloadPromozioni(lastUpdateStr),
-        refSync.downloadStatistiche(idAgente, codAgente, lastUpdateStr),
-      ]);
-      _update('Dati di riferimento scaricati', 0.60);
+      _setPhase(5, SyncPhaseStatus.downloading);
+      await refSync.downloadSconti(
+        lastUpdateStr,
+        onProgress: _throttledProgress(5),
+      );
+      _setPhase(5, SyncPhaseStatus.completed);
+
+      _setPhase(6, SyncPhaseStatus.downloading);
+      await refSync.downloadArticoli(
+        lastUpdateStr,
+        onProgress: _throttledProgress(6),
+      );
+      _setPhase(6, SyncPhaseStatus.completed);
 
-      for (int i = 0; i < agenti.length; i++) {
-        final agente = agenti[i];
+      _setPhase(7, SyncPhaseStatus.downloading);
+      await refSync.downloadListini(
+        lastUpdateStr,
+        onProgress: _throttledProgress(7),
+      );
+      _setPhase(7, SyncPhaseStatus.completed);
+
+      _setPhase(8, SyncPhaseStatus.downloading);
+      await refSync.downloadListiniArticolo(
+        lastUpdateStr,
+        onProgress: _throttledProgress(8),
+      );
+      _setPhase(8, SyncPhaseStatus.completed);
+
+      _setPhase(9, SyncPhaseStatus.downloading);
+      await refSync.downloadPromozioni(
+        lastUpdateStr,
+        onProgress: _throttledProgress(9),
+      );
+      _setPhase(9, SyncPhaseStatus.completed);
+
+      // Per-agent downloads
+      final List<Agente> agenti = await agentiDao.listAll();
+      final agentiCodici = agenti.map((a) => a.codAgente!).toList();
+
+      state = SyncState(
+        isSyncing: true,
+        progress: state.progress,
+        phases: state.phases,
+        lastSyncTime: state.lastSyncTime,
+        agentiCodici: agentiCodici,
+      );
 
-        _update('Aggiornamento per codice ${agente.codAgente} ', 0.16);
+      for (final a in agenti) {
+        _resetAgentPhases();
+        _setCurrentAgent(a.codAgente!);
 
-        await clienteSync.downloadCustomers(agente.idAgente, agente.codAgente!, lastUpdateStr);
-        _update('(${agente.codAgente}) Clienti attivi scaricati', 0.16);
+        // Phase 10: Scaricamento clienti
+        _setPhase(10, SyncPhaseStatus.downloading);
+        await clienteSync.downloadCustomers(
+          a.idAgente,
+          a.codAgente!,
+          lastUpdateStr,
+          onProgress: _throttledProgress(10),
+        );
+        _setPhase(10, SyncPhaseStatus.completed);
 
+        // Phase 11: Scaricamento filiali
+        _setPhase(11, SyncPhaseStatus.downloading);
         await filialeSync.downloadDestinations(
-          agente.idAgente,
-          agente.codAgente!,
+          a.idAgente,
+          a.codAgente!,
+          lastUpdateStr,
+          onProgress: _throttledProgress(11),
+        );
+        _setPhase(11, SyncPhaseStatus.completed);
+
+        // Phase 12: Scaricamento scadenze
+        _setPhase(12, SyncPhaseStatus.downloading);
+        await refSync.downloadScadenze(
+          a.idAgente,
+          a.codAgente!,
           lastUpdateStr,
+          onProgress: _throttledProgress(12),
         );
-        _update('(${agente.codAgente}) Destinazioni scaricate', 0.20);
-
-        await Future.wait([
-          refSync.downloadScadenze(agente.idAgente, agente.codAgente!, lastUpdateStr),
-          refSync.downloadListiniCliente(agente.idAgente, agente.codAgente!, lastUpdateStr),
-          refSync.downloadScontiCliente(agente.idAgente, agente.codAgente!, lastUpdateStr),
-          ordineSync.downloadRemoteOrders(agente.idAgente, agente.codAgente!, lastUpdateStr),
-        ]);
+        _setPhase(12, SyncPhaseStatus.completed);
+
+        // Phase 13: Scaricamento listini cliente
+        _setPhase(13, SyncPhaseStatus.downloading);
+        await refSync.downloadListiniCliente(
+          a.idAgente,
+          a.codAgente!,
+          lastUpdateStr,
+          onProgress: _throttledProgress(13),
+        );
+        _setPhase(13, SyncPhaseStatus.completed);
+
+        // Phase 14: Scaricamento sconti cliente
+        _setPhase(14, SyncPhaseStatus.downloading);
+        await refSync.downloadScontiCliente(
+          a.idAgente,
+          a.codAgente!,
+          lastUpdateStr,
+          onProgress: _throttledProgress(14),
+        );
+        _setPhase(14, SyncPhaseStatus.completed);
       }
 
-      _update('Dati per agente scaricati', 0.92);
+      // Phase 15: Scaricamento statistiche
+      _setPhase(15, SyncPhaseStatus.downloading);
+      await refSync.downloadStatistiche(
+        idAgente,
+        codAgente,
+        lastUpdateStr,
+        onProgress: _throttledProgress(15),
+      );
+      _setPhase(15, SyncPhaseStatus.completed);
 
+      // Download ordini remoti (no visual phase)
+      for (final a in agenti) {
+        await ordineSync.downloadRemoteOrders(
+          a.idAgente,
+          a.codAgente!,
+          lastUpdateStr,
+        );
+      }
+
+      // Download parametro
       await refSync.downloadParametro('PALLET_PERC');
-      _update('Parametri verificati', 0.98);
 
+      // Save timestamps
       final now = DateTime.now();
       final newLastUpdate = '${now.year}${_pad(now.month)}${_pad(now.day)}${_pad(now.hour)}${_pad(now.minute)}';
-      await parametriDao.set('lastUpdate', newLastUpdate);
+      await parametriDao.set(AppConstants.kLastUpdateParam, newLastUpdate);
+      // await parametriDao.set(AppConstants.kLastUpdateParam, now.toIso8601String());
 
-      state = const SyncState(
+      state = SyncState(
         message: 'Sincronizzazione completata!',
         progress: 1.0,
+        phases: state.phases,
+        lastSyncTime: now,
+        agentiCodici: state.agentiCodici,
       );
 
       ref.invalidate(articleSearchProvider);
@@ -202,10 +489,6 @@ class SyncNotifier extends Notifier<SyncState> {
   }
 
   String _pad(int n) => n.toString().padLeft(2, '0');
-
-  void _update(String msg, double progress) {
-    state = SyncState(isSyncing: true, message: msg, progress: progress);
-  }
 }
 
 final syncProvider = NotifierProvider<SyncNotifier, SyncState>(

+ 7 - 2
lib/services/sync/agente_sync_service.dart

@@ -25,15 +25,20 @@ class AgenteSyncService {
   Future<int> downloadAgenti(
     int idAgente,
     String codAgente,
-    String lastUpdate,
-  ) async {
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList(
         '/rest/agente/updates/$codAgente/$lastUpdate',
       );
       final db = _agentiDao.db;
+      final total = data.length;
+      int count = 0;
       await db.transaction(() async {
         for (final json in data) {
+          count++;
+          onProgress?.call(count, total);
           if (codAgente == json['codAgente'] as String) {
             await setNuovoIdAgente(codAgente, json['idAgente'] as int? ?? 0);
           }

+ 15 - 6
lib/services/sync/cliente_sync_service.dart

@@ -13,11 +13,18 @@ class ClienteSyncService {
 
   ClienteSyncService(this._api, this._clientiDao, this._clientiAgenteDao);
 
-  Future<int> uploadNewCustomers(int idAgente, String codAgente) async {
+  Future<int> uploadNewCustomers(
+    int idAgente,
+    String codAgente, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     final newClients = await _clientiDao.getNewCustomers(idAgente);
     int sent = 0;
-    for (final c in newClients) {
+    final total = newClients.length;
+    for (int i = 0; i < newClients.length; i++) {
+      onProgress?.call(i + 1, total);
       try {
+        final c = newClients[i];
         final dto = _toDto(c);
         final resp = await _api.post('/rest/clienti/$codAgente', dto.toJson());
         if (resp['status'] == 0) {
@@ -35,8 +42,9 @@ class ClienteSyncService {
   Future<int> downloadCustomers(
     int idAgente,
     String codAgente,
-    String lastUpdate,
-  ) async {
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList(
         '/rest/clienti/updates/$codAgente/$lastUpdate',
@@ -46,9 +54,12 @@ class ClienteSyncService {
       final cacheCode = <String, Cliente?>{};
       final cacheAssoc = <String, bool>{};
 
+      final total = data.length;
       int count = 0;
       await db.transaction(() async {
         for (final json in data) {
+          count++;
+          onProgress?.call(count, total);
           final dto = SyncCustomerDto.fromJson(json as Map<String, dynamic>);
 
           Cliente? existing;
@@ -175,8 +186,6 @@ class ClienteSyncService {
             }
             cacheAssoc[assocKey] = true;
           }
-
-          count++;
         }
       });
       return count;

+ 15 - 5
lib/services/sync/filiale_sync_service.dart

@@ -12,11 +12,18 @@ class FilialeSyncService {
 
   FilialeSyncService(this._api, this._dao, this._clientiDao);
 
-  Future<int> uploadNewDestinations(int idAgente, String codAgente) async {
+  Future<int> uploadNewDestinations(
+    int idAgente,
+    String codAgente, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     final dests = await _dao.getNewDestinations();
     int sent = 0;
-    for (final d in dests) {
+    final total = dests.length;
+    for (int i = 0; i < dests.length; i++) {
+      onProgress?.call(i + 1, total);
       try {
+        final d = dests[i];
         final dto = SyncFilialeDto(
           idFiliale: d.idFiliale,
           idCliente: d.idCliente,
@@ -49,8 +56,9 @@ class FilialeSyncService {
   Future<int> downloadDestinations(
     int idAgente,
     String codAgente,
-    String lastUpdate,
-  ) async {
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList(
         '/rest/filiali/updates/$codAgente/$lastUpdate',
@@ -59,9 +67,12 @@ class FilialeSyncService {
       final cacheRemoteId = <int, Filiale?>{};
       final cacheCode = <String, Filiale?>{};
       final cacheCliente = <String, Cliente?>{};
+      final total = data.length;
       int count = 0;
       await db.transaction(() async {
         for (final json in data) {
+          count++;
+          onProgress?.call(count, total);
           final dto = SyncFilialeDto.fromJson(json as Map<String, dynamic>);
 
           Filiale? existing;
@@ -144,7 +155,6 @@ class FilialeSyncService {
                   ),
                 );
           }
-          count++;
         }
       });
       return count;

+ 15 - 6
lib/services/sync/ordine_sync_service.dart

@@ -25,11 +25,18 @@ class OrdineSyncService {
     this._agentiDao,
   );
 
-  Future<int> uploadNewOrders(int idAgente, String codAgente) async {
+  Future<int> uploadNewOrders(
+    int idAgente,
+    String codAgente, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     final orders = await _ordiniDao.getNewOrders();
     int sent = 0;
-    for (final o in orders) {
+    final total = orders.length;
+    for (int i = 0; i < orders.length; i++) {
+      onProgress?.call(i + 1, total);
       try {
+        final o = orders[i];
         final lines = await _ordiniDao.getOrderLines(o.idOrdine);
         final dto = SyncOrderDto(
           idOrdine: o.idOrdine,
@@ -87,8 +94,9 @@ class OrdineSyncService {
   Future<int> downloadRemoteOrders(
     int idAgente,
     String codAgente,
-    String lastUpdate,
-  ) async {
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList(
         '/rest/ordini/updates/$codAgente/$lastUpdate',
@@ -98,12 +106,15 @@ class OrdineSyncService {
       final cacheAgente = <String, Agente>{};
       final cacheFilialeById = <int, int?>{};
       final cacheFilialeByRemoteId = <int, int?>{};
+      final total = data.length;
       int count = 0;
 
       Agente? agente = null;
 
       await db.transaction(() async {
         for (final json in data) {
+          count++;
+          onProgress?.call(count, total);
           final idOrdine = json['idOrdine'] as int?;
           final idOrdineAgente = json['idOrdineAgente'] as int?;
           final numeroOrdineSede = json['numeroOrdineSede'] as int?;
@@ -239,8 +250,6 @@ class OrdineSyncService {
               }
             });
           }
-
-          count++;
         }
       });
       return count;

+ 85 - 24
lib/services/sync/reference_sync_service.dart

@@ -40,12 +40,18 @@ class ReferenceSyncService {
     this._clientiDao,
   );
 
-  Future<int> downloadTabelle(String lastUpdate) async {
+  Future<int> downloadTabelle(
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList('/rest/tabelle/updates/$lastUpdate');
       final db = _tabelleDao.db;
+      final total = data.length;
       await db.batch((batch) {
-        for (final json in data) {
+        for (int i = 0; i < data.length; i++) {
+          final json = data[i];
+          onProgress?.call(i + 1, total);
           batch.insert(
             db.tabelle,
             Tabella(
@@ -75,12 +81,18 @@ class ReferenceSyncService {
     }
   }
 
-  Future<int> downloadPagamenti(String lastUpdate) async {
+  Future<int> downloadPagamenti(
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList('/rest/pagamenti/updates/$lastUpdate');
       final db = _pagamentiDao.db;
+      final total = data.length;
       await db.batch((batch) {
-        for (final json in data) {
+        for (int i = 0; i < data.length; i++) {
+          final json = data[i];
+          onProgress?.call(i + 1, total);
           batch.insert(
             db.pagamenti,
             Pagamento(
@@ -100,12 +112,18 @@ class ReferenceSyncService {
     }
   }
 
-  Future<int> downloadSconti(String lastUpdate) async {
+  Future<int> downloadSconti(
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList('/rest/sconti/updates/$lastUpdate');
       final db = _scontiDao.db;
+      final total = data.length;
       await db.batch((batch) {
-        for (final json in data) {
+        for (int i = 0; i < data.length; i++) {
+          final json = data[i];
+          onProgress?.call(i + 1, total);
           batch.insert(
             db.sconti,
             Sconto(
@@ -128,12 +146,18 @@ class ReferenceSyncService {
     }
   }
 
-  Future<int> downloadArticoli(String lastUpdate) async {
+  Future<int> downloadArticoli(
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList('/rest/articoli/updates/$lastUpdate');
       final db = _articoliDao.db;
+      final total = data.length;
       await db.batch((batch) {
-        for (final json in data) {
+        for (int i = 0; i < data.length; i++) {
+          final json = data[i];
+          onProgress?.call(i + 1, total);
           batch.insert(
             db.articoli,
             Articolo(
@@ -177,12 +201,18 @@ class ReferenceSyncService {
     }
   }
 
-  Future<int> downloadListini(String lastUpdate) async {
+  Future<int> downloadListini(
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList('/rest/listini/updates/$lastUpdate');
       final db = _listiniDao.db;
+      final total = data.length;
       await db.batch((batch) {
-        for (final json in data) {
+        for (int i = 0; i < data.length; i++) {
+          final json = data[i];
+          onProgress?.call(i + 1, total);
           batch.insert(
             db.listini,
             Listino(
@@ -205,14 +235,20 @@ class ReferenceSyncService {
     }
   }
 
-  Future<int> downloadListiniArticolo(String lastUpdate) async {
+  Future<int> downloadListiniArticolo(
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList(
         '/rest/listinoArticoli/updates/$lastUpdate',
       );
       final db = _listiniDao.db;
+      final total = data.length;
       await db.batch((batch) {
-        for (final json in data) {
+        for (int i = 0; i < data.length; i++) {
+          final json = data[i];
+          onProgress?.call(i + 1, total);
           final articolo = json['articolo'] as Map<String, dynamic>?;
           final listino = json['listino'] as Map<String, dynamic>?;
           final sconto = json['sconto'] as Map<String, dynamic>?;
@@ -243,8 +279,9 @@ class ReferenceSyncService {
   Future<int> downloadListiniCliente(
     int idAgente,
     String codAgente,
-    String lastUpdate,
-  ) async {
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList(
         '/rest/listinoCliente/updates/$codAgente/$lastUpdate',
@@ -252,7 +289,10 @@ class ReferenceSyncService {
       final db = _listiniDao.db;
       final cacheCliente = <int, int?>{};
       final items = <ListinoCliente>[];
-      for (final json in data) {
+      final total = data.length;
+      for (int i = 0; i < data.length; i++) {
+        onProgress?.call(i + 1, total);
+        final json = data[i];
         final articolo = json['articolo'] as Map<String, dynamic>?;
         final listino = json['listino'] as Map<String, dynamic>?;
         final sconto = json['sconto'] as Map<String, dynamic>?;
@@ -301,15 +341,19 @@ class ReferenceSyncService {
   Future<int> downloadScontiCliente(
     int idAgente,
     String codAgente,
-    String lastUpdate,
-  ) async {
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList(
         '/rest/scontocliente/updates/$codAgente/$lastUpdate',
       );
       final db = _scontiDao.db;
+      final total = data.length;
       await db.batch((batch) {
-        for (final json in data) {
+        for (int i = 0; i < data.length; i++) {
+          final json = data[i];
+          onProgress?.call(i + 1, total);
           batch.insert(
             db.scontiCliente,
             ScontoCliente(
@@ -333,17 +377,24 @@ class ReferenceSyncService {
     }
   }
 
-  Future<int> downloadPromozioni(String lastUpdate) async {
+  Future<int> downloadPromozioni(
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     try {
       final data = await _api.getList('/rest/promozioni/updates/$lastUpdate');
       final db = _promozioniDao.db;
       int count = 0;
+      final total = data.length;
       await db.transaction(() async {
         await db.delete(db.promoOmaggi).go();
         await db.delete(db.promoListini).go();
         await db.delete(db.promoArticoli).go();
         await db.delete(db.promozioni).go();
-        for (final json in data) {
+        for (int i = 0; i < data.length; i++) {
+          count++;
+          onProgress?.call(count, total);
+          final json = data[i];
           final dto = SyncPromoDto.fromJson(json as Map<String, dynamic>);
           await db.into(db.promozioni).insert(
                 Promozione(
@@ -404,8 +455,9 @@ class ReferenceSyncService {
   Future<int> downloadScadenze(
     int idAgente,
     String codAgente,
-    String lastUpdate,
-  ) async {
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     Map<int, Cliente> cacheCliente = {};
 
     try {
@@ -413,10 +465,14 @@ class ReferenceSyncService {
         '/rest/scadenze/updates/$codAgente/$lastUpdate',
       );
       final db = _scadenzeDao.db;
+      final total = data.length;
+      int count = 0;
       await db.transaction(() async {
         //await _scadenzeDao.markAllAsDeleted(idAgente);
         await db.batch((batch) async {
           for (final json in data) {
+            count++;
+            onProgress?.call(count, total);
             // cerca il cliente nella cache usando idClienteRemoto
             final idClienteRemoto = json['idCliente'] as int?;
             Cliente? cliente = cacheCliente[idClienteRemoto];
@@ -471,8 +527,9 @@ class ReferenceSyncService {
   Future<int> downloadStatistiche(
     int idAgente,
     String codAgente,
-    String lastUpdate,
-  ) async {
+    String lastUpdate, {
+    void Function(int current, int total)? onProgress,
+  }) async {
     Map<int, Cliente> cacheCliente = {};
 
     try {
@@ -480,9 +537,13 @@ class ReferenceSyncService {
         '/rest/statistiche/updates/$codAgente/$lastUpdate',
       );
       final db = _statisticheDao.db;
+      final total = data.length;
+      int count = 0;
       await db.transaction(() async {
         await db.batch((batch) async {
           for (final json in data) {
+            count++;
+            onProgress?.call(count, total);
             final idAgente = json['idAgente'] as int?;
 
             // cerca il cliente nella cache usando idClienteRemoto

+ 9 - 227
lib/ui/pages/customers/customer_detail_page.dart

@@ -2,6 +2,10 @@ import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'package:intl/intl.dart';
+import 'package:ristoris/ui/pages/customers/tabs/destinations_tab.dart';
+import 'package:ristoris/ui/pages/customers/tabs/info_tab.dart';
+import 'package:ristoris/ui/pages/customers/tabs/orders_tab.dart';
+import 'package:ristoris/ui/pages/customers/tabs/payments_tab.dart';
 import '../../../database/database.dart';
 import '../../../providers/auth_provider.dart';
 import '../../../providers/database_provider.dart';
@@ -51,11 +55,11 @@ class CustomerDetailPage extends ConsumerWidget {
               }
               return TabBarView(
                 children: [
-                  _InfoTab(cliente: cliente, theme: theme),
-                  _OrdersTab(idCliente: id, theme: theme),
-                  _DestinationsTab(idCliente: id, theme: theme),
-                  _PaymentsTab(
-                    idCliente: cliente.idClienteRemoto ?? 0,
+                  InfoTab(cliente: cliente, theme: theme),
+                  OrdersTab(idCliente: id, theme: theme),
+                  DestinationsTab(idCliente: id, theme: theme),
+                  PaymentsTab(
+                    idCliente: id ?? 0,
                     theme: theme,
                   ),
                 ],
@@ -67,225 +71,3 @@ class CustomerDetailPage extends ConsumerWidget {
     );
   }
 }
-
-class _InfoTab extends StatelessWidget {
-  final Cliente cliente;
-  final ThemeData theme;
-  const _InfoTab({required this.cliente, required this.theme});
-
-  @override
-  Widget build(BuildContext context) {
-    return SingleChildScrollView(
-      padding: const EdgeInsets.all(16),
-      child: Column(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          _infoRow('Codice', cliente.codCliente ?? '-'),
-          _infoRow('Ragione Sociale', cliente.ragioneSociale ?? '-'),
-          _infoRow('Indirizzo', cliente.indirizzo ?? '-'),
-          _infoRow(
-            'Città',
-            '${cliente.citta ?? ""} ${cliente.provincia ?? ""} ${cliente.cap ?? ""}',
-          ),
-          _infoRow('Telefono', cliente.telefono ?? '-'),
-          _infoRow('Cellulare', cliente.cellulare ?? '-'),
-          _infoRow('Email', cliente.email ?? '-'),
-          _infoRow('Partita IVA', cliente.partitaIva ?? '-'),
-          _infoRow('Codice Fiscale', cliente.codiceFiscale ?? '-'),
-          if (cliente.sdi != null && cliente.sdi!.isNotEmpty)
-            _infoRow('SDI', cliente.sdi!),
-          if (cliente.pec != null && cliente.pec!.isNotEmpty)
-            _infoRow('PEC', cliente.pec!),
-          if (cliente.note != null && cliente.note!.isNotEmpty)
-            _infoRow('Note', cliente.note!),
-          if (cliente.codBlocco != null && cliente.codBlocco!.isNotEmpty)
-            Card(
-              color: Colors.red.shade50,
-              child: Padding(
-                padding: const EdgeInsets.all(12),
-                child: Row(
-                  children: [
-                    Icon(Icons.block, color: Colors.red),
-                    const SizedBox(width: 8),
-                    Text('Blocco: ${cliente.codBlocco}'),
-                  ],
-                ),
-              ),
-            ),
-        ],
-      ),
-    );
-  }
-
-  Widget _infoRow(String label, String value) {
-    return Padding(
-      padding: const EdgeInsets.symmetric(vertical: 4),
-      child: Row(
-        crossAxisAlignment: CrossAxisAlignment.start,
-        children: [
-          SizedBox(
-            width: 120,
-            child: Text(
-              label,
-              style: theme.textTheme.bodySmall?.copyWith(
-                color: theme.colorScheme.outline,
-              ),
-            ),
-          ),
-          Expanded(child: Text(value)),
-        ],
-      ),
-    );
-  }
-}
-
-class _OrdersTab extends ConsumerWidget {
-  final int idCliente;
-  final ThemeData theme;
-  const _OrdersTab({required this.idCliente, required this.theme});
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final ordiniDao = ref.read(ordiniDaoProvider);
-    return Scaffold(
-      body: SafeArea(
-        child: FutureBuilder(
-          future: ordiniDao.customerOrders(idCliente, false),
-          builder: (_, snap) {
-            if (!snap.hasData) {
-              return const Center(child: CircularProgressIndicator());
-            }
-            final orders = snap.data!;
-            if (orders.isEmpty)
-              return const Center(child: Text('Nessun ordine'));
-            return ListView.builder(
-              itemCount: orders.length,
-              itemBuilder: (_, i) => ListTile(
-                title: Text('Ordine #${orders[i].numeroOrdine ?? ""}'),
-                subtitle: Text(orders[i].dataOrdine ?? ''),
-                onTap: () =>
-                    context.push('/dashboard/orders/${orders[i].idOrdine}'),
-              ),
-            );
-          },
-        ),
-      ),
-      floatingActionButton: FloatingActionButton(
-        heroTag: 'addOrder',
-        child: const Icon(Icons.add),
-        onPressed: () =>
-            context.push('/dashboard/orders/new?cliente=$idCliente'),
-      ),
-    );
-  }
-}
-
-class _DestinationsTab extends ConsumerWidget {
-  final int idCliente;
-  final ThemeData theme;
-  const _DestinationsTab({required this.idCliente, required this.theme});
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final destsAsync = ref.watch(customerDestinationsProvider(idCliente));
-    return destsAsync.when(
-      data: (dests) {
-        if (dests.isEmpty) {
-          return const Center(child: Text('Nessuna destinazione'));
-        }
-        return ListView.builder(
-          itemCount: dests.length,
-          itemBuilder: (_, i) => ListTile(
-            leading: const Icon(Icons.location_on_outlined),
-            title: Text(dests[i].descrizione ?? ''),
-            subtitle: Text(
-              '${dests[i].indirizzo ?? ""} ${dests[i].citta ?? ""}',
-            ),
-          ),
-        );
-      },
-      loading: () => const Center(child: CircularProgressIndicator()),
-      error: (e, _) => Center(child: Text('Errore: $e')),
-    );
-  }
-}
-
-class _PaymentsTab extends ConsumerWidget {
-  final int idCliente;
-  final ThemeData theme;
-  const _PaymentsTab({required this.idCliente, required this.theme});
-
-  @override
-  Widget build(BuildContext context, WidgetRef ref) {
-    final paysAsync = ref.watch(customerPaymentsProvider(idCliente));
-    return paysAsync.when(
-      data: (pays) {
-        if (pays.isEmpty) return const Center(child: Text('Nessuna scadenza'));
-        return ListView.builder(
-          padding: const EdgeInsets.all(8),
-          itemCount: pays.length,
-          itemBuilder: (_, i) {
-            final p = pays[i];
-            return Card(
-              margin: const EdgeInsets.symmetric(vertical: 4),
-              child: Padding(
-                padding: const EdgeInsets.all(12),
-                child: Column(
-                  crossAxisAlignment: CrossAxisAlignment.start,
-                  children: [
-                    _row('Fattura N.', p.numDoc ?? '-'),
-                    _row('Data fatt.', _formatDate(p.dataDoc)),
-                    _row('Data scad', _formatDate(p.dataScadenza)),
-                    _row('Totale fattura', _formatAmount(p.importoFattura)),
-                    _row('Totale scad.', _formatAmount(p.importoScadenza)),
-                    _row('Saldo', _formatAmount(p.saldo)),
-                  ],
-                ),
-              ),
-            );
-          },
-        );
-      },
-      loading: () => const Center(child: CircularProgressIndicator()),
-      error: (e, _) => Center(child: Text('Errore: $e')),
-    );
-  }
-
-  String _formatDate(String? dateStr) {
-    if (dateStr == null) return '-';
-    try {
-      final dt = DateTime.parse(dateStr);
-      return DateFormat('dd/MM/yyyy').format(dt);
-    } catch (_) {
-      return dateStr;
-    }
-  }
-
-  String _formatAmount(double? amount) {
-    if (amount == null) return '-';
-    final fmt = NumberFormat('#,##0.00', 'it');
-    return '€ ${fmt.format(amount)}';
-  }
-
-  Widget _row(String label, String value) {
-    return Padding(
-      padding: const EdgeInsets.symmetric(vertical: 2),
-      child: Row(
-        children: [
-          SizedBox(
-            width: 110,
-            child: Text(
-              label,
-              style: theme.textTheme.bodySmall?.copyWith(
-                color: theme.colorScheme.outline,
-              ),
-            ),
-          ),
-          Expanded(
-            child: Text(value, style: theme.textTheme.bodyMedium),
-          ),
-        ],
-      ),
-    );
-  }
-}

+ 33 - 0
lib/ui/pages/customers/tabs/destinations_tab.dart

@@ -0,0 +1,33 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:ristoris/providers/customer_provider.dart';
+
+class DestinationsTab extends ConsumerWidget {
+  final int idCliente;
+  final ThemeData theme;
+  const DestinationsTab({required this.idCliente, required this.theme});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final destsAsync = ref.watch(customerDestinationsProvider(idCliente));
+    return destsAsync.when(
+      data: (dests) {
+        if (dests.isEmpty) {
+          return const Center(child: Text('Nessuna destinazione'));
+        }
+        return ListView.builder(
+          itemCount: dests.length,
+          itemBuilder: (_, i) => ListTile(
+            leading: const Icon(Icons.location_on_outlined),
+            title: Text(dests[i].descrizione ?? ''),
+            subtitle: Text(
+              '${dests[i].indirizzo ?? ""} ${dests[i].citta ?? ""}',
+            ),
+          ),
+        );
+      },
+      loading: () => const Center(child: CircularProgressIndicator()),
+      error: (e, _) => Center(child: Text('Errore: $e')),
+    );
+  }
+}

+ 71 - 0
lib/ui/pages/customers/tabs/info_tab.dart

@@ -0,0 +1,71 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:ristoris/database/database.dart';
+
+class InfoTab extends StatelessWidget {
+  final Cliente cliente;
+  final ThemeData theme;
+  const InfoTab({required this.cliente, required this.theme});
+
+  @override
+  Widget build(BuildContext context) {
+    return SingleChildScrollView(
+      padding: const EdgeInsets.all(16),
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          _infoRow('Codice', cliente.codCliente ?? '-'),
+          _infoRow('Ragione Sociale', cliente.ragioneSociale ?? '-'),
+          _infoRow('Indirizzo', cliente.indirizzo ?? '-'),
+          _infoRow(
+            'Città',
+            '${cliente.citta ?? ""} ${cliente.provincia ?? ""} ${cliente.cap ?? ""}',
+          ),
+          _infoRow('Telefono', cliente.telefono ?? '-'),
+          _infoRow('Cellulare', cliente.cellulare ?? '-'),
+          _infoRow('Email', cliente.email ?? '-'),
+          _infoRow('Partita IVA', cliente.partitaIva ?? '-'),
+          _infoRow('Codice Fiscale', cliente.codiceFiscale ?? '-'),
+          if (cliente.sdi != null && cliente.sdi!.isNotEmpty) _infoRow('SDI', cliente.sdi!),
+          if (cliente.pec != null && cliente.pec!.isNotEmpty) _infoRow('PEC', cliente.pec!),
+          if (cliente.note != null && cliente.note!.isNotEmpty) _infoRow('Note', cliente.note!),
+          if (cliente.codBlocco != null && cliente.codBlocco!.isNotEmpty)
+            Card(
+              color: Colors.red.shade50,
+              child: Padding(
+                padding: const EdgeInsets.all(12),
+                child: Row(
+                  children: [
+                    Icon(Icons.block, color: Colors.red),
+                    const SizedBox(width: 8),
+                    Text('Blocco: ${cliente.codBlocco}'),
+                  ],
+                ),
+              ),
+            ),
+        ],
+      ),
+    );
+  }
+
+  Widget _infoRow(String label, String value) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(vertical: 4),
+      child: Row(
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          SizedBox(
+            width: 120,
+            child: Text(
+              label,
+              style: theme.textTheme.bodySmall?.copyWith(
+                color: theme.colorScheme.outline,
+              ),
+            ),
+          ),
+          Expanded(child: Text(value)),
+        ],
+      ),
+    );
+  }
+}

+ 43 - 0
lib/ui/pages/customers/tabs/orders_tab.dart

@@ -0,0 +1,43 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:ristoris/core/utils.dart';
+import 'package:ristoris/providers/database_provider.dart';
+import 'package:go_router/go_router.dart';
+
+class OrdersTab extends ConsumerWidget {
+  final int idCliente;
+  final ThemeData theme;
+  const OrdersTab({required this.idCliente, required this.theme});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final ordiniDao = ref.read(ordiniDaoProvider);
+    return Scaffold(
+      body: SafeArea(
+        child: FutureBuilder(
+          future: ordiniDao.customerOrdersWidthData(idCliente, false),
+          builder: (_, snap) {
+            if (!snap.hasData) {
+              return const Center(child: CircularProgressIndicator());
+            }
+            final orders = snap.data!;
+            if (orders.isEmpty) return const Center(child: Text('Nessun ordine'));
+            return ListView.builder(
+              itemCount: orders.length,
+              itemBuilder: (_, i) => ListTile(
+                title: Text('Ordine #${orders[i].numeroOrdine ?? ""}'),
+                subtitle: Text(Utils.formatDate(orders[i].dataOrdine) ?? ''),
+                onTap: () => context.push('/dashboard/orders/${orders[i].idOrdine}'),
+              ),
+            );
+          },
+        ),
+      ),
+      floatingActionButton: FloatingActionButton(
+        heroTag: 'addOrder',
+        child: const Icon(Icons.add),
+        onPressed: () => context.push('/dashboard/orders/new?cliente=$idCliente'),
+      ),
+    );
+  }
+}

+ 85 - 0
lib/ui/pages/customers/tabs/payments_tab.dart

@@ -0,0 +1,85 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:intl/intl.dart';
+import 'package:ristoris/providers/customer_provider.dart';
+
+class PaymentsTab extends ConsumerWidget {
+  final int idCliente;
+  final ThemeData theme;
+  const PaymentsTab({required this.idCliente, required this.theme});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final paysAsync = ref.watch(customerPaymentsProvider(idCliente));
+    return paysAsync.when(
+      data: (pays) {
+        if (pays.isEmpty) return const Center(child: Text('Nessuna scadenza'));
+        return ListView.builder(
+          padding: const EdgeInsets.all(8),
+          itemCount: pays.length,
+          itemBuilder: (_, i) {
+            final p = pays[i];
+            return Card(
+              margin: const EdgeInsets.symmetric(vertical: 4),
+              child: Padding(
+                padding: const EdgeInsets.all(12),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    _row('Fattura N.', p.numDoc ?? '-'),
+                    _row('Data fatt.', _formatDate(p.dataDoc)),
+                    _row('Data scad', _formatDate(p.dataScadenza)),
+                    _row('Totale fattura', _formatAmount(p.importoFattura)),
+                    _row('Totale scad.', _formatAmount(p.importoScadenza)),
+                    _row('Saldo', _formatAmount(p.saldo)),
+                  ],
+                ),
+              ),
+            );
+          },
+        );
+      },
+      loading: () => const Center(child: CircularProgressIndicator()),
+      error: (e, _) => Center(child: Text('Errore: $e')),
+    );
+  }
+
+  String _formatDate(String? dateStr) {
+    if (dateStr == null) return '-';
+    try {
+      final dt = DateTime.parse(dateStr);
+      return DateFormat('dd/MM/yyyy').format(dt);
+    } catch (_) {
+      return dateStr;
+    }
+  }
+
+  String _formatAmount(double? amount) {
+    if (amount == null) return '-';
+    final fmt = NumberFormat('#,##0.00', 'it');
+    return '€ ${fmt.format(amount)}';
+  }
+
+  Widget _row(String label, String value) {
+    return Padding(
+      padding: const EdgeInsets.symmetric(vertical: 2),
+      child: Row(
+        children: [
+          SizedBox(
+            width: 110,
+            child: Text(
+              label,
+              style: theme.textTheme.bodySmall?.copyWith(
+                color: theme.colorScheme.outline,
+              ),
+            ),
+          ),
+          Expanded(
+            child: Text(value, style: theme.textTheme.bodyMedium),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 39 - 30
lib/ui/pages/statistics/statistics_page.dart

@@ -13,8 +13,7 @@ class StatisticsPage extends ConsumerStatefulWidget {
   ConsumerState<StatisticsPage> createState() => _StatisticsPageState();
 }
 
-class _StatisticsPageState extends ConsumerState<StatisticsPage>
-    with SingleTickerProviderStateMixin {
+class _StatisticsPageState extends ConsumerState<StatisticsPage> with SingleTickerProviderStateMixin {
   late TabController _tabCtrl;
 
   @override
@@ -90,15 +89,21 @@ class _MonthlyChart extends ConsumerWidget {
                   BarChartData(
                     alignment: BarChartAlignment.spaceAround,
                     maxY: maxVal * 1.2,
-                    barGroups: sortedKeys.asMap().entries.map((e) => BarChartGroupData(
-                      x: e.key,
-                      barRods: [BarChartRodData(
-                        toY: data[e.value]!,
-                        color: Theme.of(context).colorScheme.primary,
-                        width: 20,
-                        borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
-                      )],
-                    )).toList(),
+                    barGroups: sortedKeys
+                        .asMap()
+                        .entries
+                        .map((e) => BarChartGroupData(
+                              x: e.key,
+                              barRods: [
+                                BarChartRodData(
+                                  toY: data[e.value]!,
+                                  color: Theme.of(context).colorScheme.primary,
+                                  width: 20,
+                                  borderRadius: const BorderRadius.vertical(top: Radius.circular(4)),
+                                )
+                              ],
+                            ))
+                        .toList(),
                     titlesData: FlTitlesData(
                       bottomTitles: AxisTitles(
                         sideTitles: SideTitles(
@@ -171,13 +176,17 @@ class _TopClientsChart extends ConsumerWidget {
               Expanded(
                 child: PieChart(
                   PieChartData(
-                    sections: top5.asMap().entries.map((e) => PieChartSectionData(
-                      value: e.value.value,
-                      title: '${e.key + 1}',
-                      color: _colors[e.key % _colors.length],
-                      radius: 60,
-                      titleStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
-                    )).toList(),
+                    sections: top5
+                        .asMap()
+                        .entries
+                        .map((e) => PieChartSectionData(
+                              value: e.value.value,
+                              title: '${e.key + 1}',
+                              color: _colors[e.key % _colors.length],
+                              radius: 60,
+                              titleStyle: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
+                            ))
+                        .toList(),
                     sectionsSpace: 2,
                     centerSpaceRadius: 40,
                   ),
@@ -185,18 +194,18 @@ class _TopClientsChart extends ConsumerWidget {
               ),
               const SizedBox(height: 16),
               ...top5.asMap().entries.map((e) => Padding(
-                padding: const EdgeInsets.symmetric(vertical: 2),
-                child: Row(
-                  children: [
-                    Container(width: 12, height: 12, decoration: BoxDecoration(color: _colors[e.key % _colors.length], shape: BoxShape.circle)),
-                    const SizedBox(width: 8),
-                    Text('Cliente #${e.value.key}', style: Theme.of(context).textTheme.bodySmall),
-                    const Spacer(),
-                    Text(NumberFormat.currency(locale: 'it', symbol: '€').format(e.value.value),
-                      style: Theme.of(context).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.bold)),
-                  ],
-                ),
-              )),
+                    padding: const EdgeInsets.symmetric(vertical: 2),
+                    child: Row(
+                      children: [
+                        Container(width: 12, height: 12, decoration: BoxDecoration(color: _colors[e.key % _colors.length], shape: BoxShape.circle)),
+                        const SizedBox(width: 8),
+                        Text('${e.value.key}', style: Theme.of(context).textTheme.bodySmall),
+                        const Spacer(),
+                        Text(NumberFormat.currency(locale: 'it', symbol: '€').format(e.value.value),
+                            style: Theme.of(context).textTheme.bodySmall?.copyWith(fontWeight: FontWeight.bold)),
+                      ],
+                    ),
+                  )),
             ],
           ),
         );

+ 464 - 67
lib/ui/pages/sync/sync_page.dart

@@ -1,6 +1,9 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
+import 'package:google_fonts/google_fonts.dart';
+import 'package:intl/intl.dart';
+import '../../../database/database.dart';
 import '../../../providers/auth_provider.dart';
 import '../../../providers/sync_provider.dart';
 
@@ -11,8 +14,41 @@ class SyncPage extends ConsumerStatefulWidget {
   ConsumerState<SyncPage> createState() => _SyncPageState();
 }
 
-class _SyncPageState extends ConsumerState<SyncPage> {
+class _SyncPageState extends ConsumerState<SyncPage>
+    with SingleTickerProviderStateMixin {
   bool _forceFullSync = false;
+  late AnimationController _syncIconController;
+
+  @override
+  void initState() {
+    super.initState();
+    _syncIconController = AnimationController(
+      vsync: this,
+      duration: const Duration(seconds: 2),
+    );
+  }
+
+  @override
+  void dispose() {
+    _syncIconController.dispose();
+    super.dispose();
+  }
+
+  String _formatLastSyncTime(DateTime? time) {
+    if (time == null) return 'Mai';
+    final now = DateTime.now();
+    final today = DateTime(now.year, now.month, now.day);
+    final syncDate = DateTime(time.year, time.month, time.day);
+    final timeStr = DateFormat('HH:mm').format(time);
+
+    if (syncDate == today) {
+      return 'Oggi, $timeStr';
+    } else if (syncDate == today.subtract(const Duration(days: 1))) {
+      return 'Ieri, $timeStr';
+    } else {
+      return DateFormat('dd/MM/yyyy, HH:mm').format(time);
+    }
+  }
 
   @override
   Widget build(BuildContext context) {
@@ -20,6 +56,21 @@ class _SyncPageState extends ConsumerState<SyncPage> {
     final syncState = ref.watch(syncProvider);
     final theme = Theme.of(context);
     final agente = authState.agente;
+    final colorScheme = theme.colorScheme;
+
+    ref.listen<SyncState>(syncProvider, (prev, next) {
+      if (next.isSyncing && !(prev?.isSyncing ?? false)) {
+        _syncIconController.repeat();
+      } else if (!next.isSyncing && (prev?.isSyncing ?? false)) {
+        _syncIconController.stop();
+        _syncIconController.reset();
+        if (next.error == null && next.progress >= 1.0) {
+          ScaffoldMessenger.of(context).showSnackBar(
+            const SnackBar(content: Text('Trasmissione terminata')),
+          );
+        }
+      }
+    });
 
     return Scaffold(
       appBar: AppBar(
@@ -31,84 +82,430 @@ class _SyncPageState extends ConsumerState<SyncPage> {
           ),
         ],
       ),
-      body: SafeArea(
-        child: Center(
-          child: Padding(
-            padding: const EdgeInsets.all(32),
-            child: Column(
-              mainAxisSize: MainAxisSize.min,
+      body: SingleChildScrollView(
+        padding: const EdgeInsets.all(16),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: [
+            // Hero section: Toggle + Start button
+            LayoutBuilder(
+              builder: (context, constraints) {
+                final isWide = constraints.maxWidth > 600;
+                if (isWide) {
+                  return Row(
+                    crossAxisAlignment: CrossAxisAlignment.start,
+                    children: [
+                      Expanded(flex: 1, child: _buildToggleCard(context, syncState)),
+                      const SizedBox(width: 16),
+                      Expanded(
+                        flex: 2,
+                        child: _buildActionCard(
+                          context,
+                          agente,
+                          syncState,
+                          colorScheme,
+                        ),
+                      ),
+                    ],
+                  );
+                }
+                return Column(
+                  children: [
+                    _buildToggleCard(context, syncState),
+                    const SizedBox(height: 16),
+                    _buildActionCard(context, agente, syncState, colorScheme),
+                  ],
+                );
+              },
+            ),
+            const SizedBox(height: 16),
+
+            // Progress section
+            if (syncState.phases.isNotEmpty || syncState.isSyncing || syncState.progress >= 1.0)
+              _buildProgressSection(context, syncState, colorScheme),
+
+            // Error section
+            if (syncState.error != null) ...[
+              const SizedBox(height: 16),
+              _buildErrorCard(context, syncState, colorScheme),
+            ],
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildToggleCard(BuildContext context, SyncState syncState) {
+    final theme = Theme.of(context);
+    return Card(
+      child: Padding(
+        padding: const EdgeInsets.all(16),
+        child: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            Text(
+              'Tipo Sincronizzazione',
+              style: theme.textTheme.titleLarge,
+            ),
+            const SizedBox(height: 8),
+            Text(
+              'Scegli tra sincronizzazione parziale delle variazioni o scaricamento integrale di tutti i dati dal server.',
+              style: theme.textTheme.bodyMedium?.copyWith(
+                color: theme.colorScheme.onSurfaceVariant,
+              ),
+            ),
+            const SizedBox(height: 16),
+            Container(
+              padding: const EdgeInsets.all(12),
+              decoration: BoxDecoration(
+                color: theme.colorScheme.surface,
+                borderRadius: BorderRadius.circular(8),
+                border: Border.all(
+                  color: theme.colorScheme.outlineVariant.withValues(alpha: 0.3),
+                ),
+              ),
+              child: Row(
+                children: [
+                  Switch(
+                    value: _forceFullSync,
+                    onChanged: syncState.isSyncing
+                        ? null
+                        : (v) => setState(() => _forceFullSync = v),
+                  ),
+                  const SizedBox(width: 12),
+                  Expanded(
+                    child: Text(
+                      'Sincronizzazione completa',
+                      style: theme.textTheme.labelLarge,
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildActionCard(
+    BuildContext context,
+    Agente? agente,
+    SyncState syncState,
+    ColorScheme colorScheme,
+  ) {
+    final theme = Theme.of(context);
+    return Card(
+      child: Padding(
+        padding: const EdgeInsets.all(24),
+        child: Column(
+          mainAxisAlignment: MainAxisAlignment.center,
+          children: [
+            FilledButton.icon(
+              onPressed: syncState.isSyncing || agente == null
+                  ? null
+                  : () => ref.read(syncProvider.notifier).startFullSync(
+                        agente.idAgente,
+                        agente.codAgente!,
+                        forceFullSync: _forceFullSync,
+                      ),
+              icon: syncState.isSyncing
+                  ? RotationTransition(
+                      turns: _syncIconController,
+                      child: const Icon(Icons.sync),
+                    )
+                  : const Icon(Icons.sync),
+              label: Text(
+                syncState.isSyncing ? 'Sincronizzazione in corso...' : 'Avvia Sincronizzazione',
+              ),
+              style: FilledButton.styleFrom(
+                padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20),
+                textStyle: theme.textTheme.titleLarge,
+              ),
+            ),
+            const SizedBox(height: 12),
+            Row(
+              mainAxisAlignment: MainAxisAlignment.center,
               children: [
                 Icon(
-                  syncState.isSyncing ? Icons.sync : Icons.sync_outlined,
-                  size: 64,
-                  color: syncState.isSyncing
-                      ? theme.colorScheme.primary
-                      : theme.colorScheme.outline,
+                  Icons.schedule,
+                  size: 16,
+                  color: colorScheme.onSurfaceVariant,
                 ),
-                const SizedBox(height: 24),
+                const SizedBox(width: 6),
                 Text(
-                  syncState.isSyncing
-                      ? 'Sincronizzazione in corso...'
-                      : 'Sincronizzazione dati',
-                  style: theme.textTheme.titleLarge,
+                  'Ultimo aggiornamento: ${_formatLastSyncTime(syncState.lastSyncTime)}',
+                  style: theme.textTheme.bodyMedium?.copyWith(
+                    color: colorScheme.onSurfaceVariant,
+                  ),
                 ),
-                const SizedBox(height: 16),
-                if (syncState.isSyncing) ...[
-                  LinearProgressIndicator(value: syncState.progress),
-                  const SizedBox(height: 12),
-                  Text(syncState.message, style: theme.textTheme.bodyMedium),
-                  const SizedBox(height: 8),
-                  Text('${(syncState.progress * 100).toInt()}%',
-                      style: theme.textTheme.bodySmall),
-                ] else if (syncState.error != null) ...[
-                  Card(
-                    color: theme.colorScheme.errorContainer,
-                    child: Padding(
-                      padding: const EdgeInsets.all(12),
-                      child: Text(syncState.error!,
-                          style: TextStyle(
-                              color: theme.colorScheme.onErrorContainer)),
+              ],
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+
+  Widget _buildProgressSection(
+    BuildContext context,
+    SyncState syncState,
+    ColorScheme colorScheme,
+  ) {
+    final theme = Theme.of(context);
+    final percent = syncState.phases.isNotEmpty
+        ? (syncState.progress * 100).toInt()
+        : 0;
+
+    return Card(
+      child: Column(
+        crossAxisAlignment: CrossAxisAlignment.stretch,
+        children: [
+          // Header
+          Container(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+            decoration: BoxDecoration(
+              color: theme.colorScheme.surface,
+              border: Border(
+                bottom: BorderSide(color: theme.colorScheme.outlineVariant),
+              ),
+            ),
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.spaceBetween,
+              children: [
+                Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    Text(
+                      syncState.currentAgentCode != null
+                          ? 'Sincronizzazione dati agente (${syncState.currentAgentCode})'
+                          : 'Sincronizzazione dati agente',
+                      style: theme.textTheme.titleLarge,
                     ),
+                    if (syncState.agentiCodici.isNotEmpty)
+                      RichText(
+                        text: TextSpan(
+                          style: theme.textTheme.bodyMedium?.copyWith(
+                            color: colorScheme.onSurfaceVariant,
+                          ),
+                          children: [
+                            const TextSpan(
+                                text:
+                                    'Stato complessivo del processo (Agenti: '),
+                            for (int i = 0;
+                                i < syncState.agentiCodici.length;
+                                i++) ...[
+                              if (i > 0) const TextSpan(text: ', '),
+                              TextSpan(
+                                text: syncState.agentiCodici[i],
+                                style: syncState.agentiCodici[i] ==
+                                        syncState.currentAgentCode
+                                    ? TextStyle(
+                                        fontWeight: FontWeight.bold,
+                                        color: colorScheme.onSurfaceVariant,
+                                      )
+                                    : null,
+                              ),
+                            ],
+                            const TextSpan(text: ')'),
+                          ],
+                        ),
+                      )
+                    else
+                      Text(
+                        'Stato complessivo del processo',
+                        style: theme.textTheme.bodyMedium?.copyWith(
+                          color: colorScheme.onSurfaceVariant,
+                        ),
+                      ),
+                  ],
+                ),
+                Text(
+                  '$percent%',
+                  style: theme.textTheme.headlineMedium?.copyWith(
+                    color: colorScheme.secondary,
                   ),
-                ] else if (syncState.progress >= 1.0) ...[
-                  Icon(Icons.check_circle, size: 48, color: Colors.green),
-                  const SizedBox(height: 8),
-                  const Text('Completato!'),
-                ],
-                const SizedBox(height: 12),
-                CheckboxListTile(
-                  value: _forceFullSync,
-                  onChanged: syncState.isSyncing
-                      ? null
-                      : (v) => setState(() => _forceFullSync = v!),
-                  title: const Text('Sincronizzazione completa'),
-                  subtitle:
-                      const Text('Forza il download completo di tutti i dati'),
-                  controlAffinity: ListTileControlAffinity.leading,
-                  enabled: !syncState.isSyncing,
                 ),
-                const SizedBox(height: 12),
-                FilledButton.icon(
-                  onPressed: syncState.isSyncing || agente == null
-                      ? null
-                      : () => ref.read(syncProvider.notifier).startFullSync(
-                            agente.idAgente,
-                            agente.codAgente!,
-                            forceFullSync: _forceFullSync,
-                          ),
-                  icon: syncState.isSyncing
-                      ? const SizedBox(
-                          width: 20,
-                          height: 20,
-                          child: CircularProgressIndicator(strokeWidth: 2))
-                      : const Icon(Icons.sync),
-                  label: Text(syncState.isSyncing
-                      ? 'In corso...'
-                      : 'Avvia Sincronizzazione'),
+              ],
+            ),
+          ),
+
+          // Progress bar + phases
+          Padding(
+            padding: const EdgeInsets.all(16),
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.stretch,
+              children: [
+                // Main progress bar
+                ClipRRect(
+                  borderRadius: BorderRadius.circular(4),
+                  child: LinearProgressIndicator(
+                    value: syncState.progress,
+                    minHeight: 8,
+                    backgroundColor: theme.colorScheme.surfaceContainerHighest,
+                    valueColor: AlwaysStoppedAnimation<Color>(
+                      colorScheme.secondary,
+                    ),
+                  ),
                 ),
+                const SizedBox(height: 16),
+
+                // Phases grid
+                ...syncState.phases.asMap().entries.map(
+                      (entry) => _buildPhaseRow(
+                        context,
+                        entry.key,
+                        entry.value,
+                        colorScheme,
+                      ),
+                    ),
               ],
             ),
           ),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildPhaseRow(
+    BuildContext context,
+    int index,
+    SyncPhaseInfo phase,
+    ColorScheme colorScheme,
+  ) {
+    final theme = Theme.of(context);
+    final isCompleted = phase.status == SyncPhaseStatus.completed;
+
+    return Container(
+      margin: const EdgeInsets.only(bottom: 8),
+      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
+      decoration: BoxDecoration(
+        color: isCompleted
+            ? Colors.green.shade50
+            : theme.colorScheme.surface,
+        borderRadius: BorderRadius.circular(8),
+        border: Border.all(
+          color: theme.colorScheme.outlineVariant.withValues(alpha: 0.2),
+        ),
+      ),
+      child: Row(
+        children: [
+          // Status dot
+          _buildStatusDot(phase.status, colorScheme),
+          const SizedBox(width: 12),
+
+          // Phase name
+          Expanded(
+            child: Text(
+              phase.name,
+              style: theme.textTheme.bodyMedium?.copyWith(
+                color: isCompleted
+                    ? Colors.green.shade700
+                    : theme.colorScheme.onSurface,
+              ),
+            ),
+          ),
+
+          // Status text
+          _buildStatusText(phase, colorScheme),
+        ],
+      ),
+    );
+  }
+
+  Widget _buildStatusDot(SyncPhaseStatus status, ColorScheme colorScheme) {
+    Color dotColor;
+    List<BoxShadow>? shadow;
+
+    switch (status) {
+      case SyncPhaseStatus.waiting:
+        dotColor = colorScheme.outlineVariant;
+        break;
+      case SyncPhaseStatus.downloading:
+      case SyncPhaseStatus.processing:
+        dotColor = colorScheme.secondary;
+        shadow = [
+          BoxShadow(
+            color: colorScheme.secondary.withValues(alpha: 0.5),
+            blurRadius: 6,
+          ),
+        ];
+        break;
+      case SyncPhaseStatus.completed:
+        dotColor = Colors.green;
+        shadow = [
+          BoxShadow(
+            color: Colors.green.withValues(alpha: 0.5),
+            blurRadius: 6,
+          ),
+        ];
+    }
+
+    return AnimatedContainer(
+      duration: const Duration(milliseconds: 300),
+      width: 10,
+      height: 10,
+      decoration: BoxDecoration(
+        shape: BoxShape.circle,
+        color: dotColor,
+        boxShadow: shadow,
+      ),
+    );
+  }
+
+  Widget _buildStatusText(SyncPhaseInfo phase, ColorScheme colorScheme) {
+    Color textColor;
+    switch (phase.status) {
+      case SyncPhaseStatus.waiting:
+        textColor = colorScheme.onSurfaceVariant;
+        break;
+      case SyncPhaseStatus.downloading:
+      case SyncPhaseStatus.processing:
+        textColor = colorScheme.secondary;
+        break;
+      case SyncPhaseStatus.completed:
+        textColor = Colors.green.shade600;
+        break;
+    }
+
+    return Container(
+      constraints: const BoxConstraints(minWidth: 140),
+      child: Text(
+        phase.statusText,
+        textAlign: TextAlign.end,
+        style: GoogleFonts.jetBrainsMono(
+          fontSize: 11,
+          fontWeight: FontWeight.w500,
+          color: textColor,
+          letterSpacing: -0.3,
+        ),
+      ),
+    );
+  }
+
+  Widget _buildErrorCard(
+    BuildContext context,
+    SyncState syncState,
+    ColorScheme colorScheme,
+  ) {
+    return Card(
+      color: colorScheme.errorContainer,
+      child: Padding(
+        padding: const EdgeInsets.all(16),
+        child: Row(
+          children: [
+            Icon(
+              Icons.error_outline,
+              color: colorScheme.onErrorContainer,
+            ),
+            const SizedBox(width: 12),
+            Expanded(
+              child: Text(
+                syncState.error!,
+                style: TextStyle(color: colorScheme.onErrorContainer),
+              ),
+            ),
+          ],
         ),
       ),
     );