Fabio Dalle Ave 2 ماه پیش
والد
کامیت
4a3e05f072

+ 3 - 0
devtools_options.yaml

@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:

+ 1 - 1
lib/core/theme.dart

@@ -12,7 +12,7 @@ class AppTheme {
 
   static const ColorScheme _lightColorScheme = ColorScheme(
     brightness: Brightness.light,
-    primary: Color(0xFF000000),
+    primary: Color(0xFF0058be), //Color(0xFF000000),
     onPrimary: Color(0xFFFFFFFF),
     primaryContainer: Color(0xFF131B2E),
     onPrimaryContainer: Color(0xFF7C839B),

+ 59 - 0
lib/database/daos/ordini_dao.dart

@@ -29,6 +29,65 @@ class OrdiniDao {
     ).get();
   }
 
+  Future<List<VOrdiniData>> getOrdersView(int idAgente, {int limit = 200}) {
+    return (db.select(db.vOrdini)
+      ..where((t) => t.idAgente.equals(idAgente))
+      ..orderBy([(t) => OrderingTerm(expression: t.dataOrdine, mode: OrderingMode.desc)])
+      ..limit(limit)
+    ).get();
+  }
+
+  Future<List<VOrdiniData>> searchOrdersView(int idAgente, String filter) async {
+    final words = filter.split(' ').where((w) => w.isNotEmpty).toList();
+    final patterns = words.map((w) => '%$w%').toList();
+
+    final query = db.select(db.vOrdini)
+      ..where((t) => t.idAgente.equals(idAgente))
+      ..orderBy([(t) => OrderingTerm(expression: t.dataOrdine, mode: OrderingMode.desc)]);
+
+    for (final p in patterns) {
+      query.where((t) =>
+        t.ragioneSociale.like(p) |
+        t.filiale.like(p) |
+        t.indirizzo.like(p) |
+        t.citta.like(p) |
+        CustomExpression<String>('CAST(numeroOrdine AS TEXT)').like(p)
+      );
+    }
+
+    return query.get();
+  }
+
+  Future<int> countNewOrdersForAgent(int idAgente) async {
+    final rows = await (db.select(db.vOrdini)
+      ..where((t) => t.idAgente.equals(idAgente) & t.stato.equals('N'))
+    ).get();
+    return rows.length;
+  }
+
+  Future<int> countOrdersForAgent(int idAgente) async {
+    final rows = await (db.select(db.vOrdini)
+      ..where((t) => t.idAgente.equals(idAgente))
+    ).get();
+    return rows.length;
+  }
+
+  Future<double> getMonthlyVolume(int idAgente) async {
+    final now = DateTime.now();
+    final firstOfMonth = DateTime(now.year, now.month, 1);
+    final dateStr =
+        '${firstOfMonth.year.toString().padLeft(4, '0')}-'
+        '${firstOfMonth.month.toString().padLeft(2, '0')}-01';
+
+    final rows = await (db.select(db.vOrdini)
+      ..where((t) =>
+          t.idAgente.equals(idAgente) &
+          t.dataOrdine.isNotNull() &
+          t.dataOrdine.isBiggerOrEqualValue(dateStr))
+    ).get();
+    return rows.fold<double>(0, (sum, r) => sum + (r.totale ?? 0));
+  }
+
   Future<int> countNewOrders() async {
     final rows = await (db.select(db.ordini)..where((t) => t.stato.equals('N'))).get();
     return rows.length;

+ 29 - 1
lib/database/database.dart

@@ -26,6 +26,7 @@ import 'views/articoli_con_promo.dart';
 import 'views/ordini_senza_righe.dart';
 import 'views/top_item.dart';
 import 'views/top_customer.dart';
+import 'views/v_ordini.dart';
 
 part 'database.g.dart';
 
@@ -56,6 +57,7 @@ part 'database.g.dart';
     OrdiniSenzaRighe,
     TopItem,
     TopCustomer,
+    VOrdini,
   ],
 )
 class AppDatabase extends _$AppDatabase {
@@ -76,7 +78,7 @@ class AppDatabase extends _$AppDatabase {
   }
 
   @override
-  int get schemaVersion => 7;
+  int get schemaVersion => 9;
 
   @override
   MigrationStrategy get migration => MigrationStrategy(
@@ -127,6 +129,32 @@ class AppDatabase extends _$AppDatabase {
           '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,
+            c.codCliente,
+            c.ragioneSociale,
+            f.codFiliale,
+            f.descrizione Filiale,
+            CASE WHEN o.idFiliale IS NOT NULL THEN f.indirizzo ELSE c.indirizzo END AS indirizzo,
+            CASE WHEN o.idFiliale IS NOT NULL THEN f.citta ELSE c.citta END AS citta,
+            CASE WHEN o.idFiliale IS NOT NULL THEN f.provincia ELSE c.provincia END AS provincia,
+            CASE WHEN o.idFiliale IS NOT NULL THEN f.cap ELSE c.cap END AS cap,
+            o.*,
+            (SELECT SUM(ro.totale) FROM Righe_ordine ro WHERE ro.idOrdine = o.idOrdine) AS totale
+          FROM Ordini o
+          JOIN Clienti c ON c.idCliente = o.idCliente
+          LEFT JOIN Filiali f ON f.idFiliale = o.idFiliale
+          JOIN Agenti a ON a.idAgente = o.idAgente
+        ''');
+      }
+      if (from < 9) {
+        await m.addColumn(clienti, clienti.creditScoreColor);
+        await m.addColumn(clienti, clienti.dataCreditScore);
+      }
     },
   );
 }

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2858 - 176
lib/database/database.g.dart


+ 2 - 0
lib/database/tables/clienti.dart

@@ -38,6 +38,8 @@ class Clienti extends Table {
   TextColumn get banca => text().named('banca').nullable()();
   TextColumn get bancaFiliale => text().named('bancaFiliale').nullable()();
   TextColumn get codBlocco => text().named('codBlocco').nullable()();
+  TextColumn get creditScoreColor => text().named('creditScoreColor').nullable()();
+  TextColumn get dataCreditScore => text().named('dataCreditScore').nullable()();
 
   @override
   String get tableName => 'Clienti';

+ 40 - 0
lib/database/views/v_ordini.dart

@@ -0,0 +1,40 @@
+import 'package:drift/drift.dart';
+
+class VOrdini extends Table {
+  TextColumn get codAgente => text().named('codAgente')();
+  TextColumn get codCliente => text().named('codCliente').nullable()();
+  TextColumn get ragioneSociale => text().named('ragioneSociale').nullable()();
+  TextColumn get codFiliale => text().named('codFiliale').nullable()();
+  TextColumn get filiale => text().named('Filiale').nullable()();
+  TextColumn get indirizzo => text().named('indirizzo').nullable()();
+  TextColumn get citta => text().named('citta').nullable()();
+  TextColumn get provincia => text().named('provincia').nullable()();
+  TextColumn get cap => text().named('cap').nullable()();
+
+  IntColumn get idOrdine => integer().named('idOrdine')();
+  IntColumn get idOrdineSede => integer().named('idOrdineSede').nullable()();
+  IntColumn get numeroOrdine => integer().named('numeroOrdine').nullable()();
+  TextColumn get numeroOrdineSede => text().named('numeroOrdineSede').nullable()();
+  TextColumn get dataOrdine => text().named('dataOrdine').nullable()();
+  TextColumn get dataConsegna => text().named('dataConsegna').nullable()();
+  TextColumn get dataModifica => text().named('dataModifica').nullable()();
+  TextColumn get riferimento => text().named('riferimento').nullable()();
+  TextColumn get noteCliente => text().named('noteCliente').nullable()();
+  TextColumn get noteOrdine => text().named('noteOrdine').nullable()();
+  IntColumn get idCliente => integer().named('idCliente').nullable()();
+  IntColumn get idFiliale => integer().named('idFiliale').nullable()();
+  TextColumn get stato => text().named('stato').nullable()();
+  IntColumn get idListino => integer().named('idListino').nullable()();
+  IntColumn get idAgente => integer().named('idAgente').nullable()();
+  IntColumn get idPagamento => integer().named('idPagamento').nullable()();
+  RealColumn get totale => real().named('totale').nullable()();
+
+  @override
+  Set<Column> get primaryKey => {idOrdine};
+
+  @override
+  String get tableName => 'v_ordini';
+
+  @override
+  bool get withoutRowId => true;
+}

+ 2 - 0
lib/dto/sync_customer_dto.dart

@@ -38,6 +38,8 @@ class SyncCustomerDto with _$SyncCustomerDto {
     String? banca,
     String? bancaFiliale,
     String? codBlocco,
+    String? creditScoreColor,
+    String? dataCreditScore,
   }) = _SyncCustomerDto;
 
   factory SyncCustomerDto.fromJson(Map<String, dynamic> json) =>

+ 46 - 2
lib/dto/sync_customer_dto.freezed.dart

@@ -54,6 +54,8 @@ mixin _$SyncCustomerDto {
   String? get banca => throw _privateConstructorUsedError;
   String? get bancaFiliale => throw _privateConstructorUsedError;
   String? get codBlocco => throw _privateConstructorUsedError;
+  String? get creditScoreColor => throw _privateConstructorUsedError;
+  String? get dataCreditScore => throw _privateConstructorUsedError;
 
   /// Serializes this SyncCustomerDto to a JSON map.
   Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@@ -106,6 +108,8 @@ abstract class $SyncCustomerDtoCopyWith<$Res> {
     String? banca,
     String? bancaFiliale,
     String? codBlocco,
+    String? creditScoreColor,
+    String? dataCreditScore,
   });
 }
 
@@ -157,6 +161,8 @@ class _$SyncCustomerDtoCopyWithImpl<$Res, $Val extends SyncCustomerDto>
     Object? banca = freezed,
     Object? bancaFiliale = freezed,
     Object? codBlocco = freezed,
+    Object? creditScoreColor = freezed,
+    Object? dataCreditScore = freezed,
   }) {
     return _then(
       _value.copyWith(
@@ -292,6 +298,14 @@ class _$SyncCustomerDtoCopyWithImpl<$Res, $Val extends SyncCustomerDto>
                 ? _value.codBlocco
                 : codBlocco // ignore: cast_nullable_to_non_nullable
                       as String?,
+            creditScoreColor: freezed == creditScoreColor
+                ? _value.creditScoreColor
+                : creditScoreColor // ignore: cast_nullable_to_non_nullable
+                      as String?,
+            dataCreditScore: freezed == dataCreditScore
+                ? _value.dataCreditScore
+                : dataCreditScore // ignore: cast_nullable_to_non_nullable
+                      as String?,
           )
           as $Val,
     );
@@ -341,6 +355,8 @@ abstract class _$$SyncCustomerDtoImplCopyWith<$Res>
     String? banca,
     String? bancaFiliale,
     String? codBlocco,
+    String? creditScoreColor,
+    String? dataCreditScore,
   });
 }
 
@@ -391,6 +407,8 @@ class __$$SyncCustomerDtoImplCopyWithImpl<$Res>
     Object? banca = freezed,
     Object? bancaFiliale = freezed,
     Object? codBlocco = freezed,
+    Object? creditScoreColor = freezed,
+    Object? dataCreditScore = freezed,
   }) {
     return _then(
       _$SyncCustomerDtoImpl(
@@ -526,6 +544,14 @@ class __$$SyncCustomerDtoImplCopyWithImpl<$Res>
             ? _value.codBlocco
             : codBlocco // ignore: cast_nullable_to_non_nullable
                   as String?,
+        creditScoreColor: freezed == creditScoreColor
+            ? _value.creditScoreColor
+            : creditScoreColor // ignore: cast_nullable_to_non_nullable
+                  as String?,
+        dataCreditScore: freezed == dataCreditScore
+            ? _value.dataCreditScore
+            : dataCreditScore // ignore: cast_nullable_to_non_nullable
+                  as String?,
       ),
     );
   }
@@ -568,6 +594,8 @@ class _$SyncCustomerDtoImpl implements _SyncCustomerDto {
     this.banca,
     this.bancaFiliale,
     this.codBlocco,
+    this.creditScoreColor,
+    this.dataCreditScore,
   });
 
   factory _$SyncCustomerDtoImpl.fromJson(Map<String, dynamic> json) =>
@@ -639,10 +667,14 @@ class _$SyncCustomerDtoImpl implements _SyncCustomerDto {
   final String? bancaFiliale;
   @override
   final String? codBlocco;
+  @override
+  final String? creditScoreColor;
+  @override
+  final String? dataCreditScore;
 
   @override
   String toString() {
-    return 'SyncCustomerDto(idCliente: $idCliente, idClienteRemoto: $idClienteRemoto, codCliente: $codCliente, ragioneSociale: $ragioneSociale, indirizzo: $indirizzo, citta: $citta, provincia: $provincia, cap: $cap, frazione: $frazione, nazione: $nazione, telefono: $telefono, cellulare: $cellulare, fax: $fax, email: $email, partitaIva: $partitaIva, codiceFiscale: $codiceFiscale, note: $note, noteOrdine: $noteOrdine, stato: $stato, attivo: $attivo, idPagamento: $idPagamento, idListino: $idListino, dataModifica: $dataModifica, sdi: $sdi, pec: $pec, altreNote: $altreNote, numeroCivico: $numeroCivico, pod: $pod, pdr: $pdr, ubicazione: $ubicazione, banca: $banca, bancaFiliale: $bancaFiliale, codBlocco: $codBlocco)';
+    return 'SyncCustomerDto(idCliente: $idCliente, idClienteRemoto: $idClienteRemoto, codCliente: $codCliente, ragioneSociale: $ragioneSociale, indirizzo: $indirizzo, citta: $citta, provincia: $provincia, cap: $cap, frazione: $frazione, nazione: $nazione, telefono: $telefono, cellulare: $cellulare, fax: $fax, email: $email, partitaIva: $partitaIva, codiceFiscale: $codiceFiscale, note: $note, noteOrdine: $noteOrdine, stato: $stato, attivo: $attivo, idPagamento: $idPagamento, idListino: $idListino, dataModifica: $dataModifica, sdi: $sdi, pec: $pec, altreNote: $altreNote, numeroCivico: $numeroCivico, pod: $pod, pdr: $pdr, ubicazione: $ubicazione, banca: $banca, bancaFiliale: $bancaFiliale, codBlocco: $codBlocco, creditScoreColor: $creditScoreColor, dataCreditScore: $dataCreditScore)';
   }
 
   @override
@@ -702,7 +734,11 @@ class _$SyncCustomerDtoImpl implements _SyncCustomerDto {
             (identical(other.bancaFiliale, bancaFiliale) ||
                 other.bancaFiliale == bancaFiliale) &&
             (identical(other.codBlocco, codBlocco) ||
-                other.codBlocco == codBlocco));
+                other.codBlocco == codBlocco) &&
+            (identical(other.creditScoreColor, creditScoreColor) ||
+                other.creditScoreColor == creditScoreColor) &&
+            (identical(other.dataCreditScore, dataCreditScore) ||
+                other.dataCreditScore == dataCreditScore));
   }
 
   @JsonKey(includeFromJson: false, includeToJson: false)
@@ -742,6 +778,8 @@ class _$SyncCustomerDtoImpl implements _SyncCustomerDto {
     banca,
     bancaFiliale,
     codBlocco,
+    creditScoreColor,
+    dataCreditScore,
   ]);
 
   /// Create a copy of SyncCustomerDto
@@ -796,6 +834,8 @@ abstract class _SyncCustomerDto implements SyncCustomerDto {
     final String? banca,
     final String? bancaFiliale,
     final String? codBlocco,
+    final String? creditScoreColor,
+    final String? dataCreditScore,
   }) = _$SyncCustomerDtoImpl;
 
   factory _SyncCustomerDto.fromJson(Map<String, dynamic> json) =
@@ -867,6 +907,10 @@ abstract class _SyncCustomerDto implements SyncCustomerDto {
   String? get bancaFiliale;
   @override
   String? get codBlocco;
+  @override
+  String? get creditScoreColor;
+  @override
+  String? get dataCreditScore;
 
   /// Create a copy of SyncCustomerDto
   /// with the given fields replaced by the non-null parameter values.

+ 4 - 0
lib/dto/sync_customer_dto.g.dart

@@ -42,6 +42,8 @@ _$SyncCustomerDtoImpl _$$SyncCustomerDtoImplFromJson(
   banca: json['banca'] as String?,
   bancaFiliale: json['bancaFiliale'] as String?,
   codBlocco: json['codBlocco'] as String?,
+  creditScoreColor: json['creditScoreColor'] as String?,
+  dataCreditScore: json['dataCreditScore'] as String?,
 );
 
 Map<String, dynamic> _$$SyncCustomerDtoImplToJson(
@@ -80,6 +82,8 @@ Map<String, dynamic> _$$SyncCustomerDtoImplToJson(
   'banca': instance.banca,
   'bancaFiliale': instance.bancaFiliale,
   'codBlocco': instance.codBlocco,
+  'creditScoreColor': instance.creditScoreColor,
+  'dataCreditScore': instance.dataCreditScore,
 };
 
 _$SyncFilialeDtoImpl _$$SyncFilialeDtoImplFromJson(Map<String, dynamic> json) =>

+ 34 - 1
lib/providers/order_provider.dart

@@ -2,7 +2,6 @@ import 'package:drift/drift.dart';
 import 'package:flutter_riverpod/flutter_riverpod.dart';
 import '../database/database.dart';
 import '../../business/order_manager.dart';
-import '../../business/pricing_service.dart';
 import 'database_provider.dart';
 
 class OrderState {
@@ -149,3 +148,37 @@ class OrderNotifier extends Notifier<OrderState> {
 }
 
 final orderProvider = NotifierProvider<OrderNotifier, OrderState>(OrderNotifier.new);
+
+final ordersViewProvider = FutureProvider.family<List<VOrdiniData>, int>((ref, idAgente) {
+  return ref.read(ordiniDaoProvider).getOrdersView(idAgente);
+});
+
+final ordersSearchViewProvider = FutureProvider.family<List<VOrdiniData>, ({int idAgente, String filter})>(
+  (ref, params) {
+    return ref.read(ordiniDaoProvider).searchOrdersView(params.idAgente, params.filter);
+  },
+);
+
+class OrderStats {
+  final int daTrasmettere;
+  final int numeroOrdini;
+  final double volumeMensile;
+
+  const OrderStats({
+    this.daTrasmettere = 0,
+    this.numeroOrdini = 0,
+    this.volumeMensile = 0,
+  });
+}
+
+final orderStatsProvider = FutureProvider.family<OrderStats, int>((ref, idAgente) async {
+  final dao = ref.read(ordiniDaoProvider);
+  final daTrasmettere = await dao.countNewOrdersForAgent(idAgente);
+  final numeroOrdini = await dao.countOrdersForAgent(idAgente);
+  final volumeMensile = await dao.getMonthlyVolume(idAgente);
+  return OrderStats(
+    daTrasmettere: daTrasmettere,
+    numeroOrdini: numeroOrdini,
+    volumeMensile: volumeMensile,
+  );
+});

+ 9 - 3
lib/services/sync/cliente_sync_service.dart

@@ -23,7 +23,7 @@ class ClienteSyncService {
         if (resp['status'] == 0) {
           final idAssegnato = resp['idAssegnato'] as int?;
           if (idAssegnato != null) {
-            await _clientiDao.markAsSent(c.idCliente!, idAssegnato);
+            await _clientiDao.markAsSent(c.idCliente, idAssegnato);
             sent++;
           }
         }
@@ -109,6 +109,8 @@ class ClienteSyncService {
                     banca: dto.banca,
                     bancaFiliale: dto.bancaFiliale,
                     codBlocco: dto.codBlocco,
+                    creditScoreColor: dto.creditScoreColor,
+                    dataCreditScore: dto.dataCreditScore,
                   ),
                 );
           } else {
@@ -148,9 +150,11 @@ class ClienteSyncService {
                     banca: Value(dto.banca),
                     bancaFiliale: Value(dto.bancaFiliale),
                     codBlocco: Value(dto.codBlocco),
+                    creditScoreColor: Value(dto.creditScoreColor),
+                    dataCreditScore: Value(dto.dataCreditScore),
                   ),
-                );
-          }
+              );
+            }
 
           final assocKey = '$savedId-$idAgente';
           if (!cacheAssoc.containsKey(assocKey)) {
@@ -216,6 +220,8 @@ class ClienteSyncService {
       banca: c.banca,
       bancaFiliale: c.bancaFiliale,
       codBlocco: c.codBlocco,
+      creditScoreColor: c.creditScoreColor,
+      dataCreditScore: c.dataCreditScore,
     );
   }
 }

+ 11 - 17
lib/ui/pages/customers/customer_list_page.dart

@@ -1,11 +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 '../../../providers/customer_provider.dart';
-import '../../../database/tables/clienti.dart';
 import '../../widgets/search_bar.dart' as sw;
 import '../../widgets/empty_state.dart';
+import '../../widgets/customer_card.dart';
 
 class CustomerListPage extends ConsumerStatefulWidget {
   const CustomerListPage({super.key});
@@ -60,22 +59,17 @@ class _CustomerListPageState extends ConsumerState<CustomerListPage> {
                         icon: Icons.people_outline,
                         title: 'Nessun cliente',
                       );
-                    return ListView.builder(
-                      itemCount: customers.length,
-                      itemBuilder: (_, i) => ListTile(
-                        onTap: () => context.go(
-                          '/dashboard/customers/${customers[i].idCliente}',
-                        ),
-                        leading: CircleAvatar(
-                          child: Text(
-                            customers[i].ragioneSociale?.substring(0, 2) ??
-                                '??',
-                          ),
-                        ),
-                        title: Text(customers[i].ragioneSociale ?? ''),
-                        subtitle: Text(customers[i].codCliente ?? ''),
-                        trailing: const Icon(Icons.chevron_right),
+                    return GridView.builder(
+                      padding: const EdgeInsets.all(16),
+                      gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
+                        maxCrossAxisExtent: 470,
+                        mainAxisSpacing: 16,
+                        crossAxisSpacing: 16,
+                        childAspectRatio: 1.2,
                       ),
+                      itemCount: customers.length,
+                      itemBuilder: (_, i) =>
+                          CustomerCard(cliente: customers[i]),
                     );
                   },
                   loading: () =>

+ 1 - 1
lib/ui/pages/login/login_page.dart

@@ -303,7 +303,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
           controller: _passwordCtrl,
           obscureText: _obscurePassword,
           decoration: InputDecoration(
-            hintText: '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022',
+            hintText: '',
             prefixIcon: const Icon(Icons.lock_outline, size: 20),
             suffixIcon: IconButton(
               icon: Icon(

+ 458 - 51
lib/ui/pages/orders/order_list_page.dart

@@ -3,85 +3,492 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
 import 'package:go_router/go_router.dart';
 import 'package:intl/intl.dart';
 import '../../../providers/auth_provider.dart';
-import '../../../providers/database_provider.dart';
+import '../../../providers/order_provider.dart';
 import '../../../database/database.dart';
+import '../../widgets/search_bar.dart' as sw;
 import '../../widgets/empty_state.dart';
+import '../../../core/constants.dart';
 
-final ordersListProvider = FutureProvider.family<List<Ordine>, int>((ref, idAgente) {
-  return ref.read(ordiniDaoProvider).getLastOrders(idAgente, 200);
-});
+String _statoLabel(String? stato) {
+  switch (stato) {
+    case AppConstants.kStatusNew:
+      return 'Nuovo';
+    case AppConstants.kStatusTransferred:
+      return 'Trasmesso';
+    case AppConstants.kStatoHold:
+      return 'Sospeso';
+    default:
+      return stato ?? '';
+  }
+}
+
+Color _statoColor(String? stato) {
+  switch (stato) {
+    case AppConstants.kStatusNew:
+      return Colors.orange;
+    case AppConstants.kStatusTransferred:
+      return Colors.green;
+    case AppConstants.kStatoHold:
+      return Colors.blue;
+    default:
+      return Colors.grey;
+  }
+}
+
+class _StatoPill extends StatelessWidget {
+  final String? stato;
+  const _StatoPill({this.stato});
+
+  @override
+  Widget build(BuildContext context) {
+    final label = _statoLabel(stato);
+    final color = _statoColor(stato);
+
+    return Container(
+      padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
+      decoration: BoxDecoration(
+        color: color.withAlpha(25),
+        borderRadius: BorderRadius.circular(4),
+      ),
+      child: Text(
+        label,
+        style: TextStyle(
+          fontSize: 12,
+          height: 16 / 12,
+          fontWeight: FontWeight.w500,
+          color: color,
+        ),
+      ),
+    );
+  }
+}
 
-class OrderListPage extends ConsumerWidget {
+class OrderListPage extends ConsumerStatefulWidget {
   const OrderListPage({super.key});
 
   @override
-  Widget build(BuildContext context, WidgetRef ref) {
+  ConsumerState<OrderListPage> createState() => _OrderListPageState();
+}
+
+class _OrderListPageState extends ConsumerState<OrderListPage> {
+  String _filter = '';
+  final _searchCtrl = TextEditingController();
+
+  @override
+  void dispose() {
+    _searchCtrl.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
     final agente = ref.watch(authProvider).agente;
-    final ordersAsync = agente != null ? ref.watch(ordersListProvider(agente.idAgente)) : null;
-    final theme = Theme.of(context);
+    final isPhone = MediaQuery.of(context).size.width < 600;
+
+    final ordersAsync = agente != null
+        ? ref.watch(
+            _filter.isEmpty
+                ? ordersViewProvider(agente.idAgente)
+                : ordersSearchViewProvider((
+                    idAgente: agente.idAgente,
+                    filter: _filter,
+                  )),
+          )
+        : null;
+
+    final statsAsync = agente != null
+        ? ref.watch(orderStatsProvider(agente.idAgente))
+        : null;
 
     return Scaffold(
-      appBar: AppBar(title: const Text('Ordini')),
+      appBar: AppBar(title: const Text('Lista Ordini')),
       floatingActionButton: FloatingActionButton(
         onPressed: () => context.go('/dashboard/orders/new'),
         child: const Icon(Icons.add),
       ),
-      body: ordersAsync?.when(
-        data: (orders) {
-          if (orders.isEmpty) return const EmptyState(icon: Icons.receipt_long, title: 'Nessun ordine');
-          return ListView.builder(
-            padding: const EdgeInsets.all(8),
-            itemCount: orders.length,
-            itemBuilder: (_, i) => _OrderCard(order: orders[i], theme: theme, onTap: () => context.go('/dashboard/orders/${orders[i].idOrdine}')),
-          );
-        },
-        loading: () => const Center(child: CircularProgressIndicator()),
-        error: (e, _) => Center(child: Text('Errore: $e')),
-      ) ?? const Center(child: CircularProgressIndicator()),
+      body: Column(
+        children: [
+          sw.SearchBarWidget(
+            controller: _searchCtrl,
+            hint: 'Cerca ordine, cliente, destinazione...',
+            onChanged: (v) => setState(() => _filter = v),
+            onClear: () {
+              _searchCtrl.clear();
+              setState(() => _filter = '');
+            },
+          ),
+          Expanded(
+            child: ordersAsync?.when(
+              data: (orders) {
+                if (orders.isEmpty) {
+                  return const EmptyState(
+                    icon: Icons.receipt_long,
+                    title: 'Nessun ordine',
+                  );
+                }
+                if (isPhone) {
+                  return ListView.builder(
+                    padding: const EdgeInsets.all(16),
+                    itemCount: orders.length,
+                    itemBuilder: (_, i) => _OrderCard(order: orders[i]),
+                  );
+                }
+                return _OrderTable(orders: orders);
+              },
+              loading: () => const Center(child: CircularProgressIndicator()),
+              error: (e, _) => Center(child: Text('Errore: $e')),
+            ) ?? const Center(child: CircularProgressIndicator()),
+          ),
+          if (statsAsync != null)
+            statsAsync.when(
+              data: (stats) => _StatsBar(stats: stats),
+              loading: () => const SizedBox(height: 80),
+              error: (_, _) => const SizedBox.shrink(),
+            ),
+        ],
+      ),
     );
   }
 }
 
 class _OrderCard extends StatelessWidget {
-  final Ordine order;
-  final ThemeData theme;
-  final VoidCallback onTap;
-
-  const _OrderCard({required this.order, required this.theme, required this.onTap});
+  final VOrdiniData order;
+  const _OrderCard({required this.order});
 
   @override
   Widget build(BuildContext context) {
-    final statoColor = switch (order.stato) {
-      'N' => Colors.orange,
-      'T' => Colors.green,
-      'H' => Colors.blue,
-      _ => Colors.grey,
-    };
+    final theme = Theme.of(context);
+    final cs = theme.colorScheme;
+    final dateFormat = DateFormat('dd/MM/yyyy');
+    final currencyFormat = NumberFormat.currency(symbol: '€ ', decimalDigits: 2);
 
     return Card(
-      margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
-      child: ListTile(
-        onTap: onTap,
-        leading: CircleAvatar(
-          backgroundColor: statoColor.withAlpha(30),
-          child: Text(order.numeroOrdine?.toString() ?? '?', style: TextStyle(color: statoColor, fontWeight: FontWeight.bold)),
+      margin: const EdgeInsets.only(bottom: 12),
+      elevation: 0,
+      color: Colors.white,
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(12),
+        side: BorderSide(color: cs.outlineVariant),
+      ),
+      child: InkWell(
+        onTap: () => context.push('/dashboard/orders/${order.idOrdine}'),
+        borderRadius: BorderRadius.circular(12),
+        child: Padding(
+          padding: const EdgeInsets.all(16),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Row(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Expanded(
+                    child: Text(
+                      '#${order.numeroOrdine ?? ''}',
+                      style: theme.textTheme.titleMedium?.copyWith(
+                        fontWeight: FontWeight.bold,
+                      ),
+                    ),
+                  ),
+                  Text(
+                    order.dataOrdine != null
+                        ? dateFormat.format(DateTime.parse(order.dataOrdine!))
+                        : '',
+                    style: theme.textTheme.bodyMedium?.copyWith(
+                      color: cs.onSurfaceVariant,
+                    ),
+                  ),
+                ],
+              ),
+              const SizedBox(height: 4),
+              Text(
+                '${order.codCliente ?? ''} - ${order.ragioneSociale ?? ''}',
+                style: theme.textTheme.bodyMedium,
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+              ),
+              if ((order.filiale ?? '').isNotEmpty) ...[
+                const SizedBox(height: 4),
+                Text(
+                  'Filiale: ${order.filiale}',
+                  style: theme.textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
+                  maxLines: 1,
+                  overflow: TextOverflow.ellipsis,
+                ),
+              ],
+              if ((order.indirizzo ?? '').isNotEmpty) ...[
+                const SizedBox(height: 2),
+                Text(
+                  '${order.indirizzo}${order.citta != null ? ', ${order.citta}' : ''}',
+                  style: theme.textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
+                  maxLines: 1,
+                  overflow: TextOverflow.ellipsis,
+                ),
+              ],
+              const SizedBox(height: 12),
+              Row(
+                children: [
+                  Text(
+                    'Totale: ${currencyFormat.format(order.totale ?? 0)}',
+                    style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.bold),
+                  ),
+                  const Spacer(),
+                  _StatoPill(stato: order.stato),
+                ],
+              ),
+            ],
+          ),
         ),
-        title: Text('Ordine #${order.numeroOrdine ?? ''}'),
-        subtitle: Text(order.riferimento ?? order.noteCliente ?? ''),
-        trailing: Row(
-          mainAxisSize: MainAxisSize.min,
-          children: [
-            Container(
-              padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
-              decoration: BoxDecoration(color: statoColor.withAlpha(20), borderRadius: BorderRadius.circular(12)),
-              child: Text(
-                order.stato == 'N' ? 'Nuovo' : order.stato == 'T' ? 'Trasmesso' : 'Sospeso',
-                style: TextStyle(color: statoColor, fontSize: 12),
+      ),
+    );
+  }
+}
+
+class _OrderTable extends StatelessWidget {
+  final List<VOrdiniData> orders;
+  const _OrderTable({required this.orders});
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    final cs = theme.colorScheme;
+    final dateFormat = DateFormat('dd/MM/yyyy');
+    final currencyFormat = NumberFormat.currency(symbol: '€ ', decimalDigits: 2);
+
+    return SingleChildScrollView(
+      scrollDirection: Axis.horizontal,
+      child: SizedBox(
+        width: 900,
+        child: SingleChildScrollView(
+          scrollDirection: Axis.vertical,
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.stretch,
+            children: [
+              Container(
+                padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 14),
+                decoration: BoxDecoration(
+                  color: cs.surfaceContainerHigh.withAlpha(50),
+                  border: Border(bottom: BorderSide(color: cs.outlineVariant)),
+                ),
+                child: Row(
+                  children: [
+                    _headerCell(context, 'Numero Ordine', flex: 2),
+                    _headerCell(context, 'Data', flex: 1),
+                    _headerCell(context, 'Cliente', flex: 3),
+                    _headerCell(context, 'Destinazione', flex: 2),
+                    _headerCell(context, 'Totale', flex: 1),
+                    _headerCell(context, 'Stato', flex: 1),
+                    _headerCell(context, 'Azioni', flex: 1),
+                  ],
+                ),
               ),
-            ),
-          ],
+              ...orders.map((o) {
+                return Container(
+                  padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
+                  decoration: BoxDecoration(
+                    border: Border(bottom: BorderSide(color: cs.outlineVariant.withAlpha(80))),
+                  ),
+                  child: InkWell(
+                    onTap: () => context.push('/dashboard/orders/${o.idOrdine}'),
+                    child: Row(
+                      children: [
+                        _cell(
+                          Text(
+                            '#${o.numeroOrdine ?? ''}',
+                            style: theme.textTheme.bodyMedium?.copyWith(
+                              fontWeight: FontWeight.bold,
+                              fontFamily: 'monospace',
+                            ),
+                          ),
+                          flex: 2,
+                        ),
+                        _cell(
+                          Text(
+                            o.dataOrdine != null
+                                ? dateFormat.format(DateTime.parse(o.dataOrdine!))
+                                : '',
+                            style: theme.textTheme.bodyMedium?.copyWith(color: cs.onSurfaceVariant),
+                          ),
+                          flex: 1,
+                        ),
+                        _cell(
+                          Text(
+                            '${o.codCliente ?? ''} - ${o.ragioneSociale ?? ''}',
+                            style: theme.textTheme.bodyMedium,
+                            maxLines: 1,
+                            overflow: TextOverflow.ellipsis,
+                          ),
+                          flex: 3,
+                        ),
+                        _cell(
+                          Text(
+                            '${o.citta ?? ''}${o.provincia != null ? ' (${o.provincia})' : ''}',
+                            style: theme.textTheme.bodyMedium?.copyWith(color: cs.onSurfaceVariant),
+                            maxLines: 1,
+                            overflow: TextOverflow.ellipsis,
+                          ),
+                          flex: 2,
+                        ),
+                        _cell(
+                          Text(
+                            currencyFormat.format(o.totale ?? 0),
+                            style: theme.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
+                          ),
+                          flex: 1,
+                        ),
+                        _cell(_StatoPill(stato: o.stato), flex: 1),
+                        _cell(
+                          IconButton(
+                            icon: Icon(Icons.visibility, size: 20, color: cs.secondary),
+                            onPressed: () => context.push('/dashboard/orders/${o.idOrdine}'),
+                            visualDensity: VisualDensity.compact,
+                          ),
+                          flex: 1,
+                        ),
+                      ],
+                    ),
+                  ),
+                );
+              }),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  Widget _headerCell(BuildContext context, String label, {int flex = 1}) {
+    final cs = Theme.of(context).colorScheme;
+    return Expanded(
+      flex: flex,
+      child: Text(
+        label,
+        style: TextStyle(
+          fontSize: 12,
+          fontWeight: FontWeight.w600,
+          color: cs.onSurfaceVariant,
         ),
       ),
     );
   }
+
+  Widget _cell(Widget child, {int flex = 1}) {
+    return Expanded(flex: flex, child: child);
+  }
+}
+
+class _StatsBar extends StatelessWidget {
+  final OrderStats stats;
+  const _StatsBar({required this.stats});
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    final cs = theme.colorScheme;
+    final currencyFormat = NumberFormat.currency(symbol: '€ ', decimalDigits: 2);
+
+    return Container(
+      padding: const EdgeInsets.all(16),
+      decoration: BoxDecoration(
+        color: cs.surface,
+        border: Border(top: BorderSide(color: cs.outlineVariant)),
+      ),
+      child: Row(
+        children: [
+          Expanded(
+            child: _StatCard(
+              icon: Icons.shopping_cart,
+              iconBgColor: cs.secondaryContainer.withAlpha(30),
+              iconColor: cs.secondary,
+              label: 'Volume Mensile',
+              value: currencyFormat.format(stats.volumeMensile),
+            ),
+          ),
+          const SizedBox(width: 12),
+          Expanded(
+            child: _StatCard(
+              icon: Icons.pending_actions,
+              iconBgColor: Colors.orange.withAlpha(30),
+              iconColor: Colors.orange,
+              label: 'Da Trasmettere',
+              value: stats.daTrasmettere.toString(),
+            ),
+          ),
+          const SizedBox(width: 12),
+          Expanded(
+            child: _StatCard(
+              icon: Icons.verified,
+              iconBgColor: Colors.green.withAlpha(30),
+              iconColor: Colors.green,
+              label: 'Numero Ordini',
+              value: stats.numeroOrdini.toString(),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class _StatCard extends StatelessWidget {
+  final IconData icon;
+  final Color iconBgColor;
+  final Color iconColor;
+  final String label;
+  final String value;
+  const _StatCard({
+    required this.icon,
+    required this.iconBgColor,
+    required this.iconColor,
+    required this.label,
+    required this.value,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    final cs = theme.colorScheme;
+
+    return Container(
+      padding: const EdgeInsets.all(12),
+      decoration: BoxDecoration(
+        color: Colors.white,
+        borderRadius: BorderRadius.circular(12),
+        border: Border.all(color: cs.outlineVariant),
+      ),
+      child: Row(
+        children: [
+          Container(
+            width: 40,
+            height: 40,
+            decoration: BoxDecoration(
+              color: iconBgColor,
+              shape: BoxShape.circle,
+            ),
+            child: Icon(icon, color: iconColor, size: 20),
+          ),
+          const SizedBox(width: 12),
+          Expanded(
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: [
+                Text(
+                  label,
+                  style: theme.textTheme.labelSmall?.copyWith(color: cs.onSurfaceVariant),
+                  maxLines: 1,
+                  overflow: TextOverflow.ellipsis,
+                ),
+                const SizedBox(height: 2),
+                Text(
+                  value,
+                  style: theme.textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
+                  maxLines: 1,
+                  overflow: TextOverflow.ellipsis,
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
 }

+ 8 - 5
lib/ui/routes.dart

@@ -67,11 +67,14 @@ final routerProvider = Provider<GoRouter>((ref) {
           GoRoute(
             path: 'customers',
             builder: (_, __) => const CustomerListPage(),
-          ),
-          GoRoute(
-            path: 'customers/:id',
-            builder: (_, state) =>
-                CustomerDetailPage(id: int.parse(state.pathParameters['id']!)),
+            routes: [
+              GoRoute(
+                path: ':id',
+                builder: (_, state) => CustomerDetailPage(
+                  id: int.parse(state.pathParameters['id']!),
+                ),
+              ),
+            ],
           ),
           GoRoute(path: 'catalog', builder: (_, __) => const CatalogPage()),
           GoRoute(

+ 195 - 0
lib/ui/widgets/customer_card.dart

@@ -0,0 +1,195 @@
+import 'package:flutter/material.dart';
+import 'package:go_router/go_router.dart';
+import 'package:ristoris/database/database.dart';
+
+Color? _creditScoreBg(String? colorName) {
+  switch (colorName?.toUpperCase()) {
+    case 'VERDE':
+      return const Color(0xFF008000);
+    case 'VERDE CHIARO':
+      return const Color(0xFF90EE90);
+    case 'VERDE SCURO':
+      return const Color(0xFF006400);
+    case 'GIALLO':
+      return const Color(0xFFFFA500);
+    case 'GRIGIO':
+      return const Color(0xFF808080);
+    case 'ROSSO VIVO':
+      return const Color(0xFFA80000);
+    case 'ROSSO SCURO':
+      return const Color(0xFF8B0000);
+    case 'ROSSO':
+      return const Color(0xFFFF0000);
+    default:
+      return null;
+  }
+}
+
+class CustomerCard extends StatelessWidget {
+  final Cliente cliente;
+  const CustomerCard({super.key, required this.cliente});
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    final cs = theme.colorScheme;
+
+    return Card(
+      elevation: 0,
+      color: Colors.white,
+      clipBehavior: Clip.antiAlias,
+      shape: RoundedRectangleBorder(
+        borderRadius: BorderRadius.circular(12),
+        side: BorderSide(color: cs.outlineVariant),
+      ),
+      child: InkWell(
+        onTap: () => context.push('/dashboard/customers/${cliente.idCliente}'),
+        child: Padding(
+          padding: const EdgeInsets.all(16),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              Row(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Expanded(
+                    child: Text(
+                      cliente.ragioneSociale ?? '',
+                      style: theme.textTheme.bodyMedium?.copyWith(
+                        color: cs.primary,
+                      ),
+                      maxLines: 2,
+                      overflow: TextOverflow.ellipsis,
+                    ),
+                  ),
+                  if ((cliente.codCliente ?? '').isNotEmpty) ...[
+                    const SizedBox(width: 8),
+                    Container(
+                      padding: const EdgeInsets.symmetric(
+                        horizontal: 8,
+                        vertical: 4,
+                      ),
+                      decoration: BoxDecoration(
+                        color: _creditScoreBg(cliente.creditScoreColor) ??
+                            cs.surfaceVariant,
+                        borderRadius: BorderRadius.circular(4),
+                      ),
+                      child: Text(
+                        cliente.codCliente ?? '',
+                        style: TextStyle(
+                          fontSize: 12,
+                          height: 16 / 12,
+                          fontWeight: FontWeight.w500,
+                          color: _creditScoreBg(cliente.creditScoreColor) != null
+                              ? Colors.white
+                              : cs.onSurfaceVariant,
+                        ),
+                      ),
+                    ),
+                  ],
+                ],
+              ),
+
+              const SizedBox(height: 4),
+              Text(
+                cliente.email ?? '',
+                style: theme.textTheme.bodyMedium?.copyWith(
+                  color: cs.onSurfaceVariant,
+                ),
+                maxLines: 1,
+                overflow: TextOverflow.ellipsis,
+              ),
+
+              const SizedBox(height: 16),
+              Row(
+                children: [
+                  Icon(Icons.phone, size: 20, color: cs.onSurfaceVariant),
+                  const SizedBox(width: 8),
+                  Expanded(
+                    child: Text(
+                      cliente.telefono ?? '',
+                      style: theme.textTheme.bodyMedium?.copyWith(
+                        color: cs.onSurfaceVariant,
+                      ),
+                      maxLines: 1,
+                      overflow: TextOverflow.ellipsis,
+                    ),
+                  ),
+                ],
+              ),
+              Row(
+                children: [
+                  Icon(Icons.business, size: 20, color: cs.onSurfaceVariant),
+                  const SizedBox(width: 8),
+                  Expanded(
+                    child: Text(
+                      cliente.indirizzo ?? '',
+                      style: theme.textTheme.bodyMedium?.copyWith(
+                        color: cs.onSurfaceVariant,
+                      ),
+                      maxLines: 1,
+                      overflow: TextOverflow.ellipsis,
+                    ),
+                  ),
+                ],
+              ),
+              const SizedBox(height: 8),
+              Row(
+                children: [
+                  Icon(Icons.location_on, size: 20, color: cs.onSurfaceVariant),
+                  const SizedBox(width: 8),
+                  Expanded(
+                    child: Text(
+                      [
+                        if ((cliente.citta ?? '').isNotEmpty) cliente.citta,
+                        if ((cliente.provincia ?? '').isNotEmpty)
+                          cliente.provincia,
+                      ].join(', '),
+                      style: theme.textTheme.bodyMedium?.copyWith(
+                        color: cs.onSurfaceVariant,
+                      ),
+                      maxLines: 1,
+                      overflow: TextOverflow.ellipsis,
+                    ),
+                  ),
+                ],
+              ),
+              const SizedBox(height: 16),
+              Divider(height: 1, color: cs.outlineVariant),
+              const SizedBox(height: 12),
+              Row(
+                children: [
+                  Expanded(
+                    child: OutlinedButton.icon(
+                      onPressed: () {
+                        context.go('/dashboard/customers/${cliente.idCliente}');
+                      },
+                      icon: const Icon(Icons.info_outline, size: 18),
+                      label: const Text('Dettagli'),
+                      style: OutlinedButton.styleFrom(
+                        foregroundColor: cs.secondary,
+                        side: BorderSide(
+                          color: cs.secondary.withValues(alpha: 0.3),
+                        ),
+                      ),
+                    ),
+                  ),
+                  const SizedBox(width: 8),
+                  Expanded(
+                    child: FilledButton.icon(
+                      onPressed: () => context.go(
+                        '/dashboard/orders/new?cliente=${cliente.idCliente}',
+                      ),
+                      icon: const Icon(Icons.shopping_cart, size: 18),
+                      label: const Text('Nuovo ordine'),
+                    ),
+                  ),
+                ],
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است