瀏覽代碼

Gestione multi agente

Fabio Dalle Ave 2 月之前
父節點
當前提交
31776feee0
共有 48 個文件被更改,包括 2654 次插入 和 906 次删除
  1. 0 0
      .flutter-plugins-dependencies
  2. 二進制
      assets/db/Ristoris.db
  3. 1 1
      lib/business/customer_manager.dart
  4. 2 6
      lib/core/theme.dart
  5. 14 0
      lib/core/utils.dart
  6. 9 3
      lib/database/daos/agenti_dao.dart
  7. 79 25
      lib/database/daos/articoli_dao.dart
  8. 164 52
      lib/database/daos/clienti_dao.dart
  9. 17 18
      lib/database/daos/listini_dao.dart
  10. 1 2
      lib/database/daos/ordini_dao.dart
  11. 7 6
      lib/database/daos/pagamenti_dao.dart
  12. 3 4
      lib/database/daos/parametri_dao.dart
  13. 16 14
      lib/database/daos/promozioni_dao.dart
  14. 14 10
      lib/database/daos/scadenze_dao.dart
  15. 4 4
      lib/database/daos/sconti_dao.dart
  16. 7 5
      lib/database/daos/tabelle_dao.dart
  17. 74 60
      lib/database/database.dart
  18. 1319 235
      lib/database/database.g.dart
  19. 6 5
      lib/database/tables/agenti.dart
  20. 1 0
      lib/database/tables/listini.dart
  21. 1 1
      lib/database/tables/scadenze.dart
  22. 1 1
      lib/database/tables/statistiche_vendita.dart
  23. 2 3
      lib/database/views/articoli_con_promo.dart
  24. 35 0
      lib/database/views/v_scadenze.dart
  25. 29 0
      lib/providers/auth_provider.dart
  26. 2 4
      lib/providers/dashboard_provider.dart
  27. 4 0
      lib/providers/database_provider.dart
  28. 38 21
      lib/providers/sync_provider.dart
  29. 1 2
      lib/services/auth_service.dart
  30. 65 1
      lib/services/sync/agente_sync_service.dart
  31. 28 19
      lib/services/sync/ordine_sync_service.dart
  32. 134 104
      lib/services/sync/reference_sync_service.dart
  33. 66 40
      lib/ui/pages/catalog/catalog_page.dart
  34. 12 6
      lib/ui/pages/customers/customer_detail_page.dart
  35. 3 3
      lib/ui/pages/customers/customer_list_page.dart
  36. 59 0
      lib/ui/pages/dashboard/agent_switch_page.dart
  37. 41 19
      lib/ui/pages/dashboard/dashboard_page.dart
  38. 46 54
      lib/ui/pages/login/init_page.dart
  39. 35 21
      lib/ui/pages/orders/order_edit_page.dart
  40. 14 5
      lib/ui/pages/orders/order_list_page.dart
  41. 200 102
      lib/ui/pages/orders/selezione_articolo_page.dart
  42. 33 16
      lib/ui/pages/orders/selezione_cliente_page.dart
  43. 52 23
      lib/ui/pages/payments/payments_page.dart
  44. 5 1
      lib/ui/routes.dart
  45. 3 0
      lib/ui/widgets/app_drawer.dart
  46. 5 8
      lib/ui/widgets/customer_card.dart
  47. 2 0
      macos/Flutter/GeneratedPluginRegistrant.swift
  48. 0 2
      test/widget_test.dart

File diff suppressed because it is too large
+ 0 - 0
.flutter-plugins-dependencies


二進制
assets/db/Ristoris.db


+ 1 - 1
lib/business/customer_manager.dart

@@ -14,7 +14,7 @@ class CustomerManager {
 
   Future<void> saveCustomer(Cliente cliente, int idAgente) async {
     final db = _clientiDao.db;
-    if (cliente.idCliente! > 0) {
+    if (cliente.idCliente > 0) {
       await db.update(db.clienti).replace(cliente);
     } else {
       final id = await db.into(db.clienti).insert(cliente);

+ 2 - 6
lib/core/theme.dart

@@ -28,11 +28,9 @@ class AppTheme {
     onError: Color(0xFFFFFFFF),
     errorContainer: Color(0xFFFFDAD6),
     onErrorContainer: Color(0xFF93000A),
-    background: Color(0xFFF8F9FF),
-    onBackground: Color(0xFF0B1C30),
     surface: Color(0xFFF8F9FF),
     onSurface: Color(0xFF0B1C30),
-    surfaceVariant: Color(0xFFD3E4FE),
+    surfaceContainerHighest: Color(0xFFD3E4FE),
     onSurfaceVariant: Color(0xFF45464D),
     outline: Color(0xFF76777D),
     outlineVariant: Color(0xFFC6C6CD),
@@ -60,11 +58,9 @@ class AppTheme {
     onError: Color(0xFF690005),
     errorContainer: Color(0xFF93000A),
     onErrorContainer: Color(0xFFFFDAD6),
-    background: Color(0xFF111318),
-    onBackground: Color(0xFFEAF1FF),
     surface: Color(0xFF111318),
     onSurface: Color(0xFFEAF1FF),
-    surfaceVariant: Color(0xFF2D3138),
+    surfaceContainerHighest: Color(0xFF2D3138),
     onSurfaceVariant: Color(0xFFC6C6CD),
     outline: Color(0xFF8E9099),
     outlineVariant: Color(0xFF45464D),

+ 14 - 0
lib/core/utils.dart

@@ -0,0 +1,14 @@
+import 'package:intl/intl.dart';
+
+class Utils {
+
+  static  String formatDate(String? dateStr) {
+    if (dateStr == null) return '-';
+    try {
+      final dt = DateTime.parse(dateStr);
+      return DateFormat('dd/MM/yyyy').format(dt);
+    } catch (_) {
+      return dateStr;
+    }
+  }
+}

+ 9 - 3
lib/database/daos/agenti_dao.dart

@@ -18,9 +18,11 @@ class AgentiDao {
   }
 
   Future<Agente?> authenticate(String code, String password) {
-    return  (db.select(db.agenti)
-      ..where((t) => t.codAgente.equals(code) & t.pwd.equals(password))
-    ).getSingleOrNull();
+    if (password.isEmpty) {
+      return (db.select(db.agenti)..where((t) => t.codAgente.equals(code) & t.pwd.isNull())).getSingleOrNull();
+    } else {
+      return (db.select(db.agenti)..where((t) => t.codAgente.equals(code) & t.pwd.equals(password))).getSingleOrNull();
+    }
   }
 
   Future<int> countAgenti() async {
@@ -39,4 +41,8 @@ class AgentiDao {
   Future<void> zapCodiceAgente(String code) {
     return (db.delete(db.agenti)..where((t) => t.codAgente.equals(code))).go();
   }
+
+  Future<void> updateIdAgente(int oldId, int newId) {
+    return (db.customStatement('update agenti set idAgente=$newId where idAgente=$oldId'));
+  }
 }

+ 79 - 25
lib/database/daos/articoli_dao.dart

@@ -12,6 +12,11 @@ class ArticoliDao {
         .getSingleOrNull();
   }
 
+  Future<Articolo?> getById(int id) {
+    return (db.select(db.articoli)..where((t) => t.idArticolo.equals(id)))
+        .getSingleOrNull();
+  }
+
   Future<List<Articolo>> getItems(String filter) async {
     final words = filter.split(' ').where((w) => w.isNotEmpty).toList();
     final q = db.select(db.articoli)
@@ -107,21 +112,20 @@ class ArticoliDao {
   }
 
   Future<List<Listino>> getPriceCodes(int idArticolo) {
-    return (db.select(db.listini)
-          ..join([
-            innerJoin(
-              db.listiniArticolo,
-              db.listiniArticolo.idListino.equalsExp(db.listini.idListino) &
-                  db.listiniArticolo.idArticolo.equals(idArticolo),
-            ),
-          ])
-          ..where(
-            (t) =>
-                t.codListino.isNotNull() &
-                t.codListino.like('OF2').not() &
-                t.codListino.like('OF4').not() &
-                t.codListino.like('I0').not(),
-          ))
+    return (db.select(db.listini).join([
+          innerJoin(
+            db.listiniArticolo,
+            db.listiniArticolo.idListino.equalsExp(db.listini.idListino) &
+                db.listiniArticolo.idArticolo.equals(idArticolo),
+          ),
+        ])
+        ..where(
+          db.listini.codListino.isNotNull() &
+              db.listini.codListino.like('OF2').not() &
+              db.listini.codListino.like('OF4').not() &
+              db.listini.codListino.like('I0').not(),
+        ))
+        .map((row) => row.readTable(db.listini))
         .get();
   }
 
@@ -160,9 +164,10 @@ class ArticoliDao {
           Variable.withString(pattern),
           Variable.withString(pattern),
         ]);
+
+        buffer.write(filters.join(' AND '));
+        buffer.write(')');
       }
-      buffer.write(filters.join(' AND '));
-      buffer.write(')');
     }
 
     buffer.write(' ORDER BY a."descrizione" ASC');
@@ -177,8 +182,59 @@ class ArticoliDao {
     return rows.map((row) => Articolo.fromJson(row.data)).toList();
   }
 
+  Future<List<ArticoliConPromoData>> getActiveItemsWithPromo(
+    String filter,
+    int idCliente,
+  ) async {
+    final words = filter.split(' ').where((w) => w.isNotEmpty).toList();
+
+    final buffer = StringBuffer('''
+      SELECT DISTINCT a.idArticolo, a.codArticolo,a.descrizione, 0 as prezzoBase,
+             a.attivo, a."nonDisponibile",
+             0 AS inPromo,
+             (SELECT ROUND(sv.importo / sv.qta, 2)
+              FROM "Statistiche_vendita" sv
+              WHERE sv.idCliente = ? AND sv.idArticolo = a.idArticolo
+              ORDER BY sv."dataFattura" DESC LIMIT 1 OFFSET 1) AS ultimoPrezzo,
+             (SELECT sv."dataFattura"
+              FROM "Statistiche_vendita" sv
+              WHERE sv.idCliente = ? AND sv.idArticolo = a.idArticolo
+              ORDER BY sv."dataFattura" DESC LIMIT 1 OFFSET 1) AS lastDateSold
+      FROM "Articoli" a
+      WHERE a.attivo = 1
+    ''');
+
+    final variables = <Variable>[
+      Variable.withInt(idCliente),
+      Variable.withInt(idCliente),
+    ];
+
+    if (words.isNotEmpty) {
+      for (final word in words) {
+        buffer.write(' AND (a."codArticolo" LIKE ? OR a.descrizione LIKE ?)');
+        final pattern = '%$word%';
+        variables.addAll([
+          Variable.withString(pattern),
+          Variable.withString(pattern),
+        ]);
+      }
+    }
+
+    buffer.write(' ORDER BY a.descrizione LIMIT 4000');
+
+    final rows = await db
+        .customSelect(
+          buffer.toString(),
+          variables: variables,
+        )
+        .get();
+
+    return rows.map((row) => ArticoliConPromoData.fromJson(row.data)).toList();
+  }
+
   Future<List<Listino>> getListiniArticoloValidi(
     int idArticolo,
+    int idCliente,
     String dataOrdine,
   ) {
     final sql = "select distinct l.* from Listini l "
@@ -218,14 +274,12 @@ class ArticoliDao {
     String dataOrdine,
   ) async {
     final lcRows = await (db.select(db.listiniCliente)
-          ..where(
-            (t) =>
-                t.idCliente.equals(idCliente) &
-                t.attivo.equals(1) &
-                t.annullato.equals('N') &
-                t.validoDa.isSmallerOrEqualValue(dataOrdine) &
-                t.validoA.isBiggerOrEqualValue(dataOrdine),
-          ))
+          ..orderBy([(t) => OrderingTerm.desc(t.validoDa)])
+          ..where((t) =>
+              t.idCliente.equals(idCliente) &
+              t.attivo.equals(1) &
+              t.annullato.equals('N') &
+              t.validoDa.isSmallerOrEqualValue(dataOrdine)))
         .get();
 
     final validIds =

+ 164 - 52
lib/database/daos/clienti_dao.dart

@@ -10,51 +10,44 @@ class ClientiDao {
   }
 
   Future<Cliente?> getByCode(int idAgente, String code) {
-    return (db.select(db.clienti)
-      ..join([
-        innerJoin(db.clientiAgente, db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente)),
-      ])
-      ..where((t) => t.codCliente.equals(code))
-    ).getSingleOrNull();
+    return (db.select(db.clienti).join([
+          innerJoin(db.clientiAgente, db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente)),
+        ])
+        ..where(db.clienti.codCliente.equals(code)))
+        .map((row) => row.readTable(db.clienti))
+        .getSingleOrNull();
   }
 
   Future<Cliente?> getByRemoteId(int idAgente, int remoteId) {
-    return (db.select(db.clienti)
-      ..join([
-        innerJoin(db.clientiAgente, db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente)),
-      ])
-      ..where((t) => t.idClienteRemoto.equals(remoteId))
-    ).getSingleOrNull();
+    return (db.select(db.clienti)..where((t) => t.idClienteRemoto.equals(remoteId))).getSingleOrNull();
   }
 
   Future<List<Cliente>> getClienti(int idAgente, String filter) async {
     final words = filter.split(' ').where((w) => w.isNotEmpty).toList();
-    final q = db.select(db.clienti)
-      ..join([
-        innerJoin(db.clientiAgente, db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente)),
-      ])
+    final q = db.select(db.clienti).join([
+      innerJoin(db.clientiAgente, db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente)),
+    ])
       ..limit(200);
     if (words.isNotEmpty) {
       for (final word in words) {
         final pattern = '%$word%';
-        q.where((t) =>
-          t.codCliente.like(pattern) |
-          t.ragioneSociale.like(pattern) |
-          t.partitaIva.like(pattern)
-        );
+        q.where(db.clienti.codCliente.like(pattern) | db.clienti.ragioneSociale.like(pattern) | db.clienti.partitaIva.like(pattern));
       }
     }
-    return q.get();
+    return q.map((row) => row.readTable(db.clienti)).get();
   }
 
   Future<List<Cliente>> getActiveClients(int idAgente) {
-    return (db.select(db.clienti)
-      ..join([
-        innerJoin(db.clientiAgente, db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente)),
-      ])
-      ..where((t) => t.attivo.equals(1))
-      ..orderBy([(t) => OrderingTerm(expression: t.ragioneSociale)])
-    ).get();
+    return (db.select(db.clienti).join([
+          innerJoin(
+            db.clientiAgente,
+            db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente),
+          ),
+        ])
+        ..where(db.clienti.attivo.equals(1))
+        ..orderBy([OrderingTerm(expression: db.clienti.ragioneSociale)]))
+        .map((row) => row.readTable(db.clienti))
+        .get();
   }
 
   Future<List<TopCustomerData>> topCustomers(int limit) async {
@@ -62,12 +55,12 @@ class ClientiDao {
   }
 
   Future<List<Cliente>> getNewCustomers(int idAgente) {
-    return (db.select(db.clienti)
-      ..join([
-        innerJoin(db.clientiAgente, db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente)),
-      ])
-      ..where((t) => t.stato.equals('N'))
-    ).get();
+    return (db.select(db.clienti).join([
+          innerJoin(db.clientiAgente, db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente)),
+        ])
+        ..where(db.clienti.stato.equals('N')))
+        .map((row) => row.readTable(db.clienti))
+        .get();
   }
 
   Future<int> countNewCustomers() async {
@@ -76,15 +69,13 @@ class ClientiDao {
   }
 
   Future<List<Cliente>> inactiveCustomers(int idAgente, String sinceDate) async {
-    final recentIds = await (db.select(db.ordini)
-      ..where((t) => t.idCliente.isNotNull())
-    ).get().then((orders) => orders.map((o) => o.idCliente!).toSet());
-    return (db.select(db.clienti)
-      ..join([
-        innerJoin(db.clientiAgente, db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente)),
-      ])
-      ..where((t) => t.idCliente.isNotIn(recentIds))
-    ).get();
+    final recentIds = await (db.select(db.ordini)..where((t) => t.idCliente.isNotNull())).get().then((orders) => orders.map((o) => o.idCliente!).toSet());
+    return (db.select(db.clienti).join([
+          innerJoin(db.clientiAgente, db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente)),
+        ])
+        ..where(db.clienti.idCliente.isNotIn(recentIds)))
+        .map((row) => row.readTable(db.clienti))
+        .get();
   }
 
   Future<int> countCustomersToSend() async {
@@ -93,17 +84,138 @@ class ClientiDao {
   }
 
   Future<int> countCustomersToSendForAgent(int idAgente) async {
-    final rows = await (db.select(db.clienti)
-      ..join([
-        innerJoin(db.clientiAgente, db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente)),
-      ])
-      ..where((t) => t.stato.equals('N'))
-    ).get();
+    final rows = await (db.select(db.clienti).join([
+          innerJoin(db.clientiAgente, db.clientiAgente.idCliente.equalsExp(db.clienti.idCliente) & db.clientiAgente.idAgente.equals(idAgente)),
+        ])
+        ..where(db.clienti.stato.equals('N')))
+        .map((row) => row.readTable(db.clienti))
+        .get();
     return rows.length;
   }
 
   Future<void> markAsSent(int idCliente, int idAssegnato) {
-    return (db.update(db.clienti)..where((t) => t.idCliente.equals(idCliente)))
-      .write(ClientiCompanion(stato: const Value('T')));
+    return (db.update(db.clienti)..where((t) => t.idCliente.equals(idCliente))).write(ClientiCompanion(stato: const Value('T')));
+  }
+
+  Future<List<ArticoliConPromoData>> getFavoriteItems(
+    int idCliente,
+    String dateToUse,
+    String filter,
+  ) async {
+    var filtro = '%$filter';
+    if (filtro.length > 13) filtro = filtro.substring(0, 13);
+    filtro += '%';
+
+    final sql = '''
+      SELECT DISTINCT a.idArticolo, a.codArticolo, a.descrizione, 0 "prezzoBase",
+             a.attivo, a."nonDisponibile",
+        CASE WHEN EXISTS (
+          SELECT pa.idArticolo FROM "PromoArticoli" pa
+          JOIN "Promozioni" p ON pa.progressivo = p.progressivo
+          WHERE pa.idArticolo = a.idArticolo
+            AND date('now') BETWEEN p."dataInizio" AND p."dataFine"
+        ) THEN 1 ELSE 0 END AS inPromo,
+        (SELECT ROUND(sv.importo / sv.qta, 2)
+         FROM "Statistiche_vendita" sv
+         WHERE sv.idCliente = ? AND sv.idArticolo = a.idArticolo
+         ORDER BY sv."dataFattura" DESC LIMIT 1 OFFSET 1) AS ultimoPrezzo,
+        (SELECT sv."dataFattura"
+         FROM "Statistiche_vendita" sv
+         WHERE sv.idCliente = ? AND sv.idArticolo = a.idArticolo
+         ORDER BY sv."dataFattura" DESC LIMIT 1 OFFSET 1) AS lastDateSold
+      FROM "Listini_cliente" lc
+      JOIN "Articoli" a ON lc."idArticolo" = a.idArticolo
+      WHERE lc.idCliente = ?
+        AND (lc."validoDa" IS NULL OR lc."validoDa" <= ?)
+        AND (lc."validoA" IS NULL OR lc."validoA" >= ?)
+        AND a.attivo = 1
+        AND lc.stato <> 'X'
+        AND lc.prezzo > 0.001
+        AND (a.descrizione LIKE ? OR a."codArticolo" LIKE ?)
+      ORDER BY a.descrizione
+    ''';
+
+    final rows = await db.customSelect(
+      sql,
+      variables: [
+        Variable.withInt(idCliente),
+        Variable.withInt(idCliente),
+        Variable.withInt(idCliente),
+        Variable.withString(dateToUse),
+        Variable.withString(dateToUse),
+        Variable.withString(filtro),
+        Variable.withString(filtro),
+      ],
+    ).get();
+
+    return rows.map((row) => ArticoliConPromoData.fromJson(row.data)).toList();
+  }
+
+  Future<List<ArticoliConPromoData>> getAlreadySoldItems(
+    int idAgente,
+    int idCliente,
+    String dateToUse,
+    String filter,
+  ) async {
+    final words = filter.split(' ').where((w) => w.isNotEmpty).toList();
+
+    final buffer = StringBuffer('''
+      SELECT DISTINCT a.idArticolo, a.codArticolo, a.descrizione, 0 "prezzoBase",
+             a.attivo, a."nonDisponibile",
+             0 AS inPromo,
+             ROUND(sv.importo / sv.qta, 2) AS ultimoPrezzo,
+             sv."dataFattura" AS lastDateSold
+      FROM "Articoli" a
+      JOIN "Statistiche_vendita" sv ON sv.idArticolo = a.idArticolo
+      WHERE a.attivo = 1
+      and sv.idCliente = ?
+          AND sv.idArticolo = a.idArticolo
+          AND sv."dataFattura" >= ?
+          AND sv."tipoVendita" = 'N'
+    ''');
+
+    final variables = <Variable>[
+      Variable.withInt(idCliente),
+      Variable.withString(dateToUse),
+    ];
+
+    if (words.isNotEmpty) {
+      for (final word in words) {
+        buffer.write(' AND (a."codArticolo" LIKE ? OR a.descrizione LIKE ?)');
+        final pattern = '%$word%';
+        variables.addAll([
+          Variable.withString(pattern),
+          Variable.withString(pattern),
+        ]);
+      }
+    }
+
+    buffer.write(' ORDER BY a.descrizione');
+
+    final rows = await db
+        .customSelect(
+          buffer.toString(),
+          variables: variables,
+        )
+        .get();
+
+    return rows.map((row) => ArticoliConPromoData.fromJson(row.data)).toList();
+  }
+
+  Future<Agente?> getAgenteOf(String? codCliente) async {
+    if (codCliente == null) return null;
+    return (db.select(db.agenti).join([
+          innerJoin(
+            db.clientiAgente,
+            db.clientiAgente.idAgente.equalsExp(db.agenti.idAgente) & db.clientiAgente.attivo.equals(1),
+          ),
+          innerJoin(
+            db.clienti,
+            db.clienti.idCliente.equalsExp(db.clientiAgente.idCliente) & db.clienti.codCliente.equals(codCliente),
+          ),
+        ])
+        ..limit(1))
+        .map((row) => row.readTable(db.agenti))
+        .getSingleOrNull();
   }
 }

+ 17 - 18
lib/database/daos/listini_dao.dart

@@ -63,24 +63,23 @@ class ListiniDao {
     int idArticolo,
     String dataOrdine,
   ) {
-    return (db.select(db.listini)
-          ..join([
-            innerJoin(
-              db.listiniArticolo,
-              db.listiniArticolo.idListino.equalsExp(db.listini.idListino) &
-                  db.listiniArticolo.idArticolo.equals(idArticolo),
-            ),
-          ])
-          ..where(
-            (t) =>
-                db.listiniArticolo.attivo.equals(1) &
-                db.listiniArticolo.validoDa.isSmallerOrEqualValue(dataOrdine) &
-                db.listiniArticolo.validoA.isBiggerOrEqualValue(dataOrdine) &
-                t.codListino.isNotNull() &
-                t.codListino.like('OF2').not() &
-                t.codListino.like('OF4').not() &
-                t.codListino.like('I0').not(),
-          ))
+    return (db.select(db.listini).join([
+          innerJoin(
+            db.listiniArticolo,
+            db.listiniArticolo.idListino.equalsExp(db.listini.idListino) &
+                db.listiniArticolo.idArticolo.equals(idArticolo),
+          ),
+        ])
+        ..where(
+          db.listiniArticolo.attivo.equals(1) &
+              db.listiniArticolo.validoDa.isSmallerOrEqualValue(dataOrdine) &
+              db.listiniArticolo.validoA.isBiggerOrEqualValue(dataOrdine) &
+              db.listini.codListino.isNotNull() &
+              db.listini.codListino.like('OF2').not() &
+              db.listini.codListino.like('OF4').not() &
+              db.listini.codListino.like('I0').not(),
+        ))
+        .map((row) => row.readTable(db.listini))
         .get();
   }
 }

+ 1 - 2
lib/database/daos/ordini_dao.dart

@@ -68,10 +68,9 @@ class OrdiniDao {
     for (final p in patterns) {
       query.where(
         (t) =>
+            t.codCliente.like(p) |
             t.ragioneSociale.like(p) |
             t.filiale.like(p) |
-            t.indirizzo.like(p) |
-            t.citta.like(p) |
             CustomExpression<String>('CAST(numeroOrdine AS TEXT)').like(p),
       );
     }

+ 7 - 6
lib/database/daos/pagamenti_dao.dart

@@ -1,26 +1,27 @@
 import 'package:drift/drift.dart';
 import '../database.dart';
-import '../tables/pagamenti.dart';
 
 class PagamentiDao {
   final AppDatabase db;
   PagamentiDao(this.db);
 
   Future<Pagamento?> findByCod(String code) {
-    return (db.select(db.pagamenti)..where((t) => t.codPagamento.equals(code))).getSingleOrNull();
+    return (db.select(db.pagamenti)..where((t) => t.codPagamento.equals(code)))
+        .getSingleOrNull();
   }
 
   Future<List<Pagamento>> search(String filter) {
     final pattern = '%$filter%';
     return (db.select(db.pagamenti)
-      ..where((t) => t.descrizione.like(pattern) | t.codPagamento.like(pattern))
-    ).get();
+          ..where((t) =>
+              t.descrizione.like(pattern) | t.codPagamento.like(pattern)))
+        .get();
   }
 
   Future<List<Pagamento>> searchActive() {
     return (db.select(db.pagamenti)
-      ..where((t) => t.attivo.equals(1) | t.attivo.isNull())
-    ).get();
+          ..where((t) => t.attivo.equals(1) | t.attivo.isNull()))
+        .get();
   }
 
   Future<int> insert(Pagamento p) {

+ 3 - 4
lib/database/daos/parametri_dao.dart

@@ -1,13 +1,12 @@
-import 'package:drift/drift.dart';
 import '../database.dart';
-import '../tables/parametri.dart';
 
 class ParametriDao {
   final AppDatabase db;
   ParametriDao(this.db);
 
   Future<Parametro?> findByNome(String nome) {
-    return (db.select(db.parametri)..where((t) => t.nome.equals(nome))).getSingleOrNull();
+    return (db.select(db.parametri)..where((t) => t.nome.equals(nome)))
+        .getSingleOrNull();
   }
 
   Future<String?> read(String nome) async {
@@ -19,7 +18,7 @@ class ParametriDao {
     final existing = await findByNome(nome);
     if (existing != null) {
       await (db.update(db.parametri)..where((t) => t.nome.equals(nome)))
-        .write(Parametro(valore: valore, nome: nome));
+          .write(Parametro(valore: valore, nome: nome));
     } else {
       await db.into(db.parametri).insert(Parametro(nome: nome, valore: valore));
     }

+ 16 - 14
lib/database/daos/promozioni_dao.dart

@@ -1,9 +1,5 @@
 import 'package:drift/drift.dart';
 import '../database.dart';
-import '../tables/promozioni.dart';
-import '../tables/promo_articoli.dart';
-import '../tables/promo_listini.dart';
-import '../tables/promo_omaggi.dart';
 
 class PromozioniDao {
   final AppDatabase db;
@@ -11,15 +7,15 @@ class PromozioniDao {
 
   Future<List<Promozione>> getActivePromos(String today) {
     return (db.select(db.promozioni)
-      ..where((t) =>
-        (t.dataInizio.isNull() | t.dataInizio.equals('').not()) &
-        (t.stato.isNull() | t.stato.equals('A'))
-      )
-      ..orderBy([(t) => OrderingTerm(expression: t.priorita)])
-    ).get();
+          ..where((t) =>
+              (t.dataInizio.isNull() | t.dataInizio.equals('').not()) &
+              (t.stato.isNull() | t.stato.equals('A')))
+          ..orderBy([(t) => OrderingTerm(expression: t.priorita)]))
+        .get();
   }
 
-  Future<List<Promozione>> getPromosForItem(int idArticolo, String today) async {
+  Future<List<Promozione>> getPromosForItem(
+      int idArticolo, String today) async {
     final allPromos = await getActivePromos(today);
     final result = <Promozione>[];
     for (final p in allPromos) {
@@ -31,15 +27,21 @@ class PromozioniDao {
   }
 
   Future<List<PromoArticolo>> getPromoArticoli(int progressivo) {
-    return (db.select(db.promoArticoli)..where((t) => t.progressivo.equals(progressivo))).get();
+    return (db.select(db.promoArticoli)
+          ..where((t) => t.progressivo.equals(progressivo)))
+        .get();
   }
 
   Future<List<PromoListino>> getPromoListini(int progressivo) {
-    return (db.select(db.promoListini)..where((t) => t.progressivo.equals(progressivo))).get();
+    return (db.select(db.promoListini)
+          ..where((t) => t.progressivo.equals(progressivo)))
+        .get();
   }
 
   Future<List<PromoOmaggio>> getPromoOmaggi(int progressivo) {
-    return (db.select(db.promoOmaggi)..where((t) => t.progressivo.equals(progressivo))).get();
+    return (db.select(db.promoOmaggi)
+          ..where((t) => t.progressivo.equals(progressivo)))
+        .get();
   }
 
   Future<void> zapAll() {

+ 14 - 10
lib/database/daos/scadenze_dao.dart

@@ -1,23 +1,21 @@
 import 'package:drift/drift.dart';
 import '../database.dart';
-import '../tables/scadenze.dart';
 
 class ScadenzeDao {
   final AppDatabase db;
   ScadenzeDao(this.db);
 
   Future<List<Scadenza>> getScadenzeCliente(int idCliente) {
-    return (db.select(db.scadenze)..where((t) => t.idCliente.equals(idCliente))).get();
+    return (db.select(db.scadenze)..where((t) => t.idCliente.equals(idCliente)))
+        .get();
   }
 
-  Future<List<Scadenza>> getScadenzePerAgente(int idAgente) {
-    return (db.select(db.scadenze)
-      ..where((t) => t.idAgente.equals(idAgente) & (t.flgSoppressa.equals(0) | t.flgSoppressa.isNull()))
-    ).get();
-  }
-
-  Future<void> markAllAsDeleted() {
-    return (db.update(db.scadenze)).write(ScadenzeCompanion(stato: const Value('X')));
+  Future<List<VScadenzeData>> getScadenzePerAgente(int idAgente) {
+    return (db.select(db.vScadenze)
+          ..where((t) =>
+              t.idAgente.equals(idAgente) &
+              (t.flgSoppressa.equals(0) | t.flgSoppressa.isNull())))
+        .get();
   }
 
   Future<void> removeDeleted() {
@@ -31,4 +29,10 @@ class ScadenzeDao {
   Future<void> clearAll() {
     return db.delete(db.scadenze).go();
   }
+
+  Future<void> markAllAsDeleted(int idAgente) async {
+    final sql =
+        "update scadenze set stato='X' where idcliente in (select idcliente from clienti_Agente where idagente=$idAgente)";
+    await db.executor.runCustom(sql);
+  }
 }

+ 4 - 4
lib/database/daos/sconti_dao.dart

@@ -1,16 +1,16 @@
-import 'package:drift/drift.dart';
 import '../database.dart';
-import '../tables/sconti.dart';
 
 class ScontiDao {
   final AppDatabase db;
   ScontiDao(this.db);
 
   Future<Sconto?> findByCod(String code) {
-    return (db.select(db.sconti)..where((t) => t.codSconto.equals(code))).getSingleOrNull();
+    return (db.select(db.sconti)..where((t) => t.codSconto.equals(code)))
+        .getSingleOrNull();
   }
 
   Future<Sconto?> findById(int id) {
-    return (db.select(db.sconti)..where((t) => t.idSconto.equals(id))).getSingleOrNull();
+    return (db.select(db.sconti)..where((t) => t.idSconto.equals(id)))
+        .getSingleOrNull();
   }
 }

+ 7 - 5
lib/database/daos/tabelle_dao.dart

@@ -1,22 +1,24 @@
 import 'package:drift/drift.dart';
 import '../database.dart';
-import '../tables/tabelle.dart';
 
 class TabelleDao {
   final AppDatabase db;
   TabelleDao(this.db);
 
   Future<List<Tabella>> getByTipo(String tipo) {
-    return (db.select(db.tabelle)..where((t) => t.tipoTabella.equals(tipo))).get();
+    return (db.select(db.tabelle)..where((t) => t.tipoTabella.equals(tipo)))
+        .get();
   }
 
   Future<Tabella?> getByCode(String tipo, String code) {
     return (db.select(db.tabelle)
-      ..where((t) => t.tipoTabella.equals(tipo) & t.codTabella.equals(code))
-    ).getSingleOrNull();
+          ..where(
+              (t) => t.tipoTabella.equals(tipo) & t.codTabella.equals(code)))
+        .getSingleOrNull();
   }
 
   Future<Tabella?> findById(int id) {
-    return (db.select(db.tabelle)..where((t) => t.idTabella.equals(id))).getSingleOrNull();
+    return (db.select(db.tabelle)..where((t) => t.idTabella.equals(id)))
+        .getSingleOrNull();
   }
 }

+ 74 - 60
lib/database/database.dart

@@ -28,6 +28,7 @@ import 'views/top_item.dart';
 import 'views/top_customer.dart';
 import 'views/v_ordini.dart';
 import 'views/v_righe_ordine.dart';
+import 'views/v_scadenze.dart';
 
 part 'database.g.dart';
 
@@ -60,6 +61,7 @@ part 'database.g.dart';
     TopCustomer,
     VOrdini,
     VRigheOrdine,
+    VScadenze,
   ],
 )
 class AppDatabase extends _$AppDatabase {
@@ -80,60 +82,60 @@ class AppDatabase extends _$AppDatabase {
   }
 
   @override
-  int get schemaVersion => 9;
+  int get schemaVersion => 11;
 
   @override
   MigrationStrategy get migration => MigrationStrategy(
-    onCreate: (m) async {
-      await m.createAll();
-    },
-    onUpgrade: (m, from, to) async {
-      if (from < 2) {
-        await m.addColumn(
-          statisticheVendita,
-          statisticheVendita.codStatistiche,
-        );
-      }
-      if (from < 3) {
-        await customStatement(
-          'CREATE INDEX IF NOT EXISTS ndx_ro_idordine ON Righe_ordine(idOrdine)',
-        );
-      }
-      if (from < 4) {
-        await customStatement(
-          'CREATE INDEX IF NOT EXISTS NDX_FILIALE_REMOTO ON Filiali(idFilialeRemoto)',
-        );
-        await customStatement(
-          'CREATE INDEX IF NOT EXISTS NDX_CLI_COD ON Clienti(codCliente)',
-        );
-        await customStatement(
-          'CREATE INDEX IF NOT EXISTS NDX_CLI_REMOTO ON Clienti(idClienteRemoto)',
-        );
-        await customStatement(
-          'CREATE INDEX IF NOT EXISTS NDX_idOrdine_SEDE ON Ordini(idOrdineSede)',
-        );
-      }
-      if (from < 5) {
-        await customStatement(
-          'CREATE INDEX IF NOT EXISTS NDX_NUMERO_ORDINE ON Ordini(numeroOrdine)',
-        );
-      }
-      if (from < 6) {
-        await customStatement(
-          'CREATE INDEX IF NOT EXISTS NDX_filiali_cliente ON Filiali(idCliente)',
-        );
-      }
-      if (from < 7) {
-        await customStatement(
-          'CREATE INDEX IF NOT EXISTS ndx_stat_cliart ON Statistiche_vendita(idCliente, idArticolo)',
-        );
-        await customStatement(
-          'CREATE INDEX IF NOT EXISTS ndx_righeOrdine_promo ON Righe_ordine(idOrdine, rigaPromozione)',
-        );
-      }
-      if (from < 8) {
-        await customStatement('DROP VIEW IF EXISTS v_ordini');
-        await customStatement('''
+        onCreate: (m) async {
+          await m.createAll();
+        },
+        onUpgrade: (m, from, to) async {
+          if (from < 2) {
+            await m.addColumn(
+              statisticheVendita,
+              statisticheVendita.codStatistiche,
+            );
+          }
+          if (from < 3) {
+            await customStatement(
+              'CREATE INDEX IF NOT EXISTS ndx_ro_idordine ON Righe_ordine(idOrdine)',
+            );
+          }
+          if (from < 4) {
+            await customStatement(
+              'CREATE INDEX IF NOT EXISTS NDX_FILIALE_REMOTO ON Filiali(idFilialeRemoto)',
+            );
+            await customStatement(
+              'CREATE INDEX IF NOT EXISTS NDX_CLI_COD ON Clienti(codCliente)',
+            );
+            await customStatement(
+              'CREATE INDEX IF NOT EXISTS NDX_CLI_REMOTO ON Clienti(idClienteRemoto)',
+            );
+            await customStatement(
+              'CREATE INDEX IF NOT EXISTS NDX_idOrdine_SEDE ON Ordini(idOrdineSede)',
+            );
+          }
+          if (from < 5) {
+            await customStatement(
+              'CREATE INDEX IF NOT EXISTS NDX_NUMERO_ORDINE ON Ordini(numeroOrdine)',
+            );
+          }
+          if (from < 6) {
+            await customStatement(
+              'CREATE INDEX IF NOT EXISTS NDX_filiali_cliente ON Filiali(idCliente)',
+            );
+          }
+          if (from < 7) {
+            await customStatement(
+              'CREATE INDEX IF NOT EXISTS ndx_stat_cliart ON Statistiche_vendita(idCliente, idArticolo)',
+            );
+            await customStatement(
+              'CREATE INDEX IF NOT EXISTS ndx_righeOrdine_promo ON Righe_ordine(idOrdine, rigaPromozione)',
+            );
+          }
+          if (from < 8) {
+            await customStatement('DROP VIEW IF EXISTS v_ordini');
+            await customStatement('''
           CREATE VIEW v_ordini AS
           SELECT
             a.codAgente,
@@ -152,19 +154,31 @@ class AppDatabase extends _$AppDatabase {
           LEFT JOIN Filiali f ON f.idFiliale = o.idFiliale
           JOIN Agenti a ON a.idAgente = o.idAgente
         ''');
-        await customStatement('DROP VIEW IF EXISTS v_righe_ordine');
-        await customStatement('''
+            await customStatement('DROP VIEW IF EXISTS v_righe_ordine');
+            await customStatement('''
           CREATE VIEW v_righe_ordine AS
           SELECT ro.*, a.codArticolo, a.descrizione, l.codListino, l.descrizione AS Listino
           FROM Righe_ordine ro
           JOIN Articoli a ON a.idArticolo = ro.idArticolo
           LEFT JOIN Listini l ON l.idListino = ro.idListino
         ''');
-      }
-      if (from < 9) {
-        await m.addColumn(clienti, clienti.creditScoreColor);
-        await m.addColumn(clienti, clienti.dataCreditScore);
-      }
-    },
-  );
+          }
+          if (from < 9) {
+            await m.addColumn(clienti, clienti.creditScoreColor);
+            await m.addColumn(clienti, clienti.dataCreditScore);
+          }
+          if (from < 10) {
+            // await m.addColumn(listini, listini.listinoAgente);
+          }
+          if (from < 11) {
+            await customStatement('DROP VIEW IF EXISTS v_scadenze');
+            await customStatement('''
+          CREATE VIEW v_scadenze AS
+          SELECT s.*, c.codCliente, c.ragioneSociale
+          FROM Scadenze s
+          LEFT JOIN Clienti c ON c.idCliente = s.idCliente
+        ''');
+          }
+        },
+      );
 }

File diff suppressed because it is too large
+ 1319 - 235
lib/database/database.g.dart


+ 6 - 5
lib/database/tables/agenti.dart

@@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
 
 @DataClassName('Agente')
 class Agenti extends Table {
-  IntColumn get idAgente => integer().named('idAgente').autoIncrement()();
+  IntColumn get idAgente => integer().named('idAgente')();
   TextColumn get codAgente => text().named('codAgente').nullable()();
   TextColumn get ragioneSociale => text().named('ragioneSociale').nullable()();
   TextColumn get pwd => text().named('pwd').nullable()();
@@ -14,12 +14,13 @@ class Agenti extends Table {
   TextColumn get cap => text().named('cap').nullable()();
   TextColumn get provincia => text().named('provincia').nullable()();
   TextColumn get frazione => text().named('frazione').nullable()();
-  IntColumn get attivo =>
-      integer().named('attivo').nullable().withDefault(const Constant(1))();
-  IntColumn get modificaPrezzi =>
-      integer().named('modificaPrezzi').nullable()();
+  IntColumn get attivo => integer().named('attivo').nullable().withDefault(const Constant(1))();
+  IntColumn get modificaPrezzi => integer().named('modificaPrezzi').nullable()();
   TextColumn get capoArea => text().named('capoArea').nullable()();
 
+  @override
+  Set<Column> get primaryKey => {idAgente};
+
   @override
   String get tableName => 'Agenti';
 }

+ 1 - 0
lib/database/tables/listini.dart

@@ -10,6 +10,7 @@ class Listini extends Table {
   IntColumn get attivo =>
       integer().named('attivo').withDefault(const Constant(1))();
   TextColumn get dataModifica => text().named('dataModifica').nullable()();
+  IntColumn get listinoAgente => integer().named('listinoAgente').nullable()();
 
   @override
   String get tableName => 'Listini';

+ 1 - 1
lib/database/tables/scadenze.dart

@@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
 
 @DataClassName('Scadenza')
 class Scadenze extends Table {
-  IntColumn get idScadenza => integer().autoIncrement()();
+  IntColumn get idScadenza => integer().named('idScadenza').autoIncrement()();
   IntColumn get idCliente => integer().named('idCliente').nullable()();
   IntColumn get idAgente => integer().named('idAgente').nullable()();
   TextColumn get chiave => text().named('chiave').nullable()();

+ 1 - 1
lib/database/tables/statistiche_vendita.dart

@@ -2,7 +2,7 @@ import 'package:drift/drift.dart';
 
 @DataClassName('StatisticheVenditaData')
 class StatisticheVendita extends Table {
-  IntColumn get id => integer().autoIncrement()();
+  IntColumn get id => integer()();
   IntColumn get idAgente => integer().named('idAgente').nullable()();
   IntColumn get idCliente => integer().named('idCliente').nullable()();
   IntColumn get idArticolo => integer().named('idArticolo').nullable()();

+ 2 - 3
lib/database/views/articoli_con_promo.dart

@@ -1,6 +1,4 @@
 import 'package:drift/drift.dart';
-import '../tables/articoli.dart';
-import '../tables/promozioni.dart';
 
 class ArticoliConPromo extends Table {
   IntColumn get idArticolo => integer().named('idArticolo')();
@@ -8,7 +6,8 @@ class ArticoliConPromo extends Table {
   TextColumn get descrizione => text().named('descrizione').nullable()();
   RealColumn get prezzoBase => real().named('prezzoBase').nullable()();
   IntColumn get attivo => integer().named('attivo').nullable()();
-  IntColumn get nonDisponibile => integer().named('nonDisponibile').nullable()();
+  IntColumn get nonDisponibile =>
+      integer().named('nonDisponibile').nullable()();
   IntColumn get inPromo => integer().named('inPromo').nullable()();
   RealColumn get ultimoPrezzo => real().named('ultimoPrezzo').nullable()();
   TextColumn get lastDateSold => text().named('lastDateSold').nullable()();

+ 35 - 0
lib/database/views/v_scadenze.dart

@@ -0,0 +1,35 @@
+import 'package:drift/drift.dart';
+
+class VScadenze extends Table {
+  IntColumn get idScadenza => integer().named('idScadenza')();
+  IntColumn get idCliente => integer().named('idCliente').nullable()();
+  IntColumn get idAgente => integer().named('idAgente').nullable()();
+  TextColumn get chiave => text().named('chiave').nullable()();
+  TextColumn get numPartita => text().named('numPartita').nullable()();
+  TextColumn get dataScadenza => text().named('dataScadenza').nullable()();
+  TextColumn get segno => text().named('segno').nullable()();
+  RealColumn get importoFattura => real().named('importoFattura').nullable()();
+  TextColumn get valuta => text().named('valuta').nullable()();
+  TextColumn get numDoc => text().named('numDoc').nullable()();
+  TextColumn get serieDoc => text().named('serieDoc').nullable()();
+  TextColumn get dataDoc => text().named('dataDoc').nullable()();
+  RealColumn get importoScadenza => real().named('importoScadenza').nullable()();
+  RealColumn get saldo => real().named('saldo').nullable()();
+  TextColumn get tipoPagamento => text().named('tipoPagamento').nullable()();
+  IntColumn get flgSoppressa => integer().named('flgSoppressa').nullable()();
+  RealColumn get importoAbbuono => real().named('importoAbbuono').nullable()();
+  TextColumn get note => text().named('note').nullable()();
+  TextColumn get dataModifica => text().named('dataModifica').nullable()();
+  TextColumn get stato => text().named('stato').nullable()();
+  TextColumn get codCliente => text().named('codCliente').nullable()();
+  TextColumn get ragioneSociale => text().named('ragioneSociale').nullable()();
+
+  @override
+  Set<Column> get primaryKey => {idScadenza};
+
+  @override
+  String get tableName => 'v_scadenze';
+
+  @override
+  bool get withoutRowId => true;
+}

+ 29 - 0
lib/providers/auth_provider.dart

@@ -1,8 +1,13 @@
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:ristoris/ui/pages/payments/payments_page.dart';
 import '../../core/constants.dart';
 import '../../database/database.dart';
 import 'database_provider.dart';
 import '../../services/auth_service.dart';
+import 'article_provider.dart';
+import 'customer_provider.dart';
+import 'dashboard_provider.dart';
+import 'order_provider.dart';
 import 'sync_provider.dart';
 
 const _kRememberedUsername = 'REMEMBERED_USERNAME';
@@ -109,6 +114,30 @@ class AuthNotifier extends Notifier<AuthState> {
     state = const AuthState(isInitialized: true);
   }
 
+  Future<void> switchAgent(Agente newAgent) async {
+    final agentiDao = ref.read(agentiDaoProvider);
+    final agente = await agentiDao.authenticate(
+      newAgent.codAgente ?? '',
+      newAgent.pwd ?? '',
+    );
+    if (agente != null) {
+      state = AuthState(agente: agente, isInitialized: true);
+      ref.invalidate(articleSearchProvider);
+      ref.invalidate(activeArticleSearchProvider);
+      ref.invalidate(priceCodesProvider);
+      ref.invalidate(customerListProvider);
+      ref.invalidate(customerSearchProvider);
+      ref.invalidate(customerDestinationsProvider);
+      ref.invalidate(customerPaymentsProvider);
+      ref.invalidate(ordersViewProvider);
+      ref.invalidate(ordersSearchViewProvider);
+      ref.invalidate(orderStatsProvider);
+      ref.invalidate(orderProvider);
+      ref.invalidate(dashboardProvider);
+      ref.invalidate(paymentsProvider);
+    }
+  }
+
   void resetToInit() {
     state = const AuthState(isInitialized: false);
   }

+ 2 - 4
lib/providers/dashboard_provider.dart

@@ -1,9 +1,6 @@
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import '../database/database.dart';
 import 'database_provider.dart';
-import '../database/daos/clienti_dao.dart';
-import '../database/daos/articoli_dao.dart';
-import '../database/daos/ordini_dao.dart';
 
 class DashboardData {
   final List<TopCustomerData> topCustomers;
@@ -21,7 +18,8 @@ class DashboardData {
   });
 }
 
-final dashboardProvider = FutureProvider.family<DashboardData, int>((ref, idAgente) async {
+final dashboardProvider =
+    FutureProvider.family<DashboardData, int>((ref, idAgente) async {
   final clientiDao = ref.read(clientiDaoProvider);
   final articoliDao = ref.read(articoliDaoProvider);
   final ordiniDao = ref.read(ordiniDaoProvider);

+ 4 - 0
lib/providers/database_provider.dart

@@ -31,3 +31,7 @@ final scontiDaoProvider = Provider<ScontiDao>((ref) => ScontiDao(ref.read(databa
 final pagamentiDaoProvider = Provider<PagamentiDao>((ref) => PagamentiDao(ref.read(databaseProvider)));
 final tabelleDaoProvider = Provider<TabelleDao>((ref) => TabelleDao(ref.read(databaseProvider)));
 final clientiAgenteDaoProvider = Provider<ClientiAgenteDao>((ref) => ClientiAgenteDao(ref.read(databaseProvider)));
+
+final agentiListProvider = FutureProvider<List<Agente>>((ref) {
+  return ref.read(agentiDaoProvider).listAll();
+});

+ 38 - 21
lib/providers/sync_provider.dart

@@ -1,4 +1,6 @@
 import 'package:flutter_riverpod/flutter_riverpod.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';
@@ -25,7 +27,8 @@ final imageServiceProvider = Provider<ImageService>((ref) {
 });
 
 final agenteSyncServiceProvider = Provider<AgenteSyncService>((ref) {
-  return AgenteSyncService(ref.read(apiServiceProvider));
+  final agentiDao = ref.read(agentiDaoProvider);
+  return AgenteSyncService(ref.read(apiServiceProvider), agentiDao);
 });
 
 final clienteSyncServiceProvider = Provider<ClienteSyncService>((ref) {
@@ -104,30 +107,27 @@ class SyncNotifier extends Notifier<SyncState> {
         final token = await authService.loginRemote(codAgente, agente!.pwd!);
         ref.read(apiServiceProvider).token = token;
       }
-      _update('Token JWT acquisito', 0.02);
+      _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('lastUpdate') ?? '200001010000');
+
+      await agenteSync.downloadAgenti(idAgente, codAgente, lastUpdateStr);
 
       await clienteSync.uploadNewCustomers(idAgente, codAgente);
       _update('Clienti inviati', 0.12);
-      await clienteSync.downloadCustomers(idAgente, codAgente, lastUpdateStr);
-      _update('Clienti attivi scaricati', 0.16);
+
+      // cicla per tutti gli agenti a db
+      List<Agente> agenti = await agentiDao.listAll();
+
       await filialeSync.uploadNewDestinations(idAgente, codAgente);
       _update('Destinazioni inviate', 0.18);
-      await filialeSync.downloadDestinations(
-        idAgente,
-        codAgente,
-        lastUpdateStr,
-      );
-      _update('Destinazioni scaricate', 0.20);
       await ordineSync.uploadNewOrders(idAgente, codAgente);
       _update('Ordini inviati', 0.22);
 
@@ -143,20 +143,36 @@ class SyncNotifier extends Notifier<SyncState> {
       ]);
       _update('Dati di riferimento scaricati', 0.60);
 
-      await Future.wait([
-        refSync.downloadScadenze(idAgente, codAgente, lastUpdateStr),
-        refSync.downloadListiniCliente(idAgente, codAgente, lastUpdateStr),
-        refSync.downloadScontiCliente(idAgente, codAgente, lastUpdateStr),
-        ordineSync.downloadRemoteOrders(idAgente, codAgente, lastUpdateStr),
-      ]);
+      for (int i = 0; i < agenti.length; i++) {
+        final agente = agenti[i];
+
+        _update('Aggiornamento per codice ${agente.codAgente} ', 0.16);
+
+        await clienteSync.downloadCustomers(agente.idAgente, agente.codAgente!, lastUpdateStr);
+        _update('(${agente.codAgente}) Clienti attivi scaricati', 0.16);
+
+        await filialeSync.downloadDestinations(
+          agente.idAgente,
+          agente.codAgente!,
+          lastUpdateStr,
+        );
+        _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),
+        ]);
+      }
+
       _update('Dati per agente scaricati', 0.92);
 
       await refSync.downloadParametro('PALLET_PERC');
       _update('Parametri verificati', 0.98);
 
       final now = DateTime.now();
-      final newLastUpdate =
-          '${now.year}${_pad(now.month)}${_pad(now.day)}${_pad(now.hour)}${_pad(now.minute)}';
+      final newLastUpdate = '${now.year}${_pad(now.month)}${_pad(now.day)}${_pad(now.hour)}${_pad(now.minute)}';
       await parametriDao.set('lastUpdate', newLastUpdate);
 
       state = const SyncState(
@@ -174,6 +190,7 @@ class SyncNotifier extends Notifier<SyncState> {
       ref.invalidate(ordersViewProvider);
       ref.invalidate(ordersSearchViewProvider);
       ref.invalidate(orderStatsProvider);
+      ref.invalidate(orderProvider);
       ref.invalidate(dashboardProvider);
       ref.invalidate(paymentsProvider);
     } catch (e) {

+ 1 - 2
lib/services/auth_service.dart

@@ -4,7 +4,6 @@ import 'package:http/http.dart' as http;
 import '../database/daos/agenti_dao.dart';
 import '../database/daos/parametri_dao.dart';
 import '../database/database.dart';
-import '../database/tables/parametri.dart';
 import '../core/constants.dart';
 import 'api_service.dart';
 
@@ -61,7 +60,7 @@ class AuthService {
     await _parametriDao.set(AppConstants.kUrlParam, url);
     await _agentiDao.insertAgente(
       AgentiCompanion(
-	idAgente: Value(idAgente),
+        idAgente: Value(idAgente),
         codAgente: Value(codAgente),
         pwd: Value(pwd),
         ragioneSociale: Value(ragioneSociale ?? codAgente),

+ 65 - 1
lib/services/sync/agente_sync_service.dart

@@ -1,9 +1,17 @@
+import 'package:drift/drift.dart';
+import 'package:ristoris/database/daos/agenti_dao.dart';
+import 'package:ristoris/database/database.dart';
+
 import '../api_service.dart';
 
 class AgenteSyncService {
   final ApiService _api;
+  final AgentiDao _agentiDao;
 
-  AgenteSyncService(this._api);
+  AgenteSyncService(
+    this._api,
+    this._agentiDao,
+  );
 
   Future<bool> checkAgent(String codAgente) async {
     try {
@@ -13,4 +21,60 @@ class AgenteSyncService {
       return false;
     }
   }
+
+  Future<int> downloadAgenti(
+    int idAgente,
+    String codAgente,
+    String lastUpdate,
+  ) async {
+    try {
+      final data = await _api.getList(
+        '/rest/agente/updates/$codAgente/$lastUpdate',
+      );
+      final db = _agentiDao.db;
+      await db.transaction(() async {
+        for (final json in data) {
+          if (codAgente == json['codAgente'] as String) {
+            await setNuovoIdAgente(codAgente, json['idAgente'] as int? ?? 0);
+          }
+        }
+
+        await db.batch((batch) {
+          for (final json in data) {
+            batch.insert(
+                db.agenti,
+                Agente(
+                  idAgente: json['idAgente'] as int? ?? 0,
+                  codAgente: json['codAgente'] as String?,
+                  pwd: json['pwd'] as String?,
+                  ragioneSociale: json['ragioneSociale'] as String?,
+                  attivo: json['attivo'] as int? ?? 0,
+                  cap: json['cap'] as String?,
+                  capoArea: json['capoArea'] as String?,
+                  cellulare: json['cellulare'] as String?,
+                  citta: json['citta'] as String?,
+                  email: json['email'] as String?,
+                  frazione: json['frazione'] as String?,
+                  indirizzo: json['indirizzo'] as String?,
+                  modificaPrezzi: json['modificaPrezzi'] as int? ?? 0,
+                  provincia: json['provincia'] as String?,
+                  telefono: json['telefono'] as String?,
+                ),
+                mode: InsertMode.insertOrReplace);
+          }
+        });
+      });
+
+      return data.length;
+    } catch (_) {
+      return 0;
+    }
+  }
+
+  Future<void> setNuovoIdAgente(String codAgente, int idAgente) async {
+    Agente? agente = await _agentiDao.findByCode(codAgente);
+    if (agente != null && agente.idAgente != idAgente) {
+      await _agentiDao.updateIdAgente(agente.idAgente, idAgente);
+    }
+  }
 }

+ 28 - 19
lib/services/sync/ordine_sync_service.dart

@@ -1,5 +1,8 @@
 import 'package:drift/drift.dart';
 import 'package:ristoris/database/daos/agenti_dao.dart';
+import 'package:ristoris/database/daos/clienti_agente_dao.dart';
+import 'package:ristoris/providers/database_provider.dart';
+import 'package:riverpod/src/provider.dart';
 import '../api_service.dart';
 import '../../database/database.dart';
 import '../../database/daos/ordini_dao.dart';
@@ -92,17 +95,22 @@ class OrdineSyncService {
       );
       final db = _ordiniDao.db;
       final cacheCliente = <String, int?>{};
-      final cacheAgente = <int, Agente>{};
+      final cacheAgente = <String, Agente>{};
       final cacheFilialeById = <int, int?>{};
       final cacheFilialeByRemoteId = <int, int?>{};
       int count = 0;
 
+      Agente? agente = null;
+
       await db.transaction(() async {
         for (final json in data) {
           final idOrdine = json['idOrdine'] as int?;
           final idOrdineAgente = json['idOrdineAgente'] as int?;
           final numeroOrdineSede = json['numeroOrdineSede'] as int?;
           final stato = json['stato'] as String?;
+          if (agente == null) {
+            agente = await _agentiDao.findById(json['idAgente'] as int);
+          }
 
           Ordine? existing;
           if (idOrdineAgente != null) {
@@ -121,31 +129,35 @@ class OrdineSyncService {
               final deleteId = existing.idOrdine;
               await (db.delete(
                 db.ordini,
-              )..where((t) => t.idOrdine.equals(deleteId))).go();
+              )..where((t) => t.idOrdine.equals(deleteId)))
+                  .go();
             }
             continue;
           }
 
-          final codAgente = json['codAgente'] as String?;
-          Agente? agente = cacheAgente[idAgente];
-          if (agente == null) {
-            agente = await _agentiDao.findByCode(codAgente!);
-            if (agente != null) {
-              cacheAgente[idAgente] = agente;
-            }
-          }
-
           final codCliente = json['codCliente'] as String?;
           int? localIdCliente;
           if (codCliente != null) {
             localIdCliente = cacheCliente[codCliente];
             if (localIdCliente == null) {
-              final c = await _clientiDao.getByCode(idAgente, codCliente);
+              final c = await _clientiDao.getByCode(agente!.idAgente, codCliente);
               localIdCliente = c?.idCliente;
               cacheCliente[codCliente] = localIdCliente;
             }
           }
 
+          // recupera l'agente attivo per il cliente
+/*
+          if (cacheAgente.containsKey(codCliente)) {
+            agente = cacheAgente[codCliente];
+          } else {
+            agente = await _clientiDao.getAgenteOf(codCliente);
+            if (agente != null) {
+              cacheAgente[codCliente!] = agente;
+            }
+          }
+*/
+
           final idFilialeRemoto = json['idFilialeRemoto'] as int?;
           final idFiliale = json['idFiliale'] as int?;
           int? localIdFiliale;
@@ -167,9 +179,7 @@ class OrdineSyncService {
           }
 
           final ordineCompanion = OrdiniCompanion(
-            idOrdine: existing != null
-                ? Value(existing.idOrdine)
-                : Value.absent(),
+            idOrdine: existing != null ? Value(existing.idOrdine) : Value.absent(),
             idOrdineSede: Value(idOrdine),
             numeroOrdine: Value(json['numeroOrdine'] as int?),
             numeroOrdineSede: Value(numeroOrdineSede?.toString()),
@@ -187,14 +197,13 @@ class OrdineSyncService {
             idPagamento: Value(json['idPagamento'] as int?),
           );
 
-          final insertedId = await db
-              .into(db.ordini)
-              .insertOnConflictUpdate(ordineCompanion);
+          final insertedId = await db.into(db.ordini).insertOnConflictUpdate(ordineCompanion);
           final savedId = existing?.idOrdine ?? insertedId;
 
           await (db.delete(
             db.righeOrdine,
-          )..where((t) => t.idOrdine.equals(savedId))).go();
+          )..where((t) => t.idOrdine.equals(savedId)))
+              .go();
 
           final righe = json['righeOrdine'] as List<dynamic>?;
           if (righe != null && righe.isNotEmpty) {

+ 134 - 104
lib/services/sync/reference_sync_service.dart

@@ -142,20 +142,16 @@ class ReferenceSyncService {
               codArticolo: json['codArticolo'] as String? ?? '',
               descrizione: json['descrizione'] as String?,
               ean: json['ean'] as String?,
-              idIva: (json['iva'] as Map<String, dynamic>?)?['idTabella']
-                  as int?,
-              idClassifica1:
-                  (json['classifica1'] as Map<String, dynamic>?)?['idTabella']
-                      as int?,
-              idClassifica2:
-                  (json['classifica2'] as Map<String, dynamic>?)?['idTabella']
-                      as int?,
-              idClassifica3:
-                  (json['classifica3'] as Map<String, dynamic>?)?['idTabella']
-                      as int?,
-              idClassifica4:
-                  (json['classifica4'] as Map<String, dynamic>?)?['idTabella']
-                      as int?,
+              idIva:
+                  (json['iva'] as Map<String, dynamic>?)?['idTabella'] as int?,
+              idClassifica1: (json['classifica1']
+                  as Map<String, dynamic>?)?['idTabella'] as int?,
+              idClassifica2: (json['classifica2']
+                  as Map<String, dynamic>?)?['idTabella'] as int?,
+              idClassifica3: (json['classifica3']
+                  as Map<String, dynamic>?)?['idTabella'] as int?,
+              idClassifica4: (json['classifica4']
+                  as Map<String, dynamic>?)?['idTabella'] as int?,
               peso: _toDouble(json['peso']),
               giacenza: _toDouble(json['giacenza']),
               unitaMisura: json['unitaMisura'] as String?,
@@ -197,6 +193,7 @@ class ReferenceSyncService {
               validoA: json['validoA'] as String?,
               attivo: json['attivo'] as int? ?? 1,
               dataModifica: json['dataModifica'] as String?,
+              listinoAgente: json['listinoAgente'] as int?,
             ),
             mode: InsertMode.insertOrReplace,
           );
@@ -226,11 +223,9 @@ class ReferenceSyncService {
               idListino:
                   listino?['idListino'] as int? ?? json['idListino'] as int?,
               idArticolo:
-                  articolo?['idArticolo'] as int? ??
-                  json['idArticolo'] as int?,
+                  articolo?['idArticolo'] as int? ?? json['idArticolo'] as int?,
               prezzo: _toDouble(json['prezzo']),
-              idSconto:
-                  sconto?['idSconto'] as int? ?? json['idSconto'] as int?,
+              idSconto: sconto?['idSconto'] as int? ?? json['idSconto'] as int?,
               validoDa: json['validoDa'] as String?,
               validoA: json['validoA'] as String?,
               attivo: json['attivo'] as int? ?? 1,
@@ -267,8 +262,7 @@ class ReferenceSyncService {
         if (jsonIdCliente != null) {
           localIdCliente = cacheCliente[jsonIdCliente];
           if (localIdCliente == null) {
-            final c =
-                await _clientiDao.getByRemoteId(idAgente, jsonIdCliente);
+            final c = await _clientiDao.getByRemoteId(idAgente, jsonIdCliente);
             localIdCliente = c?.idCliente;
             cacheCliente[jsonIdCliente] = localIdCliente;
           }
@@ -280,8 +274,7 @@ class ReferenceSyncService {
             idListino: listino?['idListino'] as int?,
             idCliente: localIdCliente,
             idArticolo:
-                articolo?['idArticolo'] as int? ??
-                json['idArticolo'] as int?,
+                articolo?['idArticolo'] as int? ?? json['idArticolo'] as int?,
             idSconto: sconto?['idSconto'] as int?,
             validoDa: json['validoDa'] as String?,
             validoA: json['validoA'] as String?,
@@ -295,7 +288,8 @@ class ReferenceSyncService {
       }
       await db.batch((batch) {
         for (final item in items) {
-          batch.insert(db.listiniCliente, item, mode: InsertMode.insertOrReplace);
+          batch.insert(db.listiniCliente, item,
+              mode: InsertMode.insertOrReplace);
         }
       });
       return data.length;
@@ -352,50 +346,51 @@ class ReferenceSyncService {
         for (final json in data) {
           final dto = SyncPromoDto.fromJson(json as Map<String, dynamic>);
           await db.into(db.promozioni).insert(
-            Promozione(
-              progressivo: dto.progressivo ?? 0,
-              tipoPromozione: dto.tipoPromozione,
-              descrizione: dto.descrizione,
-              dataInizio: dto.dataInizio,
-              dataFine: dto.dataFine,
-              limiteQta: dto.limiteQta,
-              quantitaOmaggio: dto.quantitaOmaggio,
-              limiteImporto: dto.limiteImporto,
-              stato: dto.stato,
-              priorita: dto.priorita ?? 0,
-              codice: dto.codice,
-              idArticoloOmaggio: dto.articoloOmaggio?.idArticolo,
-              idSconto: dto.sconto?.idSconto,
-              idListino: dto.listinoOmaggio?.idListino,
-            ),
-          );
+                Promozione(
+                  progressivo: dto.progressivo ?? 0,
+                  tipoPromozione: dto.tipoPromozione,
+                  descrizione: dto.descrizione,
+                  dataInizio: dto.dataInizio,
+                  dataFine: dto.dataFine,
+                  limiteQta: dto.limiteQta,
+                  quantitaOmaggio: dto.quantitaOmaggio,
+                  limiteImporto: dto.limiteImporto,
+                  stato: dto.stato,
+                  priorita: dto.priorita ?? 0,
+                  codice: dto.codice,
+                  idArticoloOmaggio: dto.articoloOmaggio?.idArticolo,
+                  idSconto: dto.sconto?.idSconto,
+                  idListino: dto.listinoOmaggio?.idListino,
+                ),
+              );
           for (final art in dto.promozioneArticoli) {
             await db.into(db.promoArticoli).insert(
-              PromoArticolo(
-                progressivo: dto.progressivo ?? 0,
-                riga: art.promozioneArticoloKey?.riga ?? 0,
-                quantitaOmaggio: art.promozioneArticoloKey?.riga?.toDouble(),
-                idArticolo: art.articolo?.idArticolo,
-              ),
-            );
+                  PromoArticolo(
+                    progressivo: dto.progressivo ?? 0,
+                    riga: art.promozioneArticoloKey?.riga ?? 0,
+                    quantitaOmaggio:
+                        art.promozioneArticoloKey?.riga?.toDouble(),
+                    idArticolo: art.articolo?.idArticolo,
+                  ),
+                );
           }
           for (final l in dto.promozioneListini) {
             await db.into(db.promoListini).insert(
-              PromoListino(
-                progressivo: dto.progressivo ?? 0,
-                idListino: l.promozioneListinoKey?.idListino ?? 0,
-              ),
-            );
+                  PromoListino(
+                    progressivo: dto.progressivo ?? 0,
+                    idListino: l.promozioneListinoKey?.idListino ?? 0,
+                  ),
+                );
           }
           for (final om in dto.promozioneOmaggio) {
             await db.into(db.promoOmaggi).insert(
-              PromoOmaggio(
-                progressivo: dto.progressivo ?? 0,
-                riga: om.promozioneOmaggioKey?.riga ?? 0,
-                idArticolo: om.articolo?.idArticolo,
-                quantita: om.quantita,
-              ),
-            );
+                  PromoOmaggio(
+                    progressivo: dto.progressivo ?? 0,
+                    riga: om.promozioneOmaggioKey?.riga ?? 0,
+                    idArticolo: om.articolo?.idArticolo,
+                    quantita: om.quantita,
+                  ),
+                );
           }
           count++;
         }
@@ -411,43 +406,61 @@ class ReferenceSyncService {
     String codAgente,
     String lastUpdate,
   ) async {
+    Map<int, Cliente> cacheCliente = {};
+
     try {
       final data = await _api.getList(
         '/rest/scadenze/updates/$codAgente/$lastUpdate',
       );
       final db = _scadenzeDao.db;
       await db.transaction(() async {
-        await db.delete(db.scadenze).go();
-        await db.batch((batch) {
+        //await _scadenzeDao.markAllAsDeleted(idAgente);
+        await db.batch((batch) async {
           for (final json in data) {
-            batch.insert(
-              db.scadenze,
-              Scadenza(
-                idScadenza: json['idScadenza'] as int? ?? 0,
-                idCliente: json['idCliente'] as int?,
-                idAgente: json['idAgente'] as int?,
-                chiave: json['chiave'] as String?,
-                numPartita: json['numPartita'] as String?,
-                dataScadenza: json['dataScadenza'] as String?,
-                segno: json['segno'] as String?,
-                importoFattura: _toDouble(json['importoFattura']),
-                valuta: json['valuta'] as String?,
-                numDoc: json['numDoc'] as String?,
-                serieDoc: json['serieDoc'] as String?,
-                dataDoc: json['dataDoc'] as String?,
-                importoScadenza: _toDouble(json['importoScadenza']),
-                saldo: _toDouble(json['saldo']),
-                tipoPagamento: json['tipoPagamento'] as String?,
-                flgSoppressa: json['flgSoppressa'] as int?,
-                importoAbbuono: _toDouble(json['importoAbbuono']),
-                note: json['note'] as String?,
-                dataModifica: json['dataModifica'] as String?,
-                stato: json['stato'] as String?,
-              ),
-            );
+            // cerca il cliente nella cache usando idClienteRemoto
+            final idClienteRemoto = json['idCliente'] as int?;
+            Cliente? cliente = cacheCliente[idClienteRemoto];
+            if (cliente == null) {
+              cliente =
+                  await _clientiDao.getByRemoteId(idAgente, idClienteRemoto!);
+              if (cliente != null) {
+                cacheCliente[idClienteRemoto] = cliente;
+              }
+            }
+
+            if (cliente != null) {
+              batch.insert(
+                db.scadenze,
+                Scadenza(
+                  idScadenza: json['idScadenza'] as int? ?? 0,
+                  idCliente: cliente.idCliente,
+                  idAgente: idAgente,
+                  chiave: json['chiave'] as String?,
+                  numPartita: json['numPartita'] as String?,
+                  dataScadenza: json['dataScadenza'] as String?,
+                  segno: json['segno'] as String?,
+                  importoFattura: _toDouble(json['importoFattura']),
+                  valuta: json['valuta'] as String?,
+                  numDoc: json['numDoc'] as String?,
+                  serieDoc: json['serieDoc'] as String?,
+                  dataDoc: json['dataDoc'] as String?,
+                  importoScadenza: _toDouble(json['importoScadenza']),
+                  saldo: _toDouble(json['saldo']),
+                  tipoPagamento: json['tipoPagamento'] as String?,
+                  flgSoppressa: json['flgSoppressa'] as int?,
+                  importoAbbuono: _toDouble(json['importoAbbuono']),
+                  note: json['note'] as String?,
+                  dataModifica: json['dataModifica'] as String?,
+                  stato: json['stato'] as String?,
+                ),
+                mode: InsertMode.insertOrReplace,
+              );
+            }
           }
         });
       });
+
+      _scadenzeDao.removeDeleted();
       return data.length;
     } catch (ex) {
       print('Error downloading scadenze: $ex');
@@ -460,31 +473,48 @@ class ReferenceSyncService {
     String codAgente,
     String lastUpdate,
   ) async {
+    Map<int, Cliente> cacheCliente = {};
+
     try {
       final data = await _api.getList(
         '/rest/statistiche/updates/$codAgente/$lastUpdate',
       );
       final db = _statisticheDao.db;
       await db.transaction(() async {
-        await db.delete(db.statisticheVendita).go();
-        await db.batch((batch) {
+        await db.batch((batch) async {
           for (final json in data) {
-            batch.insert(
-              db.statisticheVendita,
-              StatisticheVenditaData(
-                id: json['id'] as int? ?? 0,
-                idAgente: json['idAgente'] as int?,
-                idCliente: json['idCliente'] as int?,
-                idArticolo: json['idArticolo'] as int?,
-                codStatistiche: json['codStatistiche'] as String?,
-                riga: json['riga'] as int?,
-                numeroFattura: json['numeroFattura'] as String?,
-                dataFattura: json['dataFattura'] as String?,
-                qta: _toDouble(json['qta']),
-                importo: _toDouble(json['importo']),
-                tipoVendita: json['tipoVendita'] as String?,
-              ),
-            );
+            final idAgente = json['idAgente'] as int?;
+
+            // cerca il cliente nella cache usando idClienteRemoto
+            final idClienteRemoto = json['idCliente'] as int?;
+            Cliente? cliente = cacheCliente[idClienteRemoto];
+            if (cliente == null) {
+              cliente =
+                  await _clientiDao.getByRemoteId(idAgente!, idClienteRemoto!);
+              if (cliente != null) {
+                cacheCliente[idClienteRemoto] = cliente;
+              }
+            }
+
+            if (cliente != null) {
+              batch.insert(
+                db.statisticheVendita,
+                StatisticheVenditaData(
+                  id: json['id'] as int? ?? 0,
+                  idAgente: json['idAgente'] as int?,
+                  idCliente: cliente.idCliente,
+                  idArticolo: json['idArticolo'] as int?,
+                  codStatistiche: json['codStatistiche'] as String?,
+                  riga: json['riga'] as int?,
+                  numeroFattura: json['numeroFattura'] as String?,
+                  dataFattura: json['dataFattura'] as String?,
+                  qta: _toDouble(json['qta']),
+                  importo: _toDouble(json['importo']),
+                  tipoVendita: json['tipoVendita'] as String?,
+                ),
+                mode: InsertMode.insertOrReplace,
+              );
+            }
           }
         });
       });

+ 66 - 40
lib/ui/pages/catalog/catalog_page.dart

@@ -1,6 +1,5 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:intl/intl.dart';
 import '../../../database/database.dart';
 import '../../../providers/article_provider.dart';
 import '../../widgets/article_image.dart';
@@ -35,39 +34,48 @@ class _CatalogPageState extends ConsumerState<CatalogPage> {
         child: Column(
           children: [
             sw.SearchBarWidget(
-            controller: _searchCtrl,
-            hint: 'Cerca articolo...',
-            onChanged: (v) => setState(() => _filter = v),
-            onClear: () { _searchCtrl.clear(); setState(() => _filter = ''); },
-          ),
-          Expanded(
-            child: articlesAsync.when(
-              data: (articles) {
-                if (articles.isEmpty) return const EmptyState(icon: Icons.inventory_2_outlined, title: 'Nessun articolo');
-                if (ResponsiveBreakpoints.isPhone(context)) {
-                  return ListView.builder(
+              controller: _searchCtrl,
+              hint: 'Cerca articolo...',
+              onChanged: (v) => setState(() => _filter = v),
+              onClear: () {
+                _searchCtrl.clear();
+                setState(() => _filter = '');
+              },
+            ),
+            Expanded(
+              child: articlesAsync.when(
+                data: (articles) {
+                  if (articles.isEmpty)
+                    return const EmptyState(
+                        icon: Icons.inventory_2_outlined,
+                        title: 'Nessun articolo');
+                  if (ResponsiveBreakpoints.isPhone(context)) {
+                    return ListView.builder(
+                      itemCount: articles.length,
+                      itemBuilder: (_, i) =>
+                          _ArticleListItem(article: articles[i]),
+                    );
+                  }
+                  return GridView.builder(
+                    padding: const EdgeInsets.all(8),
+                    gridDelegate:
+                        const SliverGridDelegateWithFixedCrossAxisCount(
+                      crossAxisCount: 3,
+                      childAspectRatio: 1.2,
+                      crossAxisSpacing: 8,
+                      mainAxisSpacing: 8,
+                    ),
                     itemCount: articles.length,
-                    itemBuilder: (_, i) => _ArticleListItem(article: articles[i]),
+                    itemBuilder: (_, i) =>
+                        _ArticleGridItem(article: articles[i]),
                   );
-                }
-                return GridView.builder(
-                  padding: const EdgeInsets.all(8),
-                  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
-                    crossAxisCount: 3,
-                    childAspectRatio: 1.2,
-                    crossAxisSpacing: 8,
-                    mainAxisSpacing: 8,
-                  ),
-                  itemCount: articles.length,
-                  itemBuilder: (_, i) => _ArticleGridItem(article: articles[i]),
-                );
-              },
-              loading: () => const Center(child: CircularProgressIndicator()),
-              error: (e, _) => Center(child: Text('Errore: $e')),
+                },
+                loading: () => const Center(child: CircularProgressIndicator()),
+                error: (e, _) => Center(child: Text('Errore: $e')),
+              ),
             ),
-          ),
-        ],
-      ),
+          ],
+        ),
       ),
     );
   }
@@ -82,14 +90,18 @@ class _ArticleListItem extends StatelessWidget {
     return Card(
       margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
       child: ListTile(
-        leading: ArticleImage(idArticolo: article.idArticolo, codArticolo: article.codArticolo),
+        leading: ArticleImage(
+            idArticolo: article.idArticolo, codArticolo: article.codArticolo),
         title: Text(article.descrizione ?? ''),
         subtitle: Text(article.codArticolo ?? ''),
         trailing: article.nonDisponibile == 1
             ? Container(
                 padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
-                decoration: BoxDecoration(color: Colors.red.shade100, borderRadius: BorderRadius.circular(8)),
-                child: const Text('N.D.', style: TextStyle(color: Colors.red, fontSize: 12)),
+                decoration: BoxDecoration(
+                    color: Colors.red.shade100,
+                    borderRadius: BorderRadius.circular(8)),
+                child: const Text('N.D.',
+                    style: TextStyle(color: Colors.red, fontSize: 12)),
               )
             : null,
       ),
@@ -111,15 +123,29 @@ class _ArticleGridItem extends StatelessWidget {
           child: Column(
             mainAxisSize: MainAxisSize.min,
             children: [
-              ArticleImage(idArticolo: article.idArticolo, codArticolo: article.codArticolo, size: 64),
+              ArticleImage(
+                  idArticolo: article.idArticolo,
+                  codArticolo: article.codArticolo,
+                  size: 64),
               const SizedBox(height: 8),
-              Text(article.descrizione ?? '', maxLines: 2, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodySmall),
-              Text(article.codArticolo ?? '', style: Theme.of(context).textTheme.labelSmall?.copyWith(color: Theme.of(context).colorScheme.outline)),
+              Text(article.descrizione ?? '',
+                  maxLines: 2,
+                  overflow: TextOverflow.ellipsis,
+                  style: Theme.of(context).textTheme.bodySmall),
+              Text(article.codArticolo ?? '',
+                  style: Theme.of(context)
+                      .textTheme
+                      .labelSmall
+                      ?.copyWith(color: Theme.of(context).colorScheme.outline)),
               if (article.nonDisponibile == 1)
                 Container(
-                  padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
-                  decoration: BoxDecoration(color: Colors.red.shade100, borderRadius: BorderRadius.circular(8)),
-                  child: const Text('N.D.', style: TextStyle(color: Colors.red, fontSize: 10)),
+                  padding:
+                      const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
+                  decoration: BoxDecoration(
+                      color: Colors.red.shade100,
+                      borderRadius: BorderRadius.circular(8)),
+                  child: const Text('N.D.',
+                      style: TextStyle(color: Colors.red, fontSize: 10)),
                 ),
             ],
           ),

+ 12 - 6
lib/ui/pages/customers/customer_detail_page.dart

@@ -46,8 +46,9 @@ class CustomerDetailPage extends ConsumerWidget {
             future: clienteAsync,
             builder: (_, snap) {
               final cliente = snap.data;
-              if (cliente == null)
+              if (cliente == null) {
                 return const Center(child: CircularProgressIndicator());
+              }
               return TabBarView(
                 children: [
                   _InfoTab(cliente: cliente, theme: theme),
@@ -151,16 +152,19 @@ class _OrdersTab extends ConsumerWidget {
         child: FutureBuilder(
           future: ordiniDao.customerOrders(idCliente, false),
           builder: (_, snap) {
-            if (!snap.hasData)
+            if (!snap.hasData) {
               return const Center(child: CircularProgressIndicator());
+            }
             final orders = snap.data!;
-            if (orders.isEmpty) return const Center(child: Text('Nessun ordine'));
+            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}'),
+                onTap: () =>
+                    context.push('/dashboard/orders/${orders[i].idOrdine}'),
               ),
             );
           },
@@ -169,7 +173,8 @@ class _OrdersTab extends ConsumerWidget {
       floatingActionButton: FloatingActionButton(
         heroTag: 'addOrder',
         child: const Icon(Icons.add),
-        onPressed: () => context.push('/dashboard/orders/new?cliente=$idCliente'),
+        onPressed: () =>
+            context.push('/dashboard/orders/new?cliente=$idCliente'),
       ),
     );
   }
@@ -185,8 +190,9 @@ class _DestinationsTab extends ConsumerWidget {
     final destsAsync = ref.watch(customerDestinationsProvider(idCliente));
     return destsAsync.when(
       data: (dests) {
-        if (dests.isEmpty)
+        if (dests.isEmpty) {
           return const Center(child: Text('Nessuna destinazione'));
+        }
         return ListView.builder(
           itemCount: dests.length,
           itemBuilder: (_, i) => ListTile(

+ 3 - 3
lib/ui/pages/customers/customer_list_page.dart

@@ -52,14 +52,14 @@ class _CustomerListPageState extends ConsumerState<CustomerListPage> {
               },
             ),
             Expanded(
-              child:
-                  customersAsync?.when(
+              child: customersAsync?.when(
                     data: (customers) {
-                      if (customers.isEmpty)
+                      if (customers.isEmpty) {
                         return const EmptyState(
                           icon: Icons.people_outline,
                           title: 'Nessun cliente',
                         );
+                      }
                       return GridView.builder(
                         padding: const EdgeInsets.all(16),
                         gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(

+ 59 - 0
lib/ui/pages/dashboard/agent_switch_page.dart

@@ -0,0 +1,59 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
+import 'package:ristoris/providers/auth_provider.dart';
+import 'package:ristoris/providers/database_provider.dart';
+
+class AgentSwitchPage extends ConsumerWidget {
+  const AgentSwitchPage({super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final agentiAsync = ref.watch(agentiListProvider);
+    final currentAgent = ref.watch(authProvider).agente;
+    final theme = Theme.of(context);
+
+    return Scaffold(
+      appBar: AppBar(title: const Text('Cambia agente')),
+      body: agentiAsync.when(
+        data: (agenti) {
+          final filtered =
+              agenti.where((a) => a.idAgente != currentAgent?.idAgente).toList();
+          if (filtered.isEmpty) {
+            return const Center(child: Text('Nessun altro agente disponibile'));
+          }
+          return ListView.builder(
+            padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+            itemCount: filtered.length,
+            itemBuilder: (_, i) {
+              final a = filtered[i];
+              return Card(
+                margin: const EdgeInsets.only(bottom: 8),
+                child: ListTile(
+                  leading: CircleAvatar(
+                    backgroundColor: theme.colorScheme.primaryContainer,
+                    child: Text(
+                      a.ragioneSociale?.substring(0, 1).toUpperCase() ?? '?',
+                      style: TextStyle(color: theme.colorScheme.onPrimaryContainer),
+                    ),
+                  ),
+                  title: Text(a.ragioneSociale ?? ''),
+                  subtitle: Text('Cod. ${a.codAgente ?? ''}'),
+                  trailing: const Icon(Icons.chevron_right),
+                  onTap: () async {
+                    await ref.read(authProvider.notifier).switchAgent(a);
+                    if (context.mounted) {
+                      context.go('/dashboard');
+                    }
+                  },
+                ),
+              );
+            },
+          );
+        },
+        loading: () => const Center(child: CircularProgressIndicator()),
+        error: (e, _) => Center(child: Text('Errore: $e')),
+      ),
+    );
+  }
+}

+ 41 - 19
lib/ui/pages/dashboard/dashboard_page.dart

@@ -2,6 +2,7 @@ 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/providers/database_provider.dart';
 import '../../../providers/auth_provider.dart';
 import '../../../providers/dashboard_provider.dart';
 import '../../../core/responsive.dart';
@@ -14,9 +15,9 @@ class DashboardPage extends ConsumerWidget {
   Widget build(BuildContext context, WidgetRef ref) {
     final authState = ref.watch(authProvider);
     final agente = authState.agente;
-    final dashboardAsync = agente != null
-        ? ref.watch(dashboardProvider(agente.idAgente))
-        : null;
+    final dashboardAsync =
+        agente != null ? ref.watch(dashboardProvider(agente.idAgente)) : null;
+    final agentiAsync = ref.watch(agentiListProvider);
     final theme = Theme.of(context);
 
     return Scaffold(
@@ -31,16 +32,25 @@ class DashboardPage extends ConsumerWidget {
         onPayments: () => context.go('/dashboard/payments'),
         onStatistics: () => context.go('/dashboard/statistics'),
         onSync: () => context.go('/dashboard/sync'),
+        onSwitchAgent: () => context.go('/dashboard/switch-agent'),
         onLogout: () => ref.read(authProvider.notifier).logout(),
       ),
       body: SafeArea(
         child: dashboardAsync?.when(
-          data: (data) =>
-              _DashboardContent(data: data, theme: theme, context: context),
-          loading: () => const Center(child: CircularProgressIndicator()),
-          error: (e, _) => Center(child: Text('Errore: $e')),
-        ) ??
-        const Center(child: CircularProgressIndicator()),
+              data: (data) {
+                final agentiList = agentiAsync.asData?.value;
+                final hasMultipleAgents =
+                    agentiList != null && agentiList.length > 1;
+                return _DashboardContent(
+                  data: data,
+                  theme: theme,
+                  hasMultipleAgents: hasMultipleAgents,
+                );
+              },
+              loading: () => const Center(child: CircularProgressIndicator()),
+              error: (e, _) => Center(child: Text('Errore: $e')),
+            ) ??
+            const Center(child: CircularProgressIndicator()),
       ),
     );
   }
@@ -49,12 +59,12 @@ class DashboardPage extends ConsumerWidget {
 class _DashboardContent extends StatelessWidget {
   final DashboardData data;
   final ThemeData theme;
-  final BuildContext context;
+  final bool hasMultipleAgents;
 
   const _DashboardContent({
     required this.data,
     required this.theme,
-    required this.context,
+    this.hasMultipleAgents = false,
   });
 
   @override
@@ -73,25 +83,25 @@ class _DashboardContent extends StatelessWidget {
                 icon: Icons.receipt_long,
                 label: 'Nuovo Ordine',
                 color: theme.colorScheme.primary,
-                onTap: () => this.context.go('/dashboard/orders/new'),
+                onTap: () => context.go('/dashboard/orders/new'),
               ),
               _ActionCard(
                 icon: Icons.people,
                 label: 'Clienti',
                 color: theme.colorScheme.tertiary,
-                onTap: () => this.context.go('/dashboard/customers'),
+                onTap: () => context.go('/dashboard/customers'),
               ),
               _ActionCard(
                 icon: Icons.inventory_2,
                 label: 'Catalogo',
                 color: theme.colorScheme.secondary,
-                onTap: () => this.context.go('/dashboard/catalog'),
+                onTap: () => context.go('/dashboard/catalog'),
               ),
               _ActionCard(
                 icon: Icons.sync,
                 label: 'Sincronizza',
                 color: theme.colorScheme.error,
-                onTap: () => this.context.go('/dashboard/sync'),
+                onTap: () => context.go('/dashboard/sync'),
               ),
             ],
           ),
@@ -183,12 +193,24 @@ class _DashboardContent extends StatelessWidget {
                             symbol: '€',
                           ).format(c.totale ?? 0),
                         ),
-                      ),
-                    )
-                    .toList(),
+                  ),
+                )
+                .toList(),
               ),
             ),
           ],
+          if (hasMultipleAgents) ...[
+            const SizedBox(height: 24),
+            SizedBox(
+              width: double.infinity,
+              child: OutlinedButton.icon(
+                onPressed: () => context.go('/dashboard/switch-agent'),
+                icon: const Icon(Icons.swap_horiz),
+                label: const Text('Cambia agente'),
+              ),
+            ),
+            const SizedBox(height: 16),
+          ],
         ],
       ),
     );
@@ -285,4 +307,4 @@ class _StatCard extends StatelessWidget {
       ),
     );
   }
-}
+}

+ 46 - 54
lib/ui/pages/login/init_page.dart

@@ -49,51 +49,51 @@ class _InitPageState extends ConsumerState<InitPage> {
         top: false,
         child: Stack(
           children: [
-          Positioned(
-            top: -MediaQuery.of(context).size.height * 0.08,
-            right: -MediaQuery.of(context).size.width * 0.05,
-            child: Container(
-              width: 320,
-              height: 320,
-              decoration: BoxDecoration(
-                color: cs.secondaryContainer.withValues(alpha: 0.15),
-                shape: BoxShape.circle,
+            Positioned(
+              top: -MediaQuery.of(context).size.height * 0.08,
+              right: -MediaQuery.of(context).size.width * 0.05,
+              child: Container(
+                width: 320,
+                height: 320,
+                decoration: BoxDecoration(
+                  color: cs.secondaryContainer.withValues(alpha: 0.15),
+                  shape: BoxShape.circle,
+                ),
               ),
             ),
-          ),
-          Positioned(
-            bottom: -MediaQuery.of(context).size.height * 0.08,
-            left: -MediaQuery.of(context).size.width * 0.05,
-            child: Container(
-              width: 320,
-              height: 320,
-              decoration: BoxDecoration(
-                color: cs.primaryContainer.withValues(alpha: 0.08),
-                shape: BoxShape.circle,
+            Positioned(
+              bottom: -MediaQuery.of(context).size.height * 0.08,
+              left: -MediaQuery.of(context).size.width * 0.05,
+              child: Container(
+                width: 320,
+                height: 320,
+                decoration: BoxDecoration(
+                  color: cs.primaryContainer.withValues(alpha: 0.08),
+                  shape: BoxShape.circle,
+                ),
               ),
             ),
-          ),
-          Center(
-            child: SingleChildScrollView(
-              padding: const EdgeInsets.symmetric(horizontal: 16),
-              child: ConstrainedBox(
-                constraints: const BoxConstraints(maxWidth: 400),
-                child: Column(
-                  mainAxisSize: MainAxisSize.min,
-                  children: [
-                    const SizedBox(height: 32),
-                    _buildBrandHeader(theme, cs),
-                    const SizedBox(height: 32),
-                    _buildFormCard(authState, theme, cs),
-                    const SizedBox(height: 32),
-                    const SizedBox(height: 24),
-                  ],
+            Center(
+              child: SingleChildScrollView(
+                padding: const EdgeInsets.symmetric(horizontal: 16),
+                child: ConstrainedBox(
+                  constraints: const BoxConstraints(maxWidth: 400),
+                  child: Column(
+                    mainAxisSize: MainAxisSize.min,
+                    children: [
+                      const SizedBox(height: 32),
+                      _buildBrandHeader(theme, cs),
+                      const SizedBox(height: 32),
+                      _buildFormCard(authState, theme, cs),
+                      const SizedBox(height: 32),
+                      const SizedBox(height: 24),
+                    ],
+                  ),
                 ),
               ),
             ),
-          ),
-        ],
-      ),
+          ],
+        ),
       ),
     );
   }
@@ -163,8 +163,7 @@ class _InitPageState extends ConsumerState<InitPage> {
                 icon: Icons.badge_outlined,
                 controller: _codiceCtrl,
                 hintText: 'Inserisci il codice agente',
-                validator: (v) =>
-                    v == null || v.isEmpty ? 'Campo obbligatorio' : null,
+                validator: (v) => v == null || v.isEmpty ? 'Campo obbligatorio' : null,
               ),
               const SizedBox(height: 16),
               _buildPasswordField(cs),
@@ -181,8 +180,7 @@ class _InitPageState extends ConsumerState<InitPage> {
                 icon: Icons.link,
                 controller: _urlCtrl,
                 hintText: 'https://api.powerage.cloud',
-                validator: (v) =>
-                    v == null || v.isEmpty ? 'Campo obbligatorio' : null,
+                validator: (v) => v == null || v.isEmpty ? 'Campo obbligatorio' : null,
               ),
               const SizedBox(height: 24),
               FilledButton.icon(
@@ -190,15 +188,11 @@ class _InitPageState extends ConsumerState<InitPage> {
                     ? null
                     : () {
                         if (_formKey.currentState!.validate()) {
-                          ref
-                              .read(authProvider.notifier)
-                              .initialize(
-                                idAgente: 1,
+                          ref.read(authProvider.notifier).initialize(
+                                idAgente: 0,
                                 codAgente: _codiceCtrl.text,
                                 password: _passwordCtrl.text,
-                                ragioneSociale: _ragsocCtrl.text.isEmpty
-                                    ? null
-                                    : _ragsocCtrl.text,
+                                ragioneSociale: _ragsocCtrl.text.isEmpty ? null : _ragsocCtrl.text,
                                 url: _urlCtrl.text,
                               );
                         }
@@ -283,9 +277,7 @@ class _InitPageState extends ConsumerState<InitPage> {
             prefixIcon: const Icon(Icons.lock_outline, size: 20),
             suffixIcon: IconButton(
               icon: Icon(
-                _obscurePassword
-                    ? Icons.visibility_outlined
-                    : Icons.visibility_off_outlined,
+                _obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
                 size: 20,
               ),
               onPressed: () {
@@ -295,8 +287,8 @@ class _InitPageState extends ConsumerState<InitPage> {
               },
             ),
           ),
-          validator: (v) =>
-              v == null || v.isEmpty ? 'Campo obbligatorio' : null,
+//          validator: (v) =>
+          //             v == null || v.isEmpty ? 'Campo obbligatorio' : null,
         ),
       ],
     );

+ 35 - 21
lib/ui/pages/orders/order_edit_page.dart

@@ -5,6 +5,7 @@ import 'package:ristoris/core/credit_score_color.dart';
 import '../../../providers/auth_provider.dart';
 import '../../widgets/cod_desc_dropdown_item.dart';
 import '../../../providers/order_provider.dart';
+import '../../../providers/dashboard_provider.dart';
 import '../../../providers/database_provider.dart';
 import '../../../database/database.dart';
 import '../../../core/constants.dart';
@@ -210,6 +211,15 @@ class _OrderEditPageState extends ConsumerState<OrderEditPage>
     notifier.setRiferimento(_riferimentoCtrl.text);
 
     await notifier.saveOrder();
+
+    final agente = ref.read(authProvider).agente;
+    if (agente != null) {
+      ref.invalidate(ordersViewProvider(agente.idAgente));
+      ref.invalidate(ordersSearchViewProvider);
+      ref.invalidate(orderStatsProvider(agente.idAgente));
+      ref.invalidate(dashboardProvider(agente.idAgente));
+    }
+
     if (context.mounted) Navigator.pop(context);
   }
 
@@ -256,6 +266,29 @@ class _OrderEditPageState extends ConsumerState<OrderEditPage>
                   child: const Icon(Icons.add),
                 )
               : null,
+      bottomNavigationBar: _tabController.index == 1
+          ? Container(
+              padding: const EdgeInsets.all(16),
+              decoration: BoxDecoration(
+                color: theme.colorScheme.surfaceContainerHighest,
+                border: Border(top: BorderSide(color: theme.dividerColor)),
+              ),
+              child: SafeArea(
+                child: Row(
+                  mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                  children: [
+                    Text('Qtà: ${orderState.totQta.toStringAsFixed(1)}'),
+                    Text(
+                      'Totale: ${NumberFormat.currency(locale: 'it', symbol: '€').format(orderState.totale)}',
+                      style: theme.textTheme.titleMedium?.copyWith(
+                        fontWeight: FontWeight.bold,
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            )
+          : null,
     );
   }
 
@@ -301,7 +334,7 @@ class _OrderEditPageState extends ConsumerState<OrderEditPage>
                                   vertical: 4,
                                 ),
                                 decoration: BoxDecoration(
-                                  color: bgColor ?? cs.surfaceVariant,
+                                  color: bgColor ?? cs.surfaceContainerHighest,
                                   borderRadius: BorderRadius.circular(4),
                                 ),
                                 child: Text(
@@ -350,7 +383,7 @@ class _OrderEditPageState extends ConsumerState<OrderEditPage>
           ),
           const SizedBox(height: 16),
           DropdownButtonFormField<int>(
-            value: _selectedFiliale,
+            initialValue: _selectedFiliale,
             decoration: const InputDecoration(labelText: 'Filiale'),
             items: _filiali
                 .map(
@@ -452,25 +485,6 @@ class _OrderEditPageState extends ConsumerState<OrderEditPage>
                   ),
                 ),
         ),
-        Container(
-          padding: const EdgeInsets.all(16),
-          decoration: BoxDecoration(
-            color: theme.colorScheme.surfaceContainerHighest,
-            border: Border(top: BorderSide(color: theme.dividerColor)),
-          ),
-          child: Row(
-            mainAxisAlignment: MainAxisAlignment.spaceBetween,
-            children: [
-              Text('Qtà: ${orderState.totQta.toStringAsFixed(1)}'),
-              Text(
-                'Totale: ${NumberFormat.currency(locale: 'it', symbol: '€').format(orderState.totale)}',
-                style: theme.textTheme.titleMedium?.copyWith(
-                  fontWeight: FontWeight.bold,
-                ),
-              ),
-            ],
-          ),
-        ),
       ],
     );
   }

+ 14 - 5
lib/ui/pages/orders/order_list_page.dart

@@ -1,3 +1,4 @@
+import 'dart:async';
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
@@ -75,9 +76,11 @@ class OrderListPage extends ConsumerStatefulWidget {
 class _OrderListPageState extends ConsumerState<OrderListPage> {
   String _filter = '';
   final _searchCtrl = TextEditingController();
+  Timer? _debounceTimer;
 
   @override
   void dispose() {
+    _debounceTimer?.cancel();
     _searchCtrl.dispose();
     super.dispose();
   }
@@ -150,8 +153,14 @@ class _OrderListPageState extends ConsumerState<OrderListPage> {
             sw.SearchBarWidget(
               controller: _searchCtrl,
               hint: 'Cerca ordine, cliente, destinazione...',
-              onChanged: (v) => setState(() => _filter = v),
+              onChanged: (v) {
+                _debounceTimer?.cancel();
+                _debounceTimer = Timer(const Duration(milliseconds: 300), () {
+                  setState(() => _filter = v);
+                });
+              },
               onClear: () {
+                _debounceTimer?.cancel();
                 _searchCtrl.clear();
                 setState(() => _filter = '');
               },
@@ -380,7 +389,7 @@ class _OrderTable extends StatelessWidget {
                             _headerCell(context, 'Cliente', flex: 5),
                             _headerCell(context, 'Totale', flex: 1),
                             _headerCell(context, 'Stato', flex: 1),
-                            _headerCell(context, 'Azioni', flex: 1),
+                            _headerCell(context, 'Azioni', flex: 2),
                           ]
                         : [
                             _headerCell(context, 'N. Ordine', flex: 1),
@@ -389,7 +398,7 @@ class _OrderTable extends StatelessWidget {
                             _headerCell(context, 'Destinazione', flex: 6),
                             _headerCell(context, 'Totale', flex: 2),
                             _headerCell(context, 'Stato', flex: 1),
-                            _headerCell(context, 'Azioni', flex: 1),
+                            _headerCell(context, 'Azioni', flex: 2),
                           ],
                   ),
                 ),
@@ -534,7 +543,7 @@ class _OrderTable extends StatelessWidget {
                                                 ),
                                             ],
                                           ),
-                                          flex: 1,
+                                          flex: 2,
                                         ),
                                       ]
                                     : [
@@ -639,7 +648,7 @@ class _OrderTable extends StatelessWidget {
                                                 ),
                                             ],
                                           ),
-                                          flex: 1,
+                                          flex: 2,
                                         ),
                                       ],
                               ),

+ 200 - 102
lib/ui/pages/orders/selezione_articolo_page.dart

@@ -1,5 +1,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:intl/intl.dart';
 import 'package:ristoris/database/daos/articoli_dao.dart';
 import '../../../database/daos/sconti_dao.dart';
 import '../../../providers/auth_provider.dart';
@@ -8,6 +9,8 @@ import '../../../providers/database_provider.dart';
 import '../../../database/database.dart';
 import '../../widgets/article_image.dart';
 
+enum ArticleFilterMode { tutti, offertaCliente, acquistatiInPassato }
+
 class ArticleSelectionResult {
   final Articolo articolo;
   final int idListino;
@@ -55,15 +58,13 @@ class SelezioneArticoloPage extends ConsumerStatefulWidget {
 
 class _SelezioneArticoloPageState extends ConsumerState<SelezioneArticoloPage> {
   final _artCtrl = TextEditingController();
-  List<Articolo> _searchResults = [];
+  List<ArticoliConPromoData> _searchResults = [];
   bool _searching = false;
-  bool _usaListinoCliente = true;
-  Map<int, Listino> _mapListini = {};
+  ArticleFilterMode _filterMode = ArticleFilterMode.offertaCliente;
 
   @override
   void initState() {
     super.initState();
-    _loadListini();
     _searchArticles('');
   }
 
@@ -73,61 +74,57 @@ class _SelezioneArticoloPageState extends ConsumerState<SelezioneArticoloPage> {
     super.dispose();
   }
 
-  Future<void> _loadListini() async {
-    final listini = await ref.read(listiniDaoProvider).listSenzaFiltro();
-    setState(() {
-      _mapListini = {for (final l in listini) l.idListino: l};
-    });
-  }
-
   Future<void> _searchArticles(String q) async {
     setState(() => _searching = true);
-    final dao = ref.read(articoliDaoProvider);
-    List<Articolo> results;
-    if (_usaListinoCliente && widget.idCliente != 0) {
-      results = await dao.getArticoliPerListinoCliente(
-        widget.idCliente,
-        widget.dataOrdine,
-      );
-    } else {
-      results = await dao.getArticoliPerListinoAttivo(widget.dataOrdine, q);
-    }
-    if (q.isNotEmpty) {
-      final lower = q.toLowerCase();
-      results = results.where((a) {
-        final cod = (a.codArticolo ?? '').toLowerCase();
-        final desc = (a.descrizione ?? '').toLowerCase();
-        final ean = (a.ean ?? '').toLowerCase();
-        return cod.contains(lower) ||
-            desc.contains(lower) ||
-            ean.contains(lower);
-      }).toList();
+    final articoliDao = ref.read(articoliDaoProvider);
+    final clientiDao = ref.read(clientiDaoProvider);
+    List<ArticoliConPromoData> results;
+
+    switch (_filterMode) {
+      case ArticleFilterMode.tutti:
+        results =
+            await articoliDao.getActiveItemsWithPromo(q, widget.idCliente);
+      case ArticleFilterMode.offertaCliente:
+        results = await clientiDao.getFavoriteItems(
+          widget.idCliente,
+          widget.dataOrdine,
+          q,
+        );
+      case ArticleFilterMode.acquistatiInPassato:
+        final dataOrdineParsed = DateTime.parse(widget.dataOrdine);
+        final dataDa180 = dataOrdineParsed.subtract(const Duration(days: 180));
+        final dataDa180Str = DateFormat('yyyy-MM-dd').format(dataDa180);
+        results = await clientiDao.getAlreadySoldItems(
+          0,
+          widget.idCliente,
+          dataDa180Str,
+          q,
+        );
     }
+
     setState(() {
       _searchResults = results;
       _searching = false;
     });
   }
 
-  void _showArticlePicker(Articolo art) async {
+  void _showArticlePicker(int idArticolo) async {
+    final art = await ref.read(articoliDaoProvider).getById(idArticolo);
+    if (art == null || !context.mounted) return;
+
     final agente = ref.read(authProvider).agente;
     final canEditPrice = agente?.modificaPrezzi == 1;
 
-    final listini = await ref
-        .read(articoliDaoProvider)
-        .getListiniArticoloValidi(art.idArticolo, widget.dataOrdine);
+    final articoliDao = ref.read(articoliDaoProvider);
+    final listini = await articoliDao.getListiniArticoloValidi(
+        art.idArticolo, widget.idCliente, widget.dataOrdine);
 
-/*
-    final seen = <int>{};
-    final listini = listiniArticolo
-        .map((la) => _mapListini[la.idListino])
-        .whereType<Listino>()
-        .where((l) => seen.add(l.idListino))
-        .toList();
-*/
+    final listiniCliente = await articoliDao.getListiniClienteValidi(
+        widget.idCliente, art.idArticolo, widget.dataOrdine);
+    final listinoCliente =
+        listiniCliente.isNotEmpty ? listiniCliente.first : null;
 
     final scontiDao = ref.read(scontiDaoProvider);
-    final articoliDao = ref.read(articoliDaoProvider);
 
     if (!context.mounted) return;
 
@@ -137,6 +134,7 @@ class _SelezioneArticoloPageState extends ConsumerState<SelezioneArticoloPage> {
       builder: (_) => _ArticlePickerSheet(
         articolo: art,
         listini: listini,
+        listinoCliente: listinoCliente,
         articoliDao: articoliDao,
         scontiDao: scontiDao,
         dataOrdine: widget.dataOrdine,
@@ -181,6 +179,10 @@ class _SelezioneArticoloPageState extends ConsumerState<SelezioneArticoloPage> {
 
   @override
   Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    final currencyFormat = NumberFormat.currency(locale: 'it', symbol: '€');
+    final dateFormat = DateFormat('dd/MM/yyyy');
+
     return Scaffold(
       appBar: AppBar(
         title: const Text('Selezione articolo'),
@@ -189,38 +191,45 @@ class _SelezioneArticoloPageState extends ConsumerState<SelezioneArticoloPage> {
         child: Column(
           children: [
             Padding(
-              padding: const EdgeInsets.all(12),
-              child: Row(
-                children: [
-                  Expanded(
-                    child: TextField(
-                      controller: _artCtrl,
-                      decoration: InputDecoration(
-                        hintText: 'Cerca articolo...',
-                        prefixIcon: const Icon(Icons.search),
-                        suffixIcon: _searching
-                            ? const SizedBox(
-                                width: 20,
-                                height: 20,
-                                child: CircularProgressIndicator(
-                                  strokeWidth: 2,
-                                ),
-                              )
-                            : null,
-                      ),
-                      onChanged: _searchArticles,
-                    ),
+              padding: const EdgeInsets.fromLTRB(12, 12, 12, 0),
+              child: TextField(
+                controller: _artCtrl,
+                decoration: InputDecoration(
+                  hintText: 'Cerca articolo...',
+                  prefixIcon: const Icon(Icons.search),
+                  suffixIcon: _searching
+                      ? const SizedBox(
+                          width: 20,
+                          height: 20,
+                          child: CircularProgressIndicator(strokeWidth: 2),
+                        )
+                      : null,
+                ),
+                onChanged: _searchArticles,
+              ),
+            ),
+            Padding(
+              padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+              child: SegmentedButton<ArticleFilterMode>(
+                segments: const [
+                  ButtonSegment(
+                    value: ArticleFilterMode.offertaCliente,
+                    label: Text('Offerta cliente'),
                   ),
-                  const SizedBox(width: 8),
-                  FilterChip(
-                    label: const Text('Listino cliente'),
-                    selected: _usaListinoCliente,
-                    onSelected: (v) {
-                      setState(() => _usaListinoCliente = v);
-                      _searchArticles(_artCtrl.text);
-                    },
+                  ButtonSegment(
+                    value: ArticleFilterMode.tutti,
+                    label: Text('Tutti'),
+                  ),
+                  ButtonSegment(
+                    value: ArticleFilterMode.acquistatiInPassato,
+                    label: Text('Acquistati'),
                   ),
                 ],
+                selected: {_filterMode},
+                onSelectionChanged: (selected) {
+                  setState(() => _filterMode = selected.first);
+                  _searchArticles(_artCtrl.text);
+                },
               ),
             ),
             Expanded(
@@ -232,19 +241,71 @@ class _SelezioneArticoloPageState extends ConsumerState<SelezioneArticoloPage> {
                     )
                   : ListView.builder(
                       itemCount: _searchResults.length,
-                      itemBuilder: (_, i) => ListTile(
-                        leading: ArticleImage(
-                          idArticolo: _searchResults[i].idArticolo,
-                          codArticolo: _searchResults[i].codArticolo,
-                        ),
-                        title: Text(_searchResults[i].descrizione ?? ''),
-                        subtitle: Text(_searchResults[i].codArticolo ?? ''),
-                        trailing: TextButton(
-                          onPressed: () =>
-                              _showArticlePicker(_searchResults[i]),
-                          child: const Text('Aggiungi'),
-                        ),
-                      ),
+                      itemBuilder: (_, i) {
+                        final item = _searchResults[i];
+                        final hasLastPurchase = item.ultimoPrezzo != null &&
+                            item.lastDateSold != null;
+
+                        return Column(
+                          mainAxisSize: MainAxisSize.min,
+                          children: [
+                            ListTile(
+                              leading: ArticleImage(
+                                idArticolo: item.idArticolo,
+                                codArticolo: item.codArticolo,
+                              ),
+                              title: Text(
+                                  '${item.codArticolo} ${item.descrizione}'),
+                              // subtitle: Text(item.codArticolo ?? ''),
+                              trailing: Row(
+                                mainAxisSize: MainAxisSize.min,
+                                children: [
+                                  if (item.inPromo == 1)
+                                    const Icon(
+                                      Icons.star,
+                                      color: Colors.amber,
+                                      size: 20,
+                                    ),
+                                  TextButton(
+                                    onPressed: () =>
+                                        _showArticlePicker(item.idArticolo),
+                                    child: const Text('Aggiungi'),
+                                  ),
+                                ],
+                              ),
+                            ),
+                            if (hasLastPurchase)
+                              Padding(
+                                padding: const EdgeInsets.only(
+                                  left: 72,
+                                  right: 16,
+                                  bottom: 8,
+                                ),
+                                child: Row(
+                                  children: [
+                                    Text(
+                                      'Ultimo acquisto: ',
+                                      style: theme.textTheme.bodySmall,
+                                    ),
+                                    Text(
+                                      currencyFormat.format(item.ultimoPrezzo),
+                                      style: theme.textTheme.bodySmall
+                                          ?.copyWith(
+                                              fontWeight: FontWeight.bold),
+                                    ),
+                                    const SizedBox(width: 8),
+                                    Text(
+                                      dateFormat.format(
+                                        DateTime.parse(item.lastDateSold!),
+                                      ),
+                                      style: theme.textTheme.bodySmall,
+                                    ),
+                                  ],
+                                ),
+                              ),
+                          ],
+                        );
+                      },
                     ),
             ),
           ],
@@ -254,10 +315,12 @@ class _SelezioneArticoloPageState extends ConsumerState<SelezioneArticoloPage> {
   }
 }
 
+const int _kListinoClienteSentinel = -1;
+
 class _ArticlePickerSheet extends StatefulWidget {
   final Articolo articolo;
-//  final List<ListinoArticolo> listiniArticolo;
   final List<Listino> listini;
+  final ListinoCliente? listinoCliente;
   final ArticoliDao articoliDao;
   final ScontiDao scontiDao;
 
@@ -280,6 +343,7 @@ class _ArticlePickerSheet extends StatefulWidget {
   const _ArticlePickerSheet({
     required this.articolo,
     required this.listini,
+    this.listinoCliente,
     required this.articoliDao,
     required this.scontiDao,
     required this.dataOrdine,
@@ -317,10 +381,14 @@ class _ArticlePickerSheetState extends State<_ArticlePickerSheet> {
   @override
   void initState() {
     super.initState();
-    _selectedListino = widget.idListinoDefault;
-    if (_selectedListino != null &&
-        !widget.listini.any((l) => l.idListino == _selectedListino)) {
-      _selectedListino = null;
+    if (widget.listinoCliente != null) {
+      _selectedListino = _kListinoClienteSentinel;
+    } else {
+      _selectedListino = widget.idListinoDefault;
+      if (_selectedListino != null &&
+          !widget.listini.any((l) => l.idListino == _selectedListino)) {
+        _selectedListino = null;
+      }
     }
     if (_selectedListino != null) {
       _loadPrezzo(_selectedListino!, widget.articolo.idArticolo);
@@ -341,6 +409,22 @@ class _ArticlePickerSheetState extends State<_ArticlePickerSheet> {
   }
 
   void _loadPrezzo(int idListino, int idaArticolo) async {
+    if (idListino == _kListinoClienteSentinel &&
+        widget.listinoCliente != null) {
+      final lc = widget.listinoCliente!;
+      if (lc.prezzo != null) {
+        setState(() => _prezzo = lc.prezzo!);
+        _prezzoCtrl.text = _prezzo.toStringAsFixed(2);
+      }
+      if (lc.idSconto != null) {
+        _loadSconti(lc.idSconto!);
+      } else {
+        _resetSconti();
+      }
+      _calcolaPrezzoNetto();
+      return;
+    }
+
     final lp = await widget.articoliDao
         .getListPrice(idListino, idaArticolo, widget.dataOrdine);
 
@@ -458,7 +542,9 @@ class _ArticlePickerSheetState extends State<_ArticlePickerSheet> {
     final colli = double.tryParse(_colliCtrl.text) ?? 0;
     final qta = double.tryParse(_qtaCtrl.text) ?? 0;
     final prezzoLordo = double.tryParse(_prezzoCtrl.text) ?? 0;
-    final listinoId = _selectedListino ?? 0;
+    final listinoId = _selectedListino == _kListinoClienteSentinel
+        ? (widget.listinoCliente?.idListino ?? 0)
+        : (_selectedListino ?? 0);
     widget.onConfirm(
       colli,
       qta,
@@ -504,19 +590,31 @@ class _ArticlePickerSheetState extends State<_ArticlePickerSheet> {
               ),
             const SizedBox(height: 16),
             DropdownButtonFormField<int>(
-              // initialValue: _selectedListino,
+              initialValue: _selectedListino,
               decoration: const InputDecoration(labelText: 'Listino'),
               isExpanded: true,
-              items: widget.listini.map((l) {
-                print(l);
-                return DropdownMenuItem(
-                  value: l.idListino,
-                  child: CodDescDropdownItem(
-                    codice: l.codListino,
-                    descrizione: l.descrizione,
+              items: [
+                if (widget.listinoCliente != null)
+                  DropdownMenuItem(
+                    value: _kListinoClienteSentinel,
+                    child: const Text(
+                      'Listino Cliente',
+                      style: TextStyle(
+                        fontWeight: FontWeight.bold,
+                        color: Colors.blue,
+                      ),
+                    ),
                   ),
-                );
-              }).toList(),
+                ...widget.listini.map((l) {
+                  return DropdownMenuItem(
+                    value: l.idListino,
+                    child: CodDescDropdownItem(
+                      codice: l.codListino,
+                      descrizione: l.descrizione,
+                    ),
+                  );
+                }),
+              ],
               onChanged: (v) {
                 setState(() => _selectedListino = v);
                 if (v != null) _loadPrezzo(v, widget.articolo.idArticolo);

+ 33 - 16
lib/ui/pages/orders/selezione_cliente_page.dart

@@ -12,7 +12,8 @@ class SelezioneClientePage extends ConsumerStatefulWidget {
   const SelezioneClientePage({super.key, required this.idAgente});
 
   @override
-  ConsumerState<SelezioneClientePage> createState() => _SelezioneClientePageState();
+  ConsumerState<SelezioneClientePage> createState() =>
+      _SelezioneClientePageState();
 }
 
 class _SelezioneClientePageState extends ConsumerState<SelezioneClientePage> {
@@ -60,7 +61,8 @@ class _SelezioneClientePageState extends ConsumerState<SelezioneClientePage> {
                     );
                   }
                   return ListView.builder(
-                    padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+                    padding:
+                        const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
                     itemCount: clienti.length,
                     itemBuilder: (_, i) => _ClienteTile(
                       cliente: clienti[i],
@@ -81,7 +83,8 @@ class _SelezioneClientePageState extends ConsumerState<SelezioneClientePage> {
   void _onClienteTap(Cliente cliente) {
     if (cliente.codBlocco == '001') {
       ScaffoldMessenger.of(context).showSnackBar(
-        const SnackBar(content: Text('Cliente bloccato: impossibile selezionare')),
+        const SnackBar(
+            content: Text('Cliente bloccato: impossibile selezionare')),
       );
       return;
     }
@@ -124,9 +127,10 @@ class _ClienteTile extends StatelessWidget {
                       children: [
                         if ((cliente.codCliente ?? '').isNotEmpty)
                           Container(
-                            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+                            padding: const EdgeInsets.symmetric(
+                                horizontal: 8, vertical: 4),
                             decoration: BoxDecoration(
-                              color: bgColor ?? cs.surfaceVariant,
+                              color: bgColor ?? cs.surfaceContainerHighest,
                               borderRadius: BorderRadius.circular(4),
                             ),
                             child: Text(
@@ -135,7 +139,9 @@ class _ClienteTile extends StatelessWidget {
                                 fontSize: 12,
                                 height: 16 / 12,
                                 fontWeight: FontWeight.w500,
-                                color: bgColor != null ? Colors.white : cs.onSurfaceVariant,
+                                color: bgColor != null
+                                    ? Colors.white
+                                    : cs.onSurfaceVariant,
                               ),
                             ),
                           ),
@@ -143,7 +149,8 @@ class _ClienteTile extends StatelessWidget {
                         Expanded(
                           child: Text(
                             cliente.ragioneSociale ?? '',
-                            style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
+                            style: theme.textTheme.bodyMedium
+                                ?.copyWith(fontWeight: FontWeight.w600),
                             maxLines: 1,
                             overflow: TextOverflow.ellipsis,
                           ),
@@ -154,17 +161,21 @@ class _ClienteTile extends StatelessWidget {
                     Row(
                       children: [
                         if ((cliente.telefono ?? '').isNotEmpty) ...[
-                          Icon(Icons.phone, size: 14, color: cs.onSurfaceVariant),
+                          Icon(Icons.phone,
+                              size: 14, color: cs.onSurfaceVariant),
                           const SizedBox(width: 4),
                           Text(cliente.telefono ?? '',
-                            style: theme.textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant)),
+                              style: theme.textTheme.bodySmall
+                                  ?.copyWith(color: cs.onSurfaceVariant)),
                           const SizedBox(width: 12),
                         ],
                         if ((cliente.cellulare ?? '').isNotEmpty) ...[
-                          Icon(Icons.smartphone, size: 14, color: cs.onSurfaceVariant),
+                          Icon(Icons.smartphone,
+                              size: 14, color: cs.onSurfaceVariant),
                           const SizedBox(width: 4),
                           Text(cliente.cellulare ?? '',
-                            style: theme.textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant)),
+                              style: theme.textTheme.bodySmall
+                                  ?.copyWith(color: cs.onSurfaceVariant)),
                         ],
                       ],
                     ),
@@ -173,8 +184,10 @@ class _ClienteTile extends StatelessWidget {
               ),
               if (cliente.codBlocco == '001')
                 Icon(Icons.block, color: Colors.red, size: 24)
-              else if (cliente.codBlocco != null && cliente.codBlocco!.isNotEmpty)
-                Icon(Icons.warning_amber_rounded, color: Colors.amber, size: 24),
+              else if (cliente.codBlocco != null &&
+                  cliente.codBlocco!.isNotEmpty)
+                Icon(Icons.warning_amber_rounded,
+                    color: Colors.amber, size: 24),
             ],
           ),
         ),
@@ -183,12 +196,16 @@ class _ClienteTile extends StatelessWidget {
   }
 }
 
-final customerListForAgentProvider = FutureProvider.family<List<Cliente>, int>((ref, idAgente) {
+final customerListForAgentProvider =
+    FutureProvider.family<List<Cliente>, int>((ref, idAgente) {
   return ref.read(clientiDaoProvider).getActiveClients(idAgente);
 });
 
-final customerSearchForAgentProvider = FutureProvider.family<List<Cliente>, ({int idAgente, String filter})>(
+final customerSearchForAgentProvider =
+    FutureProvider.family<List<Cliente>, ({int idAgente, String filter})>(
   (ref, params) {
-    return ref.read(clientiDaoProvider).getClienti(params.idAgente, params.filter);
+    return ref
+        .read(clientiDaoProvider)
+        .getClienti(params.idAgente, params.filter);
   },
 );

+ 52 - 23
lib/ui/pages/payments/payments_page.dart

@@ -1,13 +1,14 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:intl/intl.dart';
+import 'package:ristoris/core/utils.dart';
 import '../../../providers/auth_provider.dart';
 import '../../../providers/database_provider.dart';
 import '../../../database/database.dart';
 import '../../widgets/empty_state.dart';
-import '../../../core/responsive.dart';
 
-final paymentsProvider = FutureProvider.family<List<Scadenza>, int>((ref, idAgente) {
+final paymentsProvider =
+    FutureProvider.family<List<VScadenzeData>, int>((ref, idAgente) {
   return ref.read(scadenzeDaoProvider).getScadenzePerAgente(idAgente);
 });
 
@@ -17,58 +18,86 @@ class PaymentsPage extends ConsumerWidget {
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     final agente = ref.watch(authProvider).agente;
-    final paysAsync = agente != null ? ref.watch(paymentsProvider(agente.idAgente)) : null;
+    final paysAsync =
+        agente != null ? ref.watch(paymentsProvider(agente.idAgente)) : null;
 
     return Scaffold(
       appBar: AppBar(title: const Text('Scadenze')),
       body: SafeArea(
         child: paysAsync?.when(
-          data: (pays) {
-            if (pays.isEmpty) return const EmptyState(icon: Icons.account_balance_wallet_outlined, title: 'Nessuna scadenza');
-            return ListView.builder(
-              itemCount: pays.length,
-              itemBuilder: (_, i) => _ScadenzaCard(scadenza: pays[i]),
-            );
-          },
-          loading: () => const Center(child: CircularProgressIndicator()),
-          error: (e, _) => Center(child: Text('Errore: $e')),
-        ) ?? const Center(child: CircularProgressIndicator()),
+              data: (pays) {
+                if (pays.isEmpty)
+                  return const EmptyState(
+                      icon: Icons.account_balance_wallet_outlined,
+                      title: 'Nessuna scadenza');
+                return ListView.builder(
+                  itemCount: pays.length,
+                  itemBuilder: (_, i) => _ScadenzaCard(scadenza: pays[i]),
+                );
+              },
+              loading: () => const Center(child: CircularProgressIndicator()),
+              error: (e, _) => Center(child: Text('Errore: $e')),
+            ) ??
+            const Center(child: CircularProgressIndicator()),
       ),
     );
   }
 }
 
 class _ScadenzaCard extends StatelessWidget {
-  final Scadenza scadenza;
+  final VScadenzeData scadenza;
   const _ScadenzaCard({required this.scadenza});
 
   @override
   Widget build(BuildContext context) {
     final theme = Theme.of(context);
     final importo = scadenza.importoScadenza ?? 0;
-    final scaduta = scadenza.dataScadenza != null && DateTime.parse(scadenza.dataScadenza!).isBefore(DateTime.now());
+    final scaduta = scadenza.dataScadenza != null &&
+        DateTime.parse(scadenza.dataScadenza!).isBefore(DateTime.now());
 
     return Card(
       margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 3),
       color: scaduta ? Colors.red.shade50 : null,
       child: ListTile(
         leading: CircleAvatar(
-          backgroundColor: scaduta ? Colors.red.shade100 : theme.colorScheme.primaryContainer,
-          child: Icon(Icons.account_balance_wallet_outlined, color: scaduta ? Colors.red : theme.colorScheme.primary),
+          backgroundColor: scaduta
+              ? Colors.red.shade100
+              : theme.colorScheme.primaryContainer,
+          child: Icon(Icons.account_balance_wallet_outlined,
+              color: scaduta ? Colors.red : theme.colorScheme.primary),
+        ),
+        title: Text('Documento ${scadenza.numPartita ?? ""}'),
+        subtitle: Column(
+          crossAxisAlignment: CrossAxisAlignment.start,
+          children: [
+            if (scadenza.ragioneSociale != null)
+              Text(
+                scadenza.ragioneSociale!,
+                style: theme.textTheme.bodyMedium
+                    ?.copyWith(fontWeight: FontWeight.w500),
+              ),
+            if (scadenza.codCliente != null)
+              Text('Cod. ${scadenza.codCliente}',
+                  style: theme.textTheme.bodySmall),
+            Text('Scadenza: ${Utils.formatDate(scadenza.dataScadenza)}'),
+          ],
         ),
-        title: Text('Documento ${scadenza.numDoc ?? ""}'),
-        subtitle: Text('Scadenza: ${scadenza.dataScadenza ?? ""}'),
         trailing: Column(
           mainAxisSize: MainAxisSize.min,
           crossAxisAlignment: CrossAxisAlignment.end,
           children: [
-            Text(NumberFormat.currency(locale: 'it', symbol: '€').format(importo),
-              style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold)),
+            Text(
+                NumberFormat.currency(locale: 'it', symbol: '€')
+                    .format(importo),
+                style: theme.textTheme.titleSmall
+                    ?.copyWith(fontWeight: FontWeight.bold)),
             if (scaduta)
               Container(
                 padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
-                decoration: BoxDecoration(color: Colors.red, borderRadius: BorderRadius.circular(8)),
-                child: const Text('SCADUTA', style: TextStyle(color: Colors.white, fontSize: 10)),
+                decoration: BoxDecoration(
+                    color: Colors.red, borderRadius: BorderRadius.circular(8)),
+                child: const Text('SCADUTA',
+                    style: TextStyle(color: Colors.white, fontSize: 10)),
               ),
           ],
         ),

+ 5 - 1
lib/ui/routes.dart

@@ -1,10 +1,10 @@
-import 'package:flutter/material.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import '../../providers/auth_provider.dart';
 import 'pages/login/init_page.dart';
 import 'pages/login/login_page.dart';
 import 'pages/dashboard/dashboard_page.dart';
+import 'pages/dashboard/agent_switch_page.dart';
 import 'pages/orders/order_list_page.dart';
 import 'pages/orders/order_edit_page.dart';
 import 'pages/customers/customer_list_page.dart';
@@ -91,6 +91,10 @@ final routerProvider = Provider<GoRouter>((ref) {
             path: 'sync/settings',
             builder: (_, __) => const SyncSettingsPage(),
           ),
+          GoRoute(
+            path: 'switch-agent',
+            builder: (_, __) => const AgentSwitchPage(),
+          ),
         ],
       ),
     ],

+ 3 - 0
lib/ui/widgets/app_drawer.dart

@@ -11,6 +11,7 @@ class AppDrawer extends StatelessWidget {
   final VoidCallback? onPayments;
   final VoidCallback? onStatistics;
   final VoidCallback? onSync;
+  final VoidCallback? onSwitchAgent;
   final VoidCallback? onLogout;
 
   const AppDrawer({
@@ -24,6 +25,7 @@ class AppDrawer extends StatelessWidget {
     this.onPayments,
     this.onStatistics,
     this.onSync,
+    this.onSwitchAgent,
     this.onLogout,
   });
 
@@ -75,6 +77,7 @@ class AppDrawer extends StatelessWidget {
           _item(Icons.account_balance_wallet_outlined, 'Scadenze', onPayments),
           _item(Icons.bar_chart_outlined, 'Statistiche', onStatistics),
           _item(Icons.sync_outlined, 'Sincronizzazione', onSync),
+          _item(Icons.swap_horiz, 'Cambia agente', onSwitchAgent),
           const Divider(),
           _item(Icons.logout, 'Logout', onLogout),
         ],

+ 5 - 8
lib/ui/widgets/customer_card.dart

@@ -51,9 +51,8 @@ class CustomerCard extends StatelessWidget {
                               vertical: 4,
                             ),
                             decoration: BoxDecoration(
-                              color:
-                                  creditScoreBg(cliente.creditScoreColor) ??
-                                  cs.surfaceVariant,
+                              color: creditScoreBg(cliente.creditScoreColor) ??
+                                  cs.surfaceContainerHighest,
                               borderRadius: BorderRadius.circular(4),
                             ),
                             child: Text(
@@ -64,16 +63,15 @@ class CustomerCard extends StatelessWidget {
                                 fontWeight: FontWeight.w500,
                                 color:
                                     creditScoreBg(cliente.creditScoreColor) !=
-                                        null
-                                    ? Colors.white
-                                    : cs.onSurfaceVariant,
+                                            null
+                                        ? Colors.white
+                                        : cs.onSurfaceVariant,
                               ),
                             ),
                           ),
                         ],
                       ],
                     ),
-
                     const SizedBox(height: 4),
                     Text(
                       cliente.email ?? '',
@@ -83,7 +81,6 @@ class CustomerCard extends StatelessWidget {
                       maxLines: 1,
                       overflow: TextOverflow.ellipsis,
                     ),
-
                     const SizedBox(height: 16),
                     Row(
                       children: [

+ 2 - 0
macos/Flutter/GeneratedPluginRegistrant.swift

@@ -6,11 +6,13 @@ import FlutterMacOS
 import Foundation
 
 import connectivity_plus
+import path_provider_foundation
 import sqflite_darwin
 import sqlite3_flutter_libs
 
 func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
   ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
+  PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
   SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
   Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
 }

+ 0 - 2
test/widget_test.dart

@@ -8,8 +8,6 @@
 import 'package:flutter/material.dart';
 import 'package:flutter_test/flutter_test.dart';
 
-import 'package:ristoris/main.dart';
-
 void main() {
   testWidgets('Counter increments smoke test', (WidgetTester tester) async {
     // Build our app and trigger a frame.

Some files were not shown because too many files changed in this diff