Refactor service management: streamline service form, enhance state management, and improve loading logic
This commit is contained in:
@@ -7,8 +7,7 @@ import 'package:flux/features/customers/ui/customer_detail_screen.dart';
|
|||||||
import 'package:flux/features/home/ui/home_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/products/ui/products_screen.dart';
|
||||||
import 'package:flux/features/master_data/store/ui/create_store_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/service_form_screen.dart';
|
||||||
import 'package:flux/features/services/ui/service_form_screen.dart';
|
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
@@ -80,9 +79,7 @@ class AppRouter {
|
|||||||
path: '/service-form',
|
path: '/service-form',
|
||||||
name: 'service-form',
|
name: 'service-form',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// Recuperiamo il ServiceModel se passato come extra
|
return ServiceFormScreen();
|
||||||
final service = state.extra as ServiceModel?;
|
|
||||||
return ServiceFormScreen(initialService: service);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flux/core/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
import 'package:flux/features/home/ui/dashboard_action_card.dart';
|
import 'package:flux/features/home/ui/dashboard_action_card.dart';
|
||||||
|
import 'package:flux/features/services/utils/service_actions.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
class DashboardAdaptiveGrid extends StatelessWidget {
|
class DashboardAdaptiveGrid extends StatelessWidget {
|
||||||
@@ -36,7 +37,7 @@ class DashboardAdaptiveGrid extends StatelessWidget {
|
|||||||
label: 'Nuova Op',
|
label: 'Nuova Op',
|
||||||
icon: Icons.add_task,
|
icon: Icons.add_task,
|
||||||
color: context.accent,
|
color: context.accent,
|
||||||
onTap: () {},
|
onTap: () => startNewService(context),
|
||||||
),
|
),
|
||||||
DashboardActionCard(
|
DashboardActionCard(
|
||||||
label: 'Clienti',
|
label: 'Clienti',
|
||||||
|
|||||||
@@ -3,55 +3,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/core/blocs/session/session_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/data/services_repository.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';
|
||||||
import 'package:flux/features/services/models/service_model.dart';
|
import 'package:flux/features/services/models/service_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
part 'services_state.dart';
|
||||||
class ServicesState extends Equatable {
|
|
||||||
final List<ServiceModel> 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<ServiceModel>? 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<Object?> get props => [
|
|
||||||
allServices,
|
|
||||||
isLoading,
|
|
||||||
hasReachedMax,
|
|
||||||
errorMessage,
|
|
||||||
query,
|
|
||||||
dateRange,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
class ServicesCubit extends Cubit<ServicesState> {
|
class ServicesCubit extends Cubit<ServicesState> {
|
||||||
final ServicesRepository _repository = GetIt.I<ServicesRepository>();
|
final ServicesRepository _repository = GetIt.I<ServicesRepository>();
|
||||||
@@ -59,58 +16,178 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
|
|
||||||
ServicesCubit(this._sessionBloc) : super(const ServicesState());
|
ServicesCubit(this._sessionBloc) : super(const ServicesState());
|
||||||
|
|
||||||
// Carica tutto il pacchetto
|
// --- CARICAMENTO E PAGINAZIONE ---
|
||||||
|
|
||||||
Future<void> loadServices({bool refresh = false}) async {
|
Future<void> loadServices({bool refresh = false}) async {
|
||||||
// Se non è un refresh e abbiamo già dati, non disturbare Supabase
|
// Se stiamo già caricando, evitiamo chiamate doppie
|
||||||
if (!refresh && state.allServices.isNotEmpty) return;
|
|
||||||
if (state.isLoading) return;
|
if (state.isLoading) return;
|
||||||
|
|
||||||
// Se facciamo refresh, resettiamo tutto
|
// Se non è un refresh e abbiamo già raggiunto la fine dei dati, ci fermiamo
|
||||||
final currentOffset = refresh ? 0 : state.allServices.length;
|
if (!refresh && state.hasReachedMax) return;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
|
errorMessage: null,
|
||||||
|
// Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading
|
||||||
allServices: refresh ? [] : state.allServices,
|
allServices: refresh ? [] : state.allServices,
|
||||||
hasReachedMax: refresh ? false : state.hasReachedMax,
|
hasReachedMax: refresh ? false : state.hasReachedMax,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
final currentOffset = refresh ? 0 : state.allServices.length;
|
||||||
|
final companyId = _sessionBloc.state.company?.id;
|
||||||
|
|
||||||
|
if (companyId == null) {
|
||||||
|
throw Exception("Company ID non trovato nella sessione");
|
||||||
|
}
|
||||||
|
|
||||||
final newServices = await _repository.fetchServices(
|
final newServices = await _repository.fetchServices(
|
||||||
companyId: _sessionBloc.state.company!.id,
|
companyId: companyId,
|
||||||
offset: currentOffset,
|
offset: currentOffset,
|
||||||
|
limit: 50,
|
||||||
searchTerm: state.query,
|
searchTerm: state.query,
|
||||||
dateRange: state.dateRange,
|
dateRange: state.dateRange,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Se ricevi meno record del limite, significa che non ce ne sono altri sul DB
|
||||||
|
final bool reachedMax = newServices.length < 50;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
allServices: List.from(state.allServices)..addAll(newServices),
|
allServices: refresh
|
||||||
hasReachedMax:
|
? newServices
|
||||||
newServices.length <
|
: [...state.allServices, ...newServices],
|
||||||
50, // Se ne arrivano meno di 50, siamo alla fine
|
hasReachedMax: reachedMax,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: "Errore nel caricamento servizi: $e",
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- GESTIONE FILTRI ---
|
||||||
|
|
||||||
|
/// Aggiorna i parametri di ricerca e ricarica da zero
|
||||||
void updateFilters({String? query, DateTimeRange? range}) {
|
void updateFilters({String? query, DateTimeRange? range}) {
|
||||||
emit(state.copyWith(query: query, dateRange: range));
|
emit(
|
||||||
loadServices(refresh: true); // Applica i filtri e riparte da zero
|
state.copyWith(
|
||||||
|
query: query ?? state.query,
|
||||||
|
dateRange: range ?? state.dateRange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
loadServices(refresh: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Salva e ricarica
|
/// Pulisce tutti i filtri
|
||||||
Future<void> addService(ServiceModel service) async {
|
void clearFilters() {
|
||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(query: '', dateRange: null));
|
||||||
|
loadServices(refresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTIONE BOZZA (DRAFT) ---
|
||||||
|
|
||||||
|
/// Inizializza un nuovo servizio o ne carica uno esistente per la modifica
|
||||||
|
void initServiceForm(ServiceModel? existingService) {
|
||||||
|
if (existingService != null) {
|
||||||
|
emit(state.copyWith(currentService: existingService));
|
||||||
|
} else {
|
||||||
|
// Crea un template vuoto con lo store di default (se disponibile)
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
currentService: ServiceModel(
|
||||||
|
storeId: _sessionBloc.state.selectedStore?.id ?? '',
|
||||||
|
number: '', // Sarà compilato dall'utente
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Metodo generico per aggiornare i campi base (AL, MNP, Note, ecc.)
|
||||||
|
void updateField({
|
||||||
|
int? al,
|
||||||
|
int? mnp,
|
||||||
|
int? nip,
|
||||||
|
int? unica,
|
||||||
|
int? telepass,
|
||||||
|
String? note,
|
||||||
|
String? number,
|
||||||
|
bool? isBozza,
|
||||||
|
bool? resultOk,
|
||||||
|
String? customerId,
|
||||||
|
}) {
|
||||||
|
if (state.currentService == null) return;
|
||||||
|
|
||||||
|
final updated = state.currentService!.copyWith(
|
||||||
|
al: al,
|
||||||
|
mnp: mnp,
|
||||||
|
nip: nip,
|
||||||
|
unica: unica,
|
||||||
|
telepass: telepass,
|
||||||
|
note: note,
|
||||||
|
number: number,
|
||||||
|
isBozza: isBozza,
|
||||||
|
resultOk: resultOk,
|
||||||
|
customerId: customerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(state.copyWith(currentService: updated));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTIONE MODULI COMPLESSI ---
|
||||||
|
|
||||||
|
void updateEnergyServices(List<EnergyServiceModel> energyList) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
currentService: state.currentService?.copyWith(
|
||||||
|
energyServices: energyList,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFinServices(List<FinServiceModel> finList) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
currentService: state.currentService?.copyWith(finServices: finList),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateEntertainmentServices(List<EntertainmentServiceModel> entList) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
currentService: state.currentService?.copyWith(
|
||||||
|
entertainmentServices: entList,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- PERSISTENZA ---
|
||||||
|
|
||||||
|
Future<void> saveCurrentService() async {
|
||||||
|
if (state.currentService == null) return;
|
||||||
|
|
||||||
|
emit(state.copyWith(isSaving: true, errorMessage: null));
|
||||||
try {
|
try {
|
||||||
await _repository.saveFullService(service);
|
// Usiamo il repository corazzato che abbiamo scritto prima
|
||||||
await loadServices(); // Ricarichiamo la lista aggiornata
|
await _repository.saveFullService(state.currentService!);
|
||||||
|
|
||||||
|
// Reset della bozza e ricaricamento lista
|
||||||
|
emit(state.copyWith(isSaving: false, currentService: null));
|
||||||
|
await loadServices(refresh: true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
|
emit(state.copyWith(isSaving: false, errorMessage: e.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
57
lib/features/services/blocs/services_state.dart
Normal file
57
lib/features/services/blocs/services_state.dart
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
part of 'services_cubit.dart';
|
||||||
|
|
||||||
|
class ServicesState extends Equatable {
|
||||||
|
final List<ServiceModel> allServices;
|
||||||
|
final ServiceModel? currentService; // La bozza che stiamo editando
|
||||||
|
final bool isLoading;
|
||||||
|
final bool isSaving; // Per mostrare il caricamento solo sul tasto salva
|
||||||
|
final String? errorMessage;
|
||||||
|
final String query;
|
||||||
|
final DateTimeRange? dateRange;
|
||||||
|
final bool hasReachedMax;
|
||||||
|
|
||||||
|
const ServicesState({
|
||||||
|
this.allServices = const [],
|
||||||
|
this.currentService,
|
||||||
|
this.isLoading = false,
|
||||||
|
this.isSaving = false,
|
||||||
|
this.errorMessage,
|
||||||
|
this.query = '',
|
||||||
|
this.dateRange,
|
||||||
|
this.hasReachedMax = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
ServicesState copyWith({
|
||||||
|
List<ServiceModel>? allServices,
|
||||||
|
ServiceModel? currentService,
|
||||||
|
bool? isLoading,
|
||||||
|
bool? isSaving,
|
||||||
|
String? errorMessage,
|
||||||
|
String? query,
|
||||||
|
DateTimeRange? dateRange,
|
||||||
|
bool? hasReachedMax,
|
||||||
|
}) {
|
||||||
|
return ServicesState(
|
||||||
|
allServices: allServices ?? this.allServices,
|
||||||
|
currentService: currentService ?? this.currentService,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
isSaving: isSaving ?? this.isSaving,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
query: query ?? this.query,
|
||||||
|
dateRange: dateRange ?? this.dateRange,
|
||||||
|
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
allServices,
|
||||||
|
currentService,
|
||||||
|
isLoading,
|
||||||
|
isSaving,
|
||||||
|
errorMessage,
|
||||||
|
query,
|
||||||
|
dateRange,
|
||||||
|
hasReachedMax,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -55,8 +55,7 @@ class ServicesRepository {
|
|||||||
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
|
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
|
||||||
Future<void> saveFullService(ServiceModel service) async {
|
Future<void> saveFullService(ServiceModel service) async {
|
||||||
try {
|
try {
|
||||||
// 1. Inseriamo il record principale
|
// 1. Upsert del record principale
|
||||||
// Se service.id è null, Supabase fa INSERT. Se c'è, fa UPDATE (grazie all'upsert o gestione manuale)
|
|
||||||
final serviceData = await _supabase
|
final serviceData = await _supabase
|
||||||
.from('service')
|
.from('service')
|
||||||
.upsert(service.toMap())
|
.upsert(service.toMap())
|
||||||
@@ -65,45 +64,65 @@ class ServicesRepository {
|
|||||||
|
|
||||||
final String newId = serviceData['id'];
|
final String newId = serviceData['id'];
|
||||||
|
|
||||||
// 2. Pulizia vecchi record figli (necessaria se è una MODIFICA)
|
// 2. MODIFICA: Pulizia atomica dei figli
|
||||||
// Se stiamo modificando, cancelliamo i vecchi per reinserire i nuovi (più semplice)
|
// Se stiamo modificando (id != null), resettiamo le tabelle collegate
|
||||||
if (service.id != null) {
|
if (service.id != null) {
|
||||||
await _supabase.from('energy_service').delete().eq('service_id', newId);
|
await Future.wait([
|
||||||
await _supabase.from('fin_service').delete().eq('service_id', newId);
|
_supabase.from('energy_service').delete().eq('service_id', newId),
|
||||||
await _supabase
|
_supabase.from('fin_service').delete().eq('service_id', newId),
|
||||||
|
_supabase
|
||||||
.from('entertainment_service')
|
.from('entertainment_service')
|
||||||
.delete()
|
.delete()
|
||||||
.eq('service_id', newId);
|
.eq('service_id', newId),
|
||||||
|
// Aggiungi qui eventuali altre tabelle pivot o file
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Inserimento EnergyServices
|
// 3. Inserimento dei moduli in parallelo per velocità
|
||||||
|
final List<Future> insertTasks = [];
|
||||||
|
|
||||||
if (service.energyServices.isNotEmpty) {
|
if (service.energyServices.isNotEmpty) {
|
||||||
final List<Map<String, dynamic>> toInsert = [];
|
insertTasks.add(
|
||||||
for (var item in service.energyServices) {
|
_supabase
|
||||||
toInsert.add(item.copyWith(serviceId: newId).toMap());
|
.from('energy_service')
|
||||||
}
|
.insert(
|
||||||
await _supabase.from('energy_service').insert(toInsert);
|
service.energyServices
|
||||||
|
.map((item) => item.copyWith(serviceId: newId).toMap())
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Inserimento FinServices
|
|
||||||
if (service.finServices.isNotEmpty) {
|
if (service.finServices.isNotEmpty) {
|
||||||
final List<Map<String, dynamic>> toInsert = [];
|
insertTasks.add(
|
||||||
for (var item in service.finServices) {
|
_supabase
|
||||||
toInsert.add(item.copyWith(serviceId: newId).toMap());
|
.from('fin_service')
|
||||||
}
|
.insert(
|
||||||
await _supabase.from('fin_service').insert(toInsert);
|
service.finServices
|
||||||
|
.map((item) => item.copyWith(serviceId: newId).toMap())
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Inserimento EntertainmentServices
|
|
||||||
if (service.entertainmentServices.isNotEmpty) {
|
if (service.entertainmentServices.isNotEmpty) {
|
||||||
final List<Map<String, dynamic>> toInsert = [];
|
insertTasks.add(
|
||||||
for (var item in service.entertainmentServices) {
|
_supabase
|
||||||
toInsert.add(item.copyWith(serviceId: newId).toMap());
|
.from('entertainment_service')
|
||||||
|
.insert(
|
||||||
|
service.entertainmentServices
|
||||||
|
.map((item) => item.copyWith(serviceId: newId).toMap())
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
await _supabase.from('entertainment_service').insert(toInsert);
|
|
||||||
|
if (insertTasks.isNotEmpty) {
|
||||||
|
await Future.wait(insertTasks);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Errore durante il salvataggio: $e');
|
// Qui potresti aggiungere una logica di "rollback manuale" se necessario
|
||||||
|
throw Exception('Errore durante il salvataggio corazzato: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flux/features/services/models/entertainment_service_model.dart';
|
||||||
|
import 'package:flux/features/services/models/fin_service_model.dart';
|
||||||
|
|
||||||
enum EnergyType { luce, gas } // Mappa il tuo public.energy_type
|
enum EnergyType { luce, gas } // Mappa il tuo public.energy_type
|
||||||
|
|
||||||
|
|||||||
@@ -115,12 +115,14 @@ class ServiceModel extends Equatable {
|
|||||||
|
|
||||||
factory ServiceModel.fromMap(Map<String, dynamic> map) {
|
factory ServiceModel.fromMap(Map<String, dynamic> map) {
|
||||||
return ServiceModel(
|
return ServiceModel(
|
||||||
id: map['id'],
|
id: map['id'].toString(),
|
||||||
createdAt: DateTime.parse(map['created_at']),
|
createdAt: map['created_at'] != null
|
||||||
storeId: map['store_id'],
|
? DateTime.parse(map['created_at'])
|
||||||
employeeId: map['employee_id'],
|
: DateTime.now(),
|
||||||
customerId: map['customer_id'],
|
storeId: map['store_id'] ?? '',
|
||||||
number: map['number'] ?? '',
|
employeeId: map['employee_id']?.toString(),
|
||||||
|
customerId: map['customer_id']?.toString(),
|
||||||
|
number: map['number']?.toString() ?? '',
|
||||||
isBozza: map['bozza'] ?? true,
|
isBozza: map['bozza'] ?? true,
|
||||||
note: map['note'] ?? '',
|
note: map['note'] ?? '',
|
||||||
resultOk: map['result_ok'] ?? true,
|
resultOk: map['result_ok'] ?? true,
|
||||||
@@ -130,7 +132,7 @@ class ServiceModel extends Equatable {
|
|||||||
unica: map['unica'] ?? 0,
|
unica: map['unica'] ?? 0,
|
||||||
telepass: map['telepass'] ?? 0,
|
telepass: map['telepass'] ?? 0,
|
||||||
|
|
||||||
// Mappaggio delle liste collegate (se incluse nella query)
|
// Estrazione sicura liste collegate
|
||||||
energyServices:
|
energyServices:
|
||||||
(map['energy_service'] as List?)
|
(map['energy_service'] as List?)
|
||||||
?.map((x) => EnergyServiceModel.fromMap(x))
|
?.map((x) => EnergyServiceModel.fromMap(x))
|
||||||
@@ -146,9 +148,12 @@ class ServiceModel extends Equatable {
|
|||||||
?.map((x) => EntertainmentServiceModel.fromMap(x))
|
?.map((x) => EntertainmentServiceModel.fromMap(x))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
const [],
|
const [],
|
||||||
|
|
||||||
|
// Display name del cliente con fallback
|
||||||
customerDisplayName: map['customer'] != null
|
customerDisplayName: map['customer'] != null
|
||||||
? "${map['customer']['name']} ${map['customer']['surname']}"
|
? "${map['customer']['name'] ?? ''} ${map['customer']['surname'] ?? ''}"
|
||||||
: "Cliente sconosciuto",
|
.trim()
|
||||||
|
: "Cliente non assegnato",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
76
lib/features/services/ui/service_action_card.dart
Normal file
76
lib/features/services/ui/service_action_card.dart
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ServiceActionCard extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final IconData icon;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
final Color color;
|
||||||
|
final int count;
|
||||||
|
const ServiceActionCard({
|
||||||
|
super.key,
|
||||||
|
required this.title,
|
||||||
|
required this.icon,
|
||||||
|
required this.onTap,
|
||||||
|
required this.color,
|
||||||
|
this.count = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final bool isActive = count > 0;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: isActive ? 4 : 1,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
side: BorderSide(
|
||||||
|
color: isActive ? color : Colors.transparent,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: Container(
|
||||||
|
width: 110, // Dimensione fissa per farle stare in una Row/Wrap
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
color: isActive ? color.withValues(alpha: 0.1) : Colors.transparent,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
color: isActive ? color : Colors.grey.shade400,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color: isActive ? color : Colors.grey.shade600,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isActive) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
CircleAvatar(
|
||||||
|
radius: 10,
|
||||||
|
backgroundColor: color,
|
||||||
|
child: Text(
|
||||||
|
count.toString(),
|
||||||
|
style: const TextStyle(fontSize: 10, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
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<ServiceFormScreen> createState() => _ServiceFormScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ServiceFormScreenState extends State<ServiceFormScreen> {
|
|
||||||
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<EnergyServiceModel>.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<EnergyServiceModel>.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<ServicesCubit>().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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
class GeneralInfoSection extends StatelessWidget {
|
||||||
|
final ServiceModel service;
|
||||||
|
const GeneralInfoSection({super.key, required this.service});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
"Info Generali",
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Numero di Riferimento / Telefono
|
||||||
|
TextFormField(
|
||||||
|
initialValue: service.number,
|
||||||
|
keyboardType: TextInputType
|
||||||
|
.phone, // Fa aprire il tastierino numerico su mobile
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Numero di Telefono / Riferimento",
|
||||||
|
hintText: "Es. 3331234567",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.phone),
|
||||||
|
),
|
||||||
|
onChanged: (val) {
|
||||||
|
context.read<ServicesCubit>().updateField(number: val);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// I due Switch affiancati (Bozza e A buon fine)
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: SwitchListTile(
|
||||||
|
title: const Text("Bozza"),
|
||||||
|
subtitle: const Text(
|
||||||
|
"Pratica in lavorazione",
|
||||||
|
style: TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
value: service.isBozza,
|
||||||
|
activeThumbColor: Colors.orange,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
onChanged: (val) {
|
||||||
|
context.read<ServicesCubit>().updateField(isBozza: val);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: SwitchListTile(
|
||||||
|
title: const Text("A buon fine"),
|
||||||
|
subtitle: const Text(
|
||||||
|
"Esito positivo",
|
||||||
|
style: TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
value: service.resultOk,
|
||||||
|
activeThumbColor: Colors.green,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
onChanged: (val) {
|
||||||
|
context.read<ServicesCubit>().updateField(resultOk: val);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Campo Note
|
||||||
|
TextFormField(
|
||||||
|
initialValue: service.note,
|
||||||
|
maxLines: 4,
|
||||||
|
minLines: 2,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Note Operazione",
|
||||||
|
hintText:
|
||||||
|
"Scrivi qui eventuali dettagli o richieste del cliente...",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
alignLabelWithHint: true,
|
||||||
|
),
|
||||||
|
onChanged: (val) {
|
||||||
|
context.read<ServicesCubit>().updateField(note: val);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
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/ui/service_form_screen/general_info_section.dart';
|
||||||
|
|
||||||
|
class ServiceFormScreen extends StatelessWidget {
|
||||||
|
const ServiceFormScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Nuova Pratica"),
|
||||||
|
actions: [
|
||||||
|
_SaveButton(), // Tasto salva intelligente
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: BlocBuilder<ServicesCubit, ServicesState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final service = state.currentService;
|
||||||
|
|
||||||
|
// Se la bozza non è ancora inizializzata, mostriamo un loader
|
||||||
|
if (service == null) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// SEZIONE 1: CLIENTE
|
||||||
|
const _CustomerSection(),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// SEZIONE 2: INFO GENERALI (Da fare)
|
||||||
|
GeneralInfoSection(service: service),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// SEZIONE 3: I MODULI (Da fare)
|
||||||
|
Text(
|
||||||
|
"Servizi e Accessori",
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// const _ServicesGrid(),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// SEZIONE 4: ALLEGATI (Da fare)
|
||||||
|
// const _AttachmentsSection(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- COMPONENTI DELLA PAGINA ---
|
||||||
|
|
||||||
|
class _SaveButton extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<ServicesCubit, ServicesState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.isSaving) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return IconButton(
|
||||||
|
icon: const Icon(Icons.save),
|
||||||
|
tooltip: "Salva Pratica",
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: Aggiungere una validazione prima di salvare!
|
||||||
|
context.read<ServicesCubit>().saveCurrentService();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomerSection extends StatelessWidget {
|
||||||
|
const _CustomerSection();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocBuilder<ServicesCubit, ServicesState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final service = state.currentService!;
|
||||||
|
final hasCustomer = service.customerId != null;
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.person,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
"Dati Cliente",
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Se non c'è il cliente, mostriamo il tastone per cercarlo
|
||||||
|
if (!hasCustomer)
|
||||||
|
Center(
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: Aprire modale/dialog per ricerca clienti
|
||||||
|
print("Apro ricerca clienti...");
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
label: const Text("Seleziona o Crea Cliente"),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
// Se c'è, mostriamo chi è e diamo la possibilità di cambiarlo
|
||||||
|
else
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
service.customerDisplayName ?? "Cliente Selezionato",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
// TODO: Aprire modale/dialog per ricerca clienti
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.edit, size: 18),
|
||||||
|
label: const Text("Cambia"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||||
import 'package:flux/features/services/models/service_model.dart';
|
import 'package:flux/features/services/models/service_model.dart';
|
||||||
|
import 'package:flux/features/services/utils/service_actions.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
// Importa i tuoi modelli e cubit
|
// Importa i tuoi modelli e cubit
|
||||||
|
|
||||||
@@ -111,7 +112,7 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton(
|
floatingActionButton: FloatingActionButton(
|
||||||
onPressed: () => context.pushNamed('service-form'), // GoRouter
|
onPressed: () => startNewService(context),
|
||||||
child: const Icon(Icons.add),
|
child: const Icon(Icons.add),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
81
lib/features/services/utils/service_actions.dart
Normal file
81
lib/features/services/utils/service_actions.dart
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
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/master_data/store/bloc/store_cubit.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';
|
||||||
|
|
||||||
|
/// Avvia la creazione di un nuovo servizio partendo dalla selezione dell'operatore.
|
||||||
|
void startNewService(BuildContext context) {
|
||||||
|
final session = context.read<SessionBloc>().state;
|
||||||
|
final currentStoreId = session.selectedStore?.id;
|
||||||
|
|
||||||
|
if (currentStoreId == null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(content: Text("Seleziona uno store prima di iniziare")),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||||
|
),
|
||||||
|
builder: (modalContext) {
|
||||||
|
// Usiamo lo StoreCubit invece dello StaffCubit!
|
||||||
|
return BlocBuilder<StoreCubit, StoreState>(
|
||||||
|
builder: (context, storeState) {
|
||||||
|
// Recuperiamo lo staff assegnato a questo specifico store usando la mappa che avevi già creato
|
||||||
|
final storeStaff = storeState.staffByStore[currentStoreId] ?? [];
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Chi sta eseguendo l'operazione?",
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
|
if (storeStaff.isEmpty)
|
||||||
|
const Text(
|
||||||
|
"Nessun membro dello staff configurato per questo store.\nVai in Anagrafica > Negozi per assegnare il personale.",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
|
||||||
|
...storeStaff.map(
|
||||||
|
(member) => ListTile(
|
||||||
|
leading: const CircleAvatar(child: Icon(Icons.person)),
|
||||||
|
title: Text(member.name),
|
||||||
|
onTap: () {
|
||||||
|
// 1. Inizializza il form nel Cubit
|
||||||
|
context.read<ServicesCubit>().initServiceForm(
|
||||||
|
ServiceModel(
|
||||||
|
storeId: currentStoreId,
|
||||||
|
employeeId: member.id,
|
||||||
|
number: '',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Chiudi la modal
|
||||||
|
Navigator.pop(modalContext);
|
||||||
|
|
||||||
|
// 3. Naviga verso il form
|
||||||
|
context.pushNamed('service-form');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user