From 79d641bb33d48311725493c09f664361f80950c8 Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Wed, 15 Apr 2026 14:43:38 +0200 Subject: [PATCH 1/5] Feat - created Services models --- .../services/models/energy_service_model.dart | 72 +++++++++ .../models/entertainment_service_model.dart | 70 +++++++++ .../services/models/fin_service_model.dart | 57 +++++++ .../services/models/service_model.dart | 146 ++++++++++++++++++ 4 files changed, 345 insertions(+) create mode 100644 lib/features/services/models/energy_service_model.dart create mode 100644 lib/features/services/models/entertainment_service_model.dart create mode 100644 lib/features/services/models/fin_service_model.dart create mode 100644 lib/features/services/models/service_model.dart diff --git a/lib/features/services/models/energy_service_model.dart b/lib/features/services/models/energy_service_model.dart new file mode 100644 index 0000000..a0e8547 --- /dev/null +++ b/lib/features/services/models/energy_service_model.dart @@ -0,0 +1,72 @@ +import 'package:equatable/equatable.dart'; + +enum EnergyType { luce, gas } // Mappa il tuo public.energy_type + +class EnergyServiceModel extends Equatable { + final String? id; + final DateTime? createdAt; + final EnergyType type; + final DateTime expiration; + final String gestoreId; + final String? serviceId; + + const EnergyServiceModel({ + this.id, + this.createdAt, + required this.type, + required this.expiration, + required this.gestoreId, + this.serviceId, + }); + + EnergyServiceModel copyWith({ + String? id, + DateTime? createdAt, + EnergyType? type, + DateTime? expiration, + String? gestoreId, + String? serviceId, + }) { + return EnergyServiceModel( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + expiration: expiration ?? this.expiration, + gestoreId: gestoreId ?? this.gestoreId, + serviceId: serviceId ?? this.serviceId, + ); + } + + @override + List get props => [ + id, + createdAt, + type, + expiration, + gestoreId, + serviceId, + ]; + + factory EnergyServiceModel.fromMap(Map map) { + return EnergyServiceModel( + id: map['id'], + createdAt: map['created_at'] != null + ? DateTime.parse(map['created_at']) + : null, + type: map['type'] == 'gas' ? EnergyType.gas : EnergyType.luce, + expiration: DateTime.parse(map['expiration']), + gestoreId: map['gestore_id'], + serviceId: map['service_id'], + ); + } + + Map toMap() { + return { + if (id != null) 'id': id, + 'type': type.name, // .name trasforma l'enum in 'luce' o 'gas' + 'expiration': expiration.toIso8601String(), + 'gestore_id': gestoreId, + 'service_id': serviceId, + }; + } +} diff --git a/lib/features/services/models/entertainment_service_model.dart b/lib/features/services/models/entertainment_service_model.dart new file mode 100644 index 0000000..f52de08 --- /dev/null +++ b/lib/features/services/models/entertainment_service_model.dart @@ -0,0 +1,70 @@ +import 'package:equatable/equatable.dart'; + +class EntertainmentServiceModel extends Equatable { + final String? id; + final DateTime? createdAt; + final String type; // es. Sky, DAZN, ecc. + final bool constrained; // Vincolato? + final DateTime constrainExpiration; + final String? serviceId; + + const EntertainmentServiceModel({ + this.id, + this.createdAt, + required this.type, + required this.constrained, + required this.constrainExpiration, + this.serviceId, + }); + + EntertainmentServiceModel copyWith({ + String? id, + DateTime? createdAt, + String? type, + bool? constrained, + DateTime? constrainExpiration, + String? serviceId, + }) { + return EntertainmentServiceModel( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + constrained: constrained ?? this.constrained, + constrainExpiration: constrainExpiration ?? this.constrainExpiration, + serviceId: serviceId ?? this.serviceId, + ); + } + + @override + List get props => [ + id, + createdAt, + type, + constrained, + constrainExpiration, + serviceId, + ]; + + factory EntertainmentServiceModel.fromMap(Map map) { + return EntertainmentServiceModel( + id: map['id'], + createdAt: map['created_at'] != null + ? DateTime.parse(map['created_at']) + : null, + type: map['type'], + constrained: map['constrained'] ?? false, + constrainExpiration: DateTime.parse(map['constrain_expiration']), + serviceId: map['service_id'], + ); + } + + Map toMap() { + return { + if (id != null) 'id': id, + 'type': type, + 'constrained': constrained, + 'constrain_expiration': constrainExpiration.toIso8601String(), + 'service_id': serviceId, + }; + } +} diff --git a/lib/features/services/models/fin_service_model.dart b/lib/features/services/models/fin_service_model.dart new file mode 100644 index 0000000..9a0f776 --- /dev/null +++ b/lib/features/services/models/fin_service_model.dart @@ -0,0 +1,57 @@ +import 'package:equatable/equatable.dart'; + +class FinServiceModel extends Equatable { + final String? id; + final DateTime? createdAt; + final DateTime expiration; + final String? serviceId; + final String? modelId; // FK verso model (es. iPhone, Samsung, ecc.) + + const FinServiceModel({ + this.id, + this.createdAt, + required this.expiration, + this.serviceId, + this.modelId, + }); + + FinServiceModel copyWith({ + String? id, + DateTime? createdAt, + DateTime? expiration, + String? serviceId, + String? modelId, + }) { + return FinServiceModel( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + expiration: expiration ?? this.expiration, + serviceId: serviceId ?? this.serviceId, + modelId: modelId ?? this.modelId, + ); + } + + @override + List get props => [id, createdAt, expiration, serviceId, modelId]; + + factory FinServiceModel.fromMap(Map map) { + return FinServiceModel( + id: map['id'], + createdAt: map['created_at'] != null + ? DateTime.parse(map['created_at']) + : null, + expiration: DateTime.parse(map['expiration']), + serviceId: map['service_id'], + modelId: map['model_id'], + ); + } + + Map toMap() { + return { + if (id != null) 'id': id, + 'expiration': expiration.toIso8601String(), + 'service_id': serviceId, + 'model_id': modelId, + }; + } +} diff --git a/lib/features/services/models/service_model.dart b/lib/features/services/models/service_model.dart new file mode 100644 index 0000000..9a83d6b --- /dev/null +++ b/lib/features/services/models/service_model.dart @@ -0,0 +1,146 @@ +import 'package:equatable/equatable.dart'; +import 'package:flux/features/services/models/energy_service_model.dart'; +import 'package:flux/features/services/models/entertainment_service_model.dart'; +import 'package:flux/features/services/models/fin_service_model.dart'; + +class ServiceModel extends Equatable { + final String? id; + final DateTime? createdAt; + final String storeId; + final String? employeeId; + final String? customerId; + final String number; + final bool isBozza; + final String note; + final bool resultOk; + + // Telefonia + final int al; + final int mnp; + final int nip; + final int unica; + final int telepass; + + // Moduli (Liste) + final List energyServices; + final List finServices; + final List entertainmentServices; + + const ServiceModel({ + this.id, + this.createdAt, + required this.storeId, + this.employeeId, + this.customerId, + required this.number, + this.isBozza = true, + this.note = '', + this.resultOk = true, + this.al = 0, + this.mnp = 0, + this.nip = 0, + this.unica = 0, + this.telepass = 0, + this.energyServices = const [], + this.finServices = const [], + this.entertainmentServices = const [], + }); + + ServiceModel copyWith({ + String? id, + DateTime? createdAt, + String? storeId, + String? employeeId, + String? customerId, + String? number, + bool? isBozza, + String? note, + bool? resultOk, + int? al, + int? mnp, + int? nip, + int? unica, + int? telepass, + List? energyServices, + List? finServices, + List? entertainmentServices, + }) { + return ServiceModel( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + storeId: storeId ?? this.storeId, + employeeId: employeeId ?? this.employeeId, + customerId: customerId ?? this.customerId, + number: number ?? this.number, + isBozza: isBozza ?? this.isBozza, + note: note ?? this.note, + resultOk: resultOk ?? this.resultOk, + al: al ?? this.al, + mnp: mnp ?? this.mnp, + nip: nip ?? this.nip, + unica: unica ?? this.unica, + telepass: telepass ?? this.telepass, + energyServices: energyServices ?? this.energyServices, + finServices: finServices ?? this.finServices, + entertainmentServices: + entertainmentServices ?? this.entertainmentServices, + ); + } + + @override + List get props => [ + id, + createdAt, + storeId, + employeeId, + customerId, + number, + isBozza, + note, + resultOk, + al, + mnp, + nip, + unica, + telepass, + energyServices, + finServices, + entertainmentServices, + ]; + + factory ServiceModel.fromMap(Map map) { + return ServiceModel( + id: map['id'], + createdAt: DateTime.parse(map['created_at']), + storeId: map['store_id'], + employeeId: map['employee_id'], + customerId: map['customer_id'], + number: map['number'] ?? '', + isBozza: map['bozza'] ?? true, + note: map['note'] ?? '', + resultOk: map['result_ok'] ?? true, + al: map['al'] ?? 0, + mnp: map['mnp'] ?? 0, + nip: map['nip'] ?? 0, + unica: map['unica'] ?? 0, + telepass: map['telepass'] ?? 0, + + // Mappaggio delle liste collegate (se incluse nella query) + energyServices: + (map['energy_service'] as List?) + ?.map((x) => EnergyServiceModel.fromMap(x)) + .toList() ?? + const [], + finServices: + (map['fin_service'] as List?) + ?.map((x) => FinServiceModel.fromMap(x)) + .toList() ?? + const [], + entertainmentServices: + (map['entertainment_service'] as List?) + ?.map((x) => EntertainmentServiceModel.fromMap(x)) + .toList() ?? + const [], + ); + } +} -- 2.43.0 From 52c5eec67de684cbf29228b4def46e619dfebe41 Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Wed, 15 Apr 2026 14:45:03 +0200 Subject: [PATCH 2/5] Feat - Services - added Service.toMap --- .../services/models/service_model.dart | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/lib/features/services/models/service_model.dart b/lib/features/services/models/service_model.dart index 9a83d6b..f403d3f 100644 --- a/lib/features/services/models/service_model.dart +++ b/lib/features/services/models/service_model.dart @@ -143,4 +143,23 @@ class ServiceModel extends Equatable { const [], ); } + + Map toMap() { + return { + if (id != null) 'id': id, + 'store_id': storeId, + 'employee_id': employeeId, + 'customer_id': customerId, + 'number': number, + 'bozza': isBozza, + 'note': note, + 'result_ok': resultOk, + 'al': al, + 'mnp': mnp, + 'nip': nip, + 'unica': unica, + 'telepass': telepass, + // Le liste non le mettiamo qui perché vanno in tabelle diverse! + }; + } } -- 2.43.0 From 61442339fef9bd934a5e70a6887b3c020a18521d Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Wed, 15 Apr 2026 15:21:19 +0200 Subject: [PATCH 3/5] Teat - Services - fixed models with provider_id --- .../services/data/services_repository.dart | 83 +++++++++++++++++++ .../services/models/energy_service_model.dart | 14 ++-- .../models/entertainment_service_model.dart | 7 ++ .../services/models/fin_service_model.dart | 6 ++ 4 files changed, 103 insertions(+), 7 deletions(-) create mode 100644 lib/features/services/data/services_repository.dart diff --git a/lib/features/services/data/services_repository.dart b/lib/features/services/data/services_repository.dart new file mode 100644 index 0000000..aae513f --- /dev/null +++ b/lib/features/services/data/services_repository.dart @@ -0,0 +1,83 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; +import '../models/service_model.dart'; +// Importa gli altri modelli se sono in file separati + +class ServicesRepository { + final _supabase = Supabase.instance.client; + + // --- RECUPERO TUTTI I SERVIZI --- + Future> fetchAllServices() async { + try { + // La stringa di selezione tira giù il padre e TUTTI i record correlati dalle tabelle figlie + final response = await _supabase + .from('service') + .select(''' + *, + energy_service(*), + fin_service(*), + entertainment_service(*) + ''') + .order('created_at', ascending: false); + + return (response as List) + .map((map) => ServiceModel.fromMap(map)) + .toList(); + } catch (e) { + throw Exception('Errore nel recupero servizi: $e'); + } + } + + // --- SALVATAGGIO COMPLETO (A CASCATA) --- + Future saveFullService(ServiceModel service) async { + try { + // 1. Inserimento Padre + final serviceData = await _supabase + .from('service') + .insert(service.toMap()) + .select() + .single(); + + final String newId = serviceData['id']; + + // 2. Inserimento Energy (se presenti) + if (service.energyServices.isNotEmpty) { + final List> energyToInsert = []; + for (var item in service.energyServices) { + energyToInsert.add(item.copyWith(serviceId: newId).toMap()); + } + await _supabase.from('energy_service').insert(energyToInsert); + } + + // 3. Inserimento Finanziamenti (se presenti) + if (service.finServices.isNotEmpty) { + final List> finToInsert = []; + for (var item in service.finServices) { + finToInsert.add(item.copyWith(serviceId: newId).toMap()); + } + await _supabase.from('fin_service').insert(finToInsert); + } + + // 4. Inserimento Entertainment (se presenti) + if (service.entertainmentServices.isNotEmpty) { + final List> entToInsert = []; + for (var item in service.entertainmentServices) { + entToInsert.add(item.copyWith(serviceId: newId).toMap()); + } + await _supabase.from('entertainment_service').insert(entToInsert); + } + } catch (e) { + throw Exception('Errore durante il salvataggio: $e'); + } + } + + // --- ELIMINAZIONE --- + // Grazie ai "ON DELETE CASCADE" che hai messo nell'SQL, + // cancellando il padre Supabase pialla automaticamente i figli. Top! + Future deleteService(String id) async { + try { + await _supabase.from('service').delete().eq('id', id); + } catch (e) { + throw Exception('Errore durante l\'eliminazione: $e'); + } + } +} diff --git a/lib/features/services/models/energy_service_model.dart b/lib/features/services/models/energy_service_model.dart index a0e8547..9cf9b54 100644 --- a/lib/features/services/models/energy_service_model.dart +++ b/lib/features/services/models/energy_service_model.dart @@ -7,7 +7,7 @@ class EnergyServiceModel extends Equatable { final DateTime? createdAt; final EnergyType type; final DateTime expiration; - final String gestoreId; + final String providerId; final String? serviceId; const EnergyServiceModel({ @@ -15,7 +15,7 @@ class EnergyServiceModel extends Equatable { this.createdAt, required this.type, required this.expiration, - required this.gestoreId, + required this.providerId, this.serviceId, }); @@ -24,7 +24,7 @@ class EnergyServiceModel extends Equatable { DateTime? createdAt, EnergyType? type, DateTime? expiration, - String? gestoreId, + String? providerId, String? serviceId, }) { return EnergyServiceModel( @@ -32,7 +32,7 @@ class EnergyServiceModel extends Equatable { createdAt: createdAt ?? this.createdAt, type: type ?? this.type, expiration: expiration ?? this.expiration, - gestoreId: gestoreId ?? this.gestoreId, + providerId: providerId ?? this.providerId, serviceId: serviceId ?? this.serviceId, ); } @@ -43,7 +43,7 @@ class EnergyServiceModel extends Equatable { createdAt, type, expiration, - gestoreId, + providerId, serviceId, ]; @@ -55,7 +55,7 @@ class EnergyServiceModel extends Equatable { : null, type: map['type'] == 'gas' ? EnergyType.gas : EnergyType.luce, expiration: DateTime.parse(map['expiration']), - gestoreId: map['gestore_id'], + providerId: map['provider_id'], serviceId: map['service_id'], ); } @@ -65,7 +65,7 @@ class EnergyServiceModel extends Equatable { if (id != null) 'id': id, 'type': type.name, // .name trasforma l'enum in 'luce' o 'gas' 'expiration': expiration.toIso8601String(), - 'gestore_id': gestoreId, + 'provider_id': providerId, 'service_id': serviceId, }; } diff --git a/lib/features/services/models/entertainment_service_model.dart b/lib/features/services/models/entertainment_service_model.dart index f52de08..f34743a 100644 --- a/lib/features/services/models/entertainment_service_model.dart +++ b/lib/features/services/models/entertainment_service_model.dart @@ -7,6 +7,7 @@ class EntertainmentServiceModel extends Equatable { final bool constrained; // Vincolato? final DateTime constrainExpiration; final String? serviceId; + final String? providerId; const EntertainmentServiceModel({ this.id, @@ -15,6 +16,7 @@ class EntertainmentServiceModel extends Equatable { required this.constrained, required this.constrainExpiration, this.serviceId, + this.providerId, }); EntertainmentServiceModel copyWith({ @@ -24,6 +26,7 @@ class EntertainmentServiceModel extends Equatable { bool? constrained, DateTime? constrainExpiration, String? serviceId, + String? providerId, }) { return EntertainmentServiceModel( id: id ?? this.id, @@ -32,6 +35,7 @@ class EntertainmentServiceModel extends Equatable { constrained: constrained ?? this.constrained, constrainExpiration: constrainExpiration ?? this.constrainExpiration, serviceId: serviceId ?? this.serviceId, + providerId: providerId ?? this.providerId, ); } @@ -43,6 +47,7 @@ class EntertainmentServiceModel extends Equatable { constrained, constrainExpiration, serviceId, + providerId, ]; factory EntertainmentServiceModel.fromMap(Map map) { @@ -55,6 +60,7 @@ class EntertainmentServiceModel extends Equatable { constrained: map['constrained'] ?? false, constrainExpiration: DateTime.parse(map['constrain_expiration']), serviceId: map['service_id'], + providerId: map['provider_id'], ); } @@ -65,6 +71,7 @@ class EntertainmentServiceModel extends Equatable { 'constrained': constrained, 'constrain_expiration': constrainExpiration.toIso8601String(), 'service_id': serviceId, + 'provider_id': providerId, }; } } diff --git a/lib/features/services/models/fin_service_model.dart b/lib/features/services/models/fin_service_model.dart index 9a0f776..9cdaa5a 100644 --- a/lib/features/services/models/fin_service_model.dart +++ b/lib/features/services/models/fin_service_model.dart @@ -6,6 +6,7 @@ class FinServiceModel extends Equatable { final DateTime expiration; final String? serviceId; final String? modelId; // FK verso model (es. iPhone, Samsung, ecc.) + final String? providerId; const FinServiceModel({ this.id, @@ -13,6 +14,7 @@ class FinServiceModel extends Equatable { required this.expiration, this.serviceId, this.modelId, + this.providerId, }); FinServiceModel copyWith({ @@ -21,6 +23,7 @@ class FinServiceModel extends Equatable { DateTime? expiration, String? serviceId, String? modelId, + String? providerId, }) { return FinServiceModel( id: id ?? this.id, @@ -28,6 +31,7 @@ class FinServiceModel extends Equatable { expiration: expiration ?? this.expiration, serviceId: serviceId ?? this.serviceId, modelId: modelId ?? this.modelId, + providerId: providerId ?? this.providerId, ); } @@ -43,6 +47,7 @@ class FinServiceModel extends Equatable { expiration: DateTime.parse(map['expiration']), serviceId: map['service_id'], modelId: map['model_id'], + providerId: map['provider_id'], ); } @@ -52,6 +57,7 @@ class FinServiceModel extends Equatable { 'expiration': expiration.toIso8601String(), 'service_id': serviceId, 'model_id': modelId, + 'provider_id': providerId, }; } } -- 2.43.0 From 29790a7a361fed33493556e4fa3bb9ee7c5b0721 Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Wed, 15 Apr 2026 19:31:08 +0200 Subject: [PATCH 4/5] feat: service set repos and cubit --- .../services/blocs/services_cubit.dart | 111 ++++++++++++++++++ .../services/data/services_repository.dart | 51 ++++++-- 2 files changed, 149 insertions(+), 13 deletions(-) create mode 100644 lib/features/services/blocs/services_cubit.dart diff --git a/lib/features/services/blocs/services_cubit.dart b/lib/features/services/blocs/services_cubit.dart new file mode 100644 index 0000000..81b4034 --- /dev/null +++ b/lib/features/services/blocs/services_cubit.dart @@ -0,0 +1,111 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/features/services/data/services_repository.dart'; +import 'package:flux/features/services/models/service_model.dart'; +import 'package:get_it/get_it.dart'; + +class ServicesState extends Equatable { + final List allServices; + final bool isLoading; + final bool hasReachedMax; // Per lo scroll infinito + final String? errorMessage; + // Parametri di ricerca + final String query; + final DateTimeRange? dateRange; + + const ServicesState({ + this.allServices = const [], + this.isLoading = false, + this.hasReachedMax = false, + this.errorMessage, + this.query = '', + this.dateRange, + }); + ServicesState copyWith({ + List? allServices, + bool? isLoading, + String? errorMessage, + bool? hasReachedMax, + String? query, + DateTimeRange? dateRange, + }) { + return ServicesState( + allServices: allServices ?? this.allServices, + isLoading: isLoading ?? this.isLoading, + errorMessage: + errorMessage, // Se non lo passiamo, torna null (pulisce l'errore) + hasReachedMax: hasReachedMax ?? this.hasReachedMax, + query: query ?? this.query, + dateRange: dateRange ?? this.dateRange, + ); + } + + @override + List get props => [ + allServices, + isLoading, + hasReachedMax, + errorMessage, + query, + dateRange, + ]; +} + +class ServicesCubit extends Cubit { + final ServicesRepository _repository = GetIt.I(); + + ServicesCubit() : super(const ServicesState()); + + // Carica tutto il pacchetto + Future loadServices({bool refresh = false}) async { + if (state.isLoading) return; + + // Se facciamo refresh, resettiamo tutto + final currentOffset = refresh ? 0 : state.allServices.length; + + emit( + state.copyWith( + isLoading: true, + allServices: refresh ? [] : state.allServices, + hasReachedMax: refresh ? false : state.hasReachedMax, + ), + ); + + try { + final newServices = await _repository.fetchServices( + offset: currentOffset, + searchTerm: state.query, + dateRange: state.dateRange, + ); + + emit( + state.copyWith( + isLoading: false, + allServices: List.from(state.allServices)..addAll(newServices), + hasReachedMax: + newServices.length < + 50, // Se ne arrivano meno di 50, siamo alla fine + ), + ); + } catch (e) { + emit(state.copyWith(isLoading: false, errorMessage: e.toString())); + } + } + + void updateFilters({String? query, DateTimeRange? range}) { + emit(state.copyWith(query: query, dateRange: range)); + loadServices(refresh: true); // Applica i filtri e riparte da zero + } + + // Salva e ricarica + Future addService(ServiceModel service) async { + emit(state.copyWith(isLoading: true)); + try { + await _repository.saveFullService(service); + await loadServices(); // Ricarichiamo la lista aggiornata + } catch (e) { + emit(state.copyWith(isLoading: false, errorMessage: e.toString())); + } + } +} diff --git a/lib/features/services/data/services_repository.dart b/lib/features/services/data/services_repository.dart index aae513f..157f2df 100644 --- a/lib/features/services/data/services_repository.dart +++ b/lib/features/services/data/services_repository.dart @@ -1,3 +1,4 @@ +import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/service_model.dart'; // Importa gli altri modelli se sono in file separati @@ -6,24 +7,48 @@ class ServicesRepository { final _supabase = Supabase.instance.client; // --- RECUPERO TUTTI I SERVIZI --- - Future> fetchAllServices() async { + Future> fetchServices({ + required int offset, + int limit = 50, + String? searchTerm, + DateTimeRange? dateRange, + }) async { try { - // La stringa di selezione tira giù il padre e TUTTI i record correlati dalle tabelle figlie - final response = await _supabase - .from('service') - .select(''' - *, - energy_service(*), - fin_service(*), - entertainment_service(*) - ''') - .order('created_at', ascending: false); + var query = _supabase.from('service').select(''' + *, + energy_service(*), + fin_service(*), + entertainment_service(*) + '''); - return (response as List) + // Filtro per range di date + if (dateRange != null) { + query = query + .gte('created_at', dateRange.start.toIso8601String()) + .lte('created_at', dateRange.end.toIso8601String()); + } + + // Ordinamento e Paginazione + final response = await query + .order('created_at', ascending: false) + .range(offset, offset + limit - 1); + + final List services = (response as List) .map((map) => ServiceModel.fromMap(map)) .toList(); + + // Filtro testuale lato client per semplicità (o potresti farlo in SQL se preferisci) + if (searchTerm != null && searchTerm.isNotEmpty) { + return services.where((s) { + // Qui cercheremo per numero pratica o note (il nome cliente lo vedremo poi con le Join) + return s.number.toLowerCase().contains(searchTerm.toLowerCase()) || + s.note.toLowerCase().contains(searchTerm.toLowerCase()); + }).toList(); + } + + return services; } catch (e) { - throw Exception('Errore nel recupero servizi: $e'); + throw Exception('Errore fetch: $e'); } } -- 2.43.0 From 787873a26fa7be31eadc06250807df6c78c97075 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Thu, 16 Apr 2026 11:48:11 +0200 Subject: [PATCH 5/5] feat - Service - Providers --- lib/core/routes/app_router.dart | 11 + lib/features/home/ui/home_screen.dart | 40 ++-- .../providers/blocs/provider_cubit.dart | 113 ++++++++++ .../providers/data/provider_repository.dart | 93 +++++++++ .../providers/models/provider_model.dart | 96 +++++++++ .../services/blocs/services_cubit.dart | 7 +- .../services/data/services_repository.dart | 90 ++++---- .../services/models/service_model.dart | 8 + .../services/ui/service_form_screen.dart | 150 +++++++++++++ lib/features/services/ui/services_screen.dart | 197 ++++++++++++++++++ lib/main.dart | 8 + 11 files changed, 758 insertions(+), 55 deletions(-) create mode 100644 lib/features/master_data/providers/blocs/provider_cubit.dart create mode 100644 lib/features/master_data/providers/data/provider_repository.dart create mode 100644 lib/features/master_data/providers/models/provider_model.dart create mode 100644 lib/features/services/ui/service_form_screen.dart create mode 100644 lib/features/services/ui/services_screen.dart diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 8af21d0..6ac8f4b 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -7,6 +7,8 @@ import 'package:flux/features/customers/ui/customer_detail_screen.dart'; import 'package:flux/features/home/ui/home_screen.dart'; import 'package:flux/features/master_data/products/ui/products_screen.dart'; import 'package:flux/features/master_data/store/ui/create_store_screen.dart'; +import 'package:flux/features/services/models/service_model.dart'; +import 'package:flux/features/services/ui/service_form_screen.dart'; import 'package:go_router/go_router.dart'; import 'dart:async'; @@ -74,6 +76,15 @@ class AppRouter { name: 'products', builder: (context, state) => const ProductsScreen(), ), + GoRoute( + path: '/service-form', + name: 'service-form', + builder: (context, state) { + // Recuperiamo il ServiceModel se passato come extra + final service = state.extra as ServiceModel?; + return ServiceFormScreen(initialService: service); + }, + ), ], ); } diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 10f5e05..d092197 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -3,6 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_bloc.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/features/master_data/master_data_hub_content.dart'; +import 'package:flux/features/services/blocs/services_cubit.dart'; +import 'package:flux/features/services/ui/services_screen.dart'; import 'dashboard_content.dart'; // Importiamo il contenuto della dashboard class HomeScreen extends StatefulWidget { @@ -16,6 +18,16 @@ class _HomeScreenState extends State { int _selectedIndex = 0; bool _extendRailway = false; + @override + void initState() { + super.initState(); + // Caricamento "silenzioso" all'avvio dell'app + // Usiamo WidgetsBinding per assicurarci che il contesto sia pronto + WidgetsBinding.instance.addPostFrameCallback((_) { + context.read().loadServices(); + }); + } + @override Widget build(BuildContext context) { return BlocBuilder( @@ -63,7 +75,7 @@ class _HomeScreenState extends State { ), BottomNavigationBarItem( icon: Icon(Icons.receipt_long), - label: 'Operazioni', + label: 'Servizi', ), BottomNavigationBarItem( icon: Icon(Icons.folder_shared), @@ -111,7 +123,7 @@ class _HomeScreenState extends State { NavigationRailDestination( icon: Icon(Icons.receipt_long_outlined), selectedIcon: Icon(Icons.receipt_long), - label: Text('Operazioni'), + label: Text('Servizi'), ), NavigationRailDestination( icon: Icon(Icons.folder_shared_outlined), @@ -148,17 +160,18 @@ class _HomeScreenState extends State { // Switch tra le sottopagine Widget _buildPageContent(int index, bool isLargeScreen) { - switch (index) { - case 0: - return DashboardContent( + return IndexedStack( + index: index, + children: [ + DashboardContent( isLargeScreen: isLargeScreen, onTabRequested: (idx) => setState(() => _selectedIndex = 2), - ); - case 1: - return const Center(child: Text('Operazioni')); - case 2: + ), + + ServicesScreen(), + // L'unico punto di ingresso per tutte le anagrafiche - return MasterDataHubContent( + MasterDataHubContent( // Qui gestiamo la navigazione "interna" all'hub onOpenPage: (widget) { Navigator.push( @@ -166,9 +179,8 @@ class _HomeScreenState extends State { MaterialPageRoute(builder: (context) => widget), ); }, - ); - default: - return DashboardContent(isLargeScreen: isLargeScreen); - } + ), + ], + ); } } diff --git a/lib/features/master_data/providers/blocs/provider_cubit.dart b/lib/features/master_data/providers/blocs/provider_cubit.dart new file mode 100644 index 0000000..07ea2d7 --- /dev/null +++ b/lib/features/master_data/providers/blocs/provider_cubit.dart @@ -0,0 +1,113 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/features/master_data/providers/data/provider_repository.dart'; +import 'package:get_it/get_it.dart'; +import '../models/provider_model.dart'; + +class ProvidersState extends Equatable { + final List allProviders; // Tutti i provider della company + final List + associatedIds; // ID dei provider attivi nello store selezionato + final bool isLoading; + final String? errorMessage; + + const ProvidersState({ + this.allProviders = const [], + this.associatedIds = const [], + this.isLoading = false, + this.errorMessage, + }); + + ProvidersState copyWith({ + List? allProviders, + List? associatedIds, + bool? isLoading, + String? errorMessage, + }) { + return ProvidersState( + allProviders: allProviders ?? this.allProviders, + associatedIds: associatedIds ?? this.associatedIds, + isLoading: isLoading ?? this.isLoading, + errorMessage: errorMessage, + ); + } + + @override + List get props => [ + allProviders, + associatedIds, + isLoading, + errorMessage, + ]; +} + +class ProvidersCubit extends Cubit { + final ProviderRepository _repository = GetIt.I(); + + ProvidersCubit() : super(const ProvidersState()); + + // Carica i provider della company e quelli associati a uno store specifico + Future loadProviders(String companyId, String? storeId) async { + emit(state.copyWith(isLoading: true)); + try { + final all = await _repository.fetchAllCompanyProviders(companyId); + List associated = []; + + if (storeId != null) { + associated = await _repository.fetchAssociatedProviderIds(storeId); + } + + emit( + state.copyWith( + allProviders: all, + associatedIds: associated, + isLoading: false, + ), + ); + } catch (e) { + emit(state.copyWith(isLoading: false, errorMessage: e.toString())); + } + } + + // Aggiunge o rimuove l'associazione con lo store + Future toggleProviderAssociation({ + required String providerId, + required String storeId, + required bool isCurrentlyAssociated, + }) async { + try { + if (isCurrentlyAssociated) { + await _repository.disassociateProviderFromStore( + providerId: providerId, + storeId: storeId, + ); + // Aggiorniamo lo stato locale rimuovendo l'ID + final newIds = List.from(state.associatedIds) + ..remove(providerId); + emit(state.copyWith(associatedIds: newIds)); + } else { + await _repository.associateProviderToStore( + providerId: providerId, + storeId: storeId, + ); + // Aggiorniamo lo stato locale aggiungendo l'ID + final newIds = List.from(state.associatedIds)..add(providerId); + emit(state.copyWith(associatedIds: newIds)); + } + } catch (e) { + emit(state.copyWith(errorMessage: "Errore durante l'aggiornamento: $e")); + } + } + + // Salvataggio/Update anagrafica (nuovo o modifica) + Future saveProvider(ProviderModel provider) async { + emit(state.copyWith(isLoading: true)); + try { + await _repository.saveProvider(provider); + // Ricarichiamo la lista per vedere le modifiche + await loadProviders(provider.companyId, null); + } catch (e) { + emit(state.copyWith(isLoading: false, errorMessage: e.toString())); + } + } +} diff --git a/lib/features/master_data/providers/data/provider_repository.dart b/lib/features/master_data/providers/data/provider_repository.dart new file mode 100644 index 0000000..46bb48a --- /dev/null +++ b/lib/features/master_data/providers/data/provider_repository.dart @@ -0,0 +1,93 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; +import '../models/provider_model.dart'; + +class ProviderRepository { + final _supabase = Supabase.instance.client; + + // --- ASSOCIAZIONE PROVIDER <-> STORE --- + + // Aggiunge un provider a un negozio (Attiva mandato) + Future associateProviderToStore({ + required String providerId, + required String storeId, + }) async { + try { + await _supabase.from('providers_in_stores').insert({ + 'provider_id': providerId, + 'store_id': storeId, + }); + } catch (e) { + throw Exception('Errore durante l\'associazione provider: $e'); + } + } + + // Rimuove un provider da un negozio (Disattiva mandato) + Future disassociateProviderFromStore({ + required String providerId, + required String storeId, + }) async { + try { + await _supabase + .from('providers_in_stores') + .delete() + .eq('provider_id', providerId) + .eq('store_id', storeId); + } catch (e) { + throw Exception('Errore durante la disassociazione provider: $e'); + } + } + + // Recupera tutti i provider di una company (per la lista generale) + Future> fetchAllCompanyProviders(String companyId) async { + try { + final response = await _supabase + .from('provider') + .select() + .eq('company_id', companyId); + + return (response as List).map((m) => ProviderModel.fromMap(m)).toList(); + } catch (e) { + throw Exception('Errore fetch provider: $e'); + } + } + + // Recupera gli ID dei provider associati a uno store (utile per le checkbox) + Future> fetchAssociatedProviderIds(String storeId) async { + try { + final response = await _supabase + .from('providers_in_stores') + .select('provider_id') + .eq('store_id', storeId); + + return (response as List) + .map((item) => item['provider_id'].toString()) + .toList(); + } catch (e) { + throw Exception('Errore recupero ID associati: $e'); + } + } + + // --- FUNZIONI STANDARD --- + + // Questa la userai nel Form Servizi: carica solo i provider abilitati per lo store + Future> fetchActiveProvidersForStore( + String storeId, + ) async { + try { + final response = await _supabase + .from('provider') + .select('*, providers_in_stores!inner(store_id)') + .eq('providers_in_stores.store_id', storeId) + .eq('is_active', true); + + return (response as List).map((m) => ProviderModel.fromMap(m)).toList(); + } catch (e) { + throw Exception('Errore fetch provider attivi: $e'); + } + } + + // Salva o aggiorna l'anagrafica del Provider + Future saveProvider(ProviderModel provider) async { + await _supabase.from('provider').upsert(provider.toMap()); + } +} diff --git a/lib/features/master_data/providers/models/provider_model.dart b/lib/features/master_data/providers/models/provider_model.dart new file mode 100644 index 0000000..55cb4f8 --- /dev/null +++ b/lib/features/master_data/providers/models/provider_model.dart @@ -0,0 +1,96 @@ +import 'package:equatable/equatable.dart'; + +class ProviderModel extends Equatable { + final String id; + final String nome; + final bool telefoniaFissa; + final bool telefoniaMobile; + final bool energia; + final bool assicurazioni; + final bool intrattenimento; + final bool altro; + final bool isActive; + final String companyId; + + const ProviderModel({ + required this.id, + required this.nome, + required this.telefoniaFissa, + required this.telefoniaMobile, + required this.energia, + required this.assicurazioni, + required this.intrattenimento, + required this.altro, + required this.isActive, + required this.companyId, + }); + + factory ProviderModel.fromMap(Map map) { + return ProviderModel( + id: map['id'], + nome: map['nome'], + telefoniaFissa: map['telefonia_fissa'] ?? false, + telefoniaMobile: map['telefonia_mobile'] ?? false, + energia: map['energia'] ?? false, + assicurazioni: map['assicurazioni'] ?? false, + intrattenimento: map['intrattenimento'] ?? false, + altro: map['altro'] ?? false, + isActive: map['is_active'] ?? true, + companyId: map['company_id'], + ); + } + + Map toMap() { + return { + 'nome': nome, + 'telefonia_fissa': telefoniaFissa, + 'telefonia_mobile': telefoniaMobile, + 'energia': energia, + 'assicurazioni': assicurazioni, + 'intrattenimento': intrattenimento, + 'altro': altro, + 'is_active': isActive, + 'company_id': companyId, + }; + } + + @override + List get props => [ + id, + nome, + telefoniaFissa, + telefoniaMobile, + energia, + assicurazioni, + intrattenimento, + altro, + isActive, + companyId, + ]; + + ProviderModel copyWith({ + String? id, + String? nome, + bool? telefoniaFissa, + bool? telefoniaMobile, + bool? energia, + bool? assicurazioni, + bool? intrattenimento, + bool? altro, + bool? isActive, + String? companyId, + }) { + return ProviderModel( + id: id ?? this.id, + nome: nome ?? this.nome, + telefoniaFissa: telefoniaFissa ?? this.telefoniaFissa, + telefoniaMobile: telefoniaMobile ?? this.telefoniaMobile, + energia: energia ?? this.energia, + assicurazioni: assicurazioni ?? this.assicurazioni, + intrattenimento: intrattenimento ?? this.intrattenimento, + altro: altro ?? this.altro, + isActive: isActive ?? this.isActive, + companyId: companyId ?? this.companyId, + ); + } +} diff --git a/lib/features/services/blocs/services_cubit.dart b/lib/features/services/blocs/services_cubit.dart index 81b4034..66cc1a4 100644 --- a/lib/features/services/blocs/services_cubit.dart +++ b/lib/features/services/blocs/services_cubit.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_bloc.dart'; import 'package:flux/features/services/data/services_repository.dart'; import 'package:flux/features/services/models/service_model.dart'; import 'package:get_it/get_it.dart'; @@ -54,11 +55,14 @@ class ServicesState extends Equatable { class ServicesCubit extends Cubit { final ServicesRepository _repository = GetIt.I(); + final SessionBloc _sessionBloc; - ServicesCubit() : super(const ServicesState()); + ServicesCubit(this._sessionBloc) : super(const ServicesState()); // Carica tutto il pacchetto Future loadServices({bool refresh = false}) async { + // Se non è un refresh e abbiamo già dati, non disturbare Supabase + if (!refresh && state.allServices.isNotEmpty) return; if (state.isLoading) return; // Se facciamo refresh, resettiamo tutto @@ -74,6 +78,7 @@ class ServicesCubit extends Cubit { try { final newServices = await _repository.fetchServices( + companyId: _sessionBloc.state.company!.id, offset: currentOffset, searchTerm: state.query, dateRange: state.dateRange, diff --git a/lib/features/services/data/services_repository.dart b/lib/features/services/data/services_repository.dart index 157f2df..7da3966 100644 --- a/lib/features/services/data/services_repository.dart +++ b/lib/features/services/data/services_repository.dart @@ -1,94 +1,106 @@ import 'package:flutter/material.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/service_model.dart'; -// Importa gli altri modelli se sono in file separati class ServicesRepository { final _supabase = Supabase.instance.client; - // --- RECUPERO TUTTI I SERVIZI --- + // --- RECUPERO PAGINATO CON FILTRI E JOIN --- Future> fetchServices({ + required String companyId, required int offset, int limit = 50, String? searchTerm, DateTimeRange? dateRange, }) async { try { - var query = _supabase.from('service').select(''' - *, - energy_service(*), - fin_service(*), - entertainment_service(*) - '''); + // Nota: 'customer(name, surname)' serve per il display name nella card + var query = _supabase + .from('service') + .select(''' + *, + customer(name, surname), + energy_service(*), + fin_service(*), + entertainment_service(*) + ''') + .eq('company_id', companyId); - // Filtro per range di date + // Filtro Range Date if (dateRange != null) { query = query .gte('created_at', dateRange.start.toIso8601String()) .lte('created_at', dateRange.end.toIso8601String()); } - // Ordinamento e Paginazione + if (searchTerm != null && searchTerm.isNotEmpty) { + // Filtra sui campi della tabella principale O su quelli della tabella joinata + query = query.or( + 'number.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.name.ilike.%$searchTerm%,customer.surname.ilike.%$searchTerm%', + ); + } + final response = await query .order('created_at', ascending: false) .range(offset, offset + limit - 1); - final List services = (response as List) + return (response as List) .map((map) => ServiceModel.fromMap(map)) .toList(); - - // Filtro testuale lato client per semplicità (o potresti farlo in SQL se preferisci) - if (searchTerm != null && searchTerm.isNotEmpty) { - return services.where((s) { - // Qui cercheremo per numero pratica o note (il nome cliente lo vedremo poi con le Join) - return s.number.toLowerCase().contains(searchTerm.toLowerCase()) || - s.note.toLowerCase().contains(searchTerm.toLowerCase()); - }).toList(); - } - - return services; } catch (e) { - throw Exception('Errore fetch: $e'); + throw Exception('Errore nel caricamento servizi: $e'); } } - // --- SALVATAGGIO COMPLETO (A CASCATA) --- + // --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) --- Future saveFullService(ServiceModel service) async { try { - // 1. Inserimento Padre + // 1. Inseriamo il record principale + // Se service.id è null, Supabase fa INSERT. Se c'è, fa UPDATE (grazie all'upsert o gestione manuale) final serviceData = await _supabase .from('service') - .insert(service.toMap()) + .upsert(service.toMap()) .select() .single(); final String newId = serviceData['id']; - // 2. Inserimento Energy (se presenti) + // 2. Pulizia vecchi record figli (necessaria se è una MODIFICA) + // Se stiamo modificando, cancelliamo i vecchi per reinserire i nuovi (più semplice) + if (service.id != null) { + await _supabase.from('energy_service').delete().eq('service_id', newId); + await _supabase.from('fin_service').delete().eq('service_id', newId); + await _supabase + .from('entertainment_service') + .delete() + .eq('service_id', newId); + } + + // 3. Inserimento EnergyServices if (service.energyServices.isNotEmpty) { - final List> energyToInsert = []; + final List> toInsert = []; for (var item in service.energyServices) { - energyToInsert.add(item.copyWith(serviceId: newId).toMap()); + toInsert.add(item.copyWith(serviceId: newId).toMap()); } - await _supabase.from('energy_service').insert(energyToInsert); + await _supabase.from('energy_service').insert(toInsert); } - // 3. Inserimento Finanziamenti (se presenti) + // 4. Inserimento FinServices if (service.finServices.isNotEmpty) { - final List> finToInsert = []; + final List> toInsert = []; for (var item in service.finServices) { - finToInsert.add(item.copyWith(serviceId: newId).toMap()); + toInsert.add(item.copyWith(serviceId: newId).toMap()); } - await _supabase.from('fin_service').insert(finToInsert); + await _supabase.from('fin_service').insert(toInsert); } - // 4. Inserimento Entertainment (se presenti) + // 5. Inserimento EntertainmentServices if (service.entertainmentServices.isNotEmpty) { - final List> entToInsert = []; + final List> toInsert = []; for (var item in service.entertainmentServices) { - entToInsert.add(item.copyWith(serviceId: newId).toMap()); + toInsert.add(item.copyWith(serviceId: newId).toMap()); } - await _supabase.from('entertainment_service').insert(entToInsert); + await _supabase.from('entertainment_service').insert(toInsert); } } catch (e) { throw Exception('Errore durante il salvataggio: $e'); @@ -96,8 +108,6 @@ class ServicesRepository { } // --- ELIMINAZIONE --- - // Grazie ai "ON DELETE CASCADE" che hai messo nell'SQL, - // cancellando il padre Supabase pialla automaticamente i figli. Top! Future deleteService(String id) async { try { await _supabase.from('service').delete().eq('id', id); diff --git a/lib/features/services/models/service_model.dart b/lib/features/services/models/service_model.dart index f403d3f..5bbfc10 100644 --- a/lib/features/services/models/service_model.dart +++ b/lib/features/services/models/service_model.dart @@ -13,6 +13,7 @@ class ServiceModel extends Equatable { final bool isBozza; final String note; final bool resultOk; + final String? customerDisplayName; // Telefonia final int al; @@ -44,6 +45,7 @@ class ServiceModel extends Equatable { this.energyServices = const [], this.finServices = const [], this.entertainmentServices = const [], + this.customerDisplayName, }); ServiceModel copyWith({ @@ -64,6 +66,7 @@ class ServiceModel extends Equatable { List? energyServices, List? finServices, List? entertainmentServices, + String? customerDisplayName, }) { return ServiceModel( id: id ?? this.id, @@ -84,6 +87,7 @@ class ServiceModel extends Equatable { finServices: finServices ?? this.finServices, entertainmentServices: entertainmentServices ?? this.entertainmentServices, + customerDisplayName: customerDisplayName ?? this.customerDisplayName, ); } @@ -106,6 +110,7 @@ class ServiceModel extends Equatable { energyServices, finServices, entertainmentServices, + customerDisplayName, ]; factory ServiceModel.fromMap(Map map) { @@ -141,6 +146,9 @@ class ServiceModel extends Equatable { ?.map((x) => EntertainmentServiceModel.fromMap(x)) .toList() ?? const [], + customerDisplayName: map['customer'] != null + ? "${map['customer']['name']} ${map['customer']['surname']}" + : "Cliente sconosciuto", ); } diff --git a/lib/features/services/ui/service_form_screen.dart b/lib/features/services/ui/service_form_screen.dart new file mode 100644 index 0000000..d63599c --- /dev/null +++ b/lib/features/services/ui/service_form_screen.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/features/services/blocs/services_cubit.dart'; +import 'package:flux/features/services/models/energy_service_model.dart'; +import 'package:flux/features/services/models/service_model.dart'; + +class ServiceFormScreen extends StatefulWidget { + final ServiceModel? initialService; // Se nullo, è un nuovo inserimento + + const ServiceFormScreen({super.key, this.initialService}); + + @override + State createState() => _ServiceFormScreenState(); +} + +class _ServiceFormScreenState extends State { + late ServiceModel currentService; + + @override + void initState() { + super.initState(); + // Se passiamo un servizio esistente lo carichiamo, altrimenti ne creiamo uno "vuoto" + currentService = + widget.initialService ?? + ServiceModel( + storeId: 'ID_NEGOZIO_QUI', // Poi lo prenderai dal profilo utente + number: '', + energyServices: const [], + finServices: const [], + entertainmentServices: const [], + ); + } + + // Metodo generico per aggiungere un servizio energia + void _addEnergy() { + setState(() { + final newList = + List.from(currentService.energyServices)..add( + EnergyServiceModel( + type: EnergyType.luce, // Default + expiration: DateTime.now().add(const Duration(days: 365)), + providerId: '', // Lo sceglierà l'utente + ), + ); + currentService = currentService.copyWith(energyServices: newList); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text( + widget.initialService == null ? "Nuova Pratica" : "Modifica", + ), + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + // --- SEZIONE DATI GENERALI --- + TextField( + decoration: const InputDecoration(labelText: "Numero Pratica"), + onChanged: (v) => + currentService = currentService.copyWith(number: v), + ), + + const Divider(height: 32), + + // --- SEZIONE ENERGY --- + _SectionHeader( + title: "Energia (Luce/Gas)", + onAdd: _addEnergy, + icon: Icons.electric_bolt, + ), + ...currentService.energyServices.asMap().entries.map((entry) { + int idx = entry.key; + var item = entry.value; + return Card( + child: ListTile( + title: Text( + "${item.type.name.toUpperCase()} - Scadenza: ${item.expiration.year}", + ), + trailing: IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + onPressed: () { + setState(() { + final newList = List.from( + currentService.energyServices, + )..removeAt(idx); + currentService = currentService.copyWith( + energyServices: newList, + ); + }); + }, + ), + ), + ); + }), + + const SizedBox(height: 40), + + // --- BOTTONE SALVA --- + ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(50), + ), + onPressed: () { + context.read().addService(currentService); + Navigator.pop(context); + }, + child: const Text("SALVA TUTTO"), + ), + ], + ), + ), + ); + } +} + +class _SectionHeader extends StatelessWidget { + final String title; + final VoidCallback onAdd; + final IconData icon; + + const _SectionHeader({ + required this.title, + required this.onAdd, + required this.icon, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Icon(icon, color: Colors.orange), + const SizedBox(width: 8), + Text( + title, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const Spacer(), + IconButton( + onPressed: onAdd, + icon: const Icon(Icons.add_circle, color: Colors.green, size: 30), + ), + ], + ); + } +} diff --git a/lib/features/services/ui/services_screen.dart b/lib/features/services/ui/services_screen.dart new file mode 100644 index 0000000..aea5315 --- /dev/null +++ b/lib/features/services/ui/services_screen.dart @@ -0,0 +1,197 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/features/services/blocs/services_cubit.dart'; +import 'package:flux/features/services/models/service_model.dart'; +import 'package:go_router/go_router.dart'; +// Importa i tuoi modelli e cubit + +class ServicesScreen extends StatefulWidget { + const ServicesScreen({super.key}); + + @override + State createState() => _ServicesScreenState(); +} + +class _ServicesScreenState extends State { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + // Agganciamo il listener per la paginazione (Scroll Infinito) + _scrollController.addListener(_onScroll); + } + + void _onScroll() { + if (_isBottom) { + context.read().loadServices(); + } + } + + bool get _isBottom { + if (!_scrollController.hasClients) return false; + final maxScroll = _scrollController.position.maxScrollExtent; + final currentScroll = _scrollController.offset; + // Carica quando mancano 200px alla fine + return currentScroll >= (maxScroll * 0.9); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text("Gestione Servizi"), + elevation: 0, + actions: [ + IconButton( + icon: const Icon(Icons.search), + onPressed: () { + // Qui potrai implementare una barra di ricerca + }, + ), + ], + ), + body: BlocBuilder( + builder: (context, state) { + // 1. Stato di caricamento iniziale + if (state.isLoading && state.allServices.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + // 2. Lista vuota + if (state.allServices.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text("Nessuna pratica trovata."), + const SizedBox(height: 10), + ElevatedButton( + onPressed: () => context.read().loadServices( + refresh: true, + ), + child: const Text("Riprova"), + ), + ], + ), + ); + } + + // 3. La Lista (con Pull-to-refresh) + return RefreshIndicator( + onRefresh: () => + context.read().loadServices(refresh: true), + child: ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.only(bottom: 80), // Spazio per il FAB + itemCount: state.hasReachedMax + ? state.allServices.length + : state.allServices.length + 1, + itemBuilder: (context, index) { + if (index >= state.allServices.length) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(strokeWidth: 2), + ), + ); + } + + final service = state.allServices[index]; + return _buildServiceCard(context, service); + }, + ), + ); + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () => context.pushNamed('service-form'), // GoRouter + child: const Icon(Icons.add), + ), + ); + } + + Widget _buildServiceCard(BuildContext context, ServiceModel service) { + return Card( + margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + elevation: 2, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: ListTile( + contentPadding: const EdgeInsets.all(12), + title: Row( + children: [ + Expanded( + child: Text( + service.customerDisplayName ?? "Cliente sconosciuto", + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + ), + ), + if (service.isBozza) + const Chip( + label: Text( + "BOZZA", + style: TextStyle(fontSize: 10, color: Colors.white), + ), + backgroundColor: Colors.orange, + visualDensity: VisualDensity.compact, + ), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + "Pratica: ${service.number} • ${service.createdAt?.day}/${service.createdAt?.month}/${service.createdAt?.year}", + ), + const SizedBox(height: 8), + // I nostri mini-chip per i servizi attivati + Wrap( + spacing: 6, + children: [ + if (service.al > 0 || service.mnp > 0) + _miniBadge("📞 Tel", Colors.blue), + if (service.energyServices.isNotEmpty) + _miniBadge("⚡ Energy", Colors.green), + if (service.finServices.isNotEmpty) + _miniBadge("💰 Fin", Colors.purple), + if (service.entertainmentServices.isNotEmpty) + _miniBadge("📺 Ent", Colors.red), + ], + ), + ], + ), + trailing: const Icon(Icons.chevron_right), + onTap: () => context.pushNamed('service-form', extra: service), + ), + ); + } + + Widget _miniBadge(String text, Color color) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(4), + border: Border.all(color: color.withValues(alpha: 0.5)), + ), + child: Text( + text, + style: TextStyle( + color: color, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index fa4d3c4..147acc4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -16,6 +18,8 @@ import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:flux/features/master_data/staff/data/staff_repository.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/data/store_repository.dart'; +import 'package:flux/features/services/blocs/services_cubit.dart'; +import 'package:flux/features/services/data/services_repository.dart'; import 'package:flux/features/settings/settings.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; @@ -59,6 +63,7 @@ Future setupLocator() async { getIt.registerLazySingleton(() => CustomerRepository()); getIt.registerLazySingleton(() => ProductRepository()); getIt.registerLazySingleton(() => StaffRepository()); + getIt.registerLazySingleton(() => ServicesRepository()); } class FluxApp extends StatefulWidget { @@ -95,6 +100,9 @@ class _FluxAppState extends State { create: (_) => StaffCubit(context.read())..loadAllStaff(), ), + BlocProvider( + create: (_) => ServicesCubit(context.read()), + ), ], child: BlocBuilder( builder: (context, state) { -- 2.43.0