Refactor service management: streamline service form, enhance state management, and improve loading logic

This commit is contained in:
2026-04-17 19:19:01 +02:00
parent 667bbf6404
commit a06be4bf7a
13 changed files with 715 additions and 261 deletions

View File

@@ -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/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:flux/features/services/ui/service_form_screen/service_form_screen.dart';
import 'package:go_router/go_router.dart';
import 'dart:async';
@@ -80,9 +79,7 @@ class AppRouter {
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);
return ServiceFormScreen();
},
),
],

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flux/core/theme/theme.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';
class DashboardAdaptiveGrid extends StatelessWidget {
@@ -36,7 +37,7 @@ class DashboardAdaptiveGrid extends StatelessWidget {
label: 'Nuova Op',
icon: Icons.add_task,
color: context.accent,
onTap: () {},
onTap: () => startNewService(context),
),
DashboardActionCard(
label: 'Clienti',

View File

@@ -3,55 +3,12 @@ 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/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:get_it/get_it.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,
];
}
part 'services_state.dart';
class ServicesCubit extends Cubit<ServicesState> {
final ServicesRepository _repository = GetIt.I<ServicesRepository>();
@@ -59,58 +16,178 @@ class ServicesCubit extends Cubit<ServicesState> {
ServicesCubit(this._sessionBloc) : super(const ServicesState());
// Carica tutto il pacchetto
// --- CARICAMENTO E PAGINAZIONE ---
Future<void> loadServices({bool refresh = false}) async {
// Se non è un refresh e abbiamo già dati, non disturbare Supabase
if (!refresh && state.allServices.isNotEmpty) return;
// Se stiamo già caricando, evitiamo chiamate doppie
if (state.isLoading) return;
// Se facciamo refresh, resettiamo tutto
final currentOffset = refresh ? 0 : state.allServices.length;
// Se non è un refresh e abbiamo già raggiunto la fine dei dati, ci fermiamo
if (!refresh && state.hasReachedMax) return;
emit(
state.copyWith(
isLoading: true,
errorMessage: null,
// Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading
allServices: refresh ? [] : state.allServices,
hasReachedMax: refresh ? false : state.hasReachedMax,
),
);
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(
companyId: _sessionBloc.state.company!.id,
companyId: companyId,
offset: currentOffset,
limit: 50,
searchTerm: state.query,
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(
state.copyWith(
isLoading: false,
allServices: List.from(state.allServices)..addAll(newServices),
hasReachedMax:
newServices.length <
50, // Se ne arrivano meno di 50, siamo alla fine
allServices: refresh
? newServices
: [...state.allServices, ...newServices],
hasReachedMax: reachedMax,
),
);
} 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}) {
emit(state.copyWith(query: query, dateRange: range));
loadServices(refresh: true); // Applica i filtri e riparte da zero
emit(
state.copyWith(
query: query ?? state.query,
dateRange: range ?? state.dateRange,
),
);
loadServices(refresh: true);
}
// Salva e ricarica
Future<void> addService(ServiceModel service) async {
emit(state.copyWith(isLoading: true));
/// Pulisce tutti i filtri
void clearFilters() {
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 {
await _repository.saveFullService(service);
await loadServices(); // Ricarichiamo la lista aggiornata
// Usiamo il repository corazzato che abbiamo scritto prima
await _repository.saveFullService(state.currentService!);
// Reset della bozza e ricaricamento lista
emit(state.copyWith(isSaving: false, currentService: null));
await loadServices(refresh: true);
} catch (e) {
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
emit(state.copyWith(isSaving: false, errorMessage: e.toString()));
}
}
}

View 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,
];
}

View File

@@ -55,8 +55,7 @@ class ServicesRepository {
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
Future<void> saveFullService(ServiceModel service) async {
try {
// 1. Inseriamo il record principale
// Se service.id è null, Supabase fa INSERT. Se c'è, fa UPDATE (grazie all'upsert o gestione manuale)
// 1. Upsert del record principale
final serviceData = await _supabase
.from('service')
.upsert(service.toMap())
@@ -65,45 +64,65 @@ class ServicesRepository {
final String newId = serviceData['id'];
// 2. Pulizia vecchi record figli (necessaria se è una MODIFICA)
// Se stiamo modificando, cancelliamo i vecchi per reinserire i nuovi (più semplice)
// 2. MODIFICA: Pulizia atomica dei figli
// Se stiamo modificando (id != null), resettiamo le tabelle collegate
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
await Future.wait([
_supabase.from('energy_service').delete().eq('service_id', newId),
_supabase.from('fin_service').delete().eq('service_id', newId),
_supabase
.from('entertainment_service')
.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) {
final List<Map<String, dynamic>> toInsert = [];
for (var item in service.energyServices) {
toInsert.add(item.copyWith(serviceId: newId).toMap());
}
await _supabase.from('energy_service').insert(toInsert);
insertTasks.add(
_supabase
.from('energy_service')
.insert(
service.energyServices
.map((item) => item.copyWith(serviceId: newId).toMap())
.toList(),
),
);
}
// 4. Inserimento FinServices
if (service.finServices.isNotEmpty) {
final List<Map<String, dynamic>> toInsert = [];
for (var item in service.finServices) {
toInsert.add(item.copyWith(serviceId: newId).toMap());
}
await _supabase.from('fin_service').insert(toInsert);
insertTasks.add(
_supabase
.from('fin_service')
.insert(
service.finServices
.map((item) => item.copyWith(serviceId: newId).toMap())
.toList(),
),
);
}
// 5. Inserimento EntertainmentServices
if (service.entertainmentServices.isNotEmpty) {
final List<Map<String, dynamic>> toInsert = [];
for (var item in service.entertainmentServices) {
toInsert.add(item.copyWith(serviceId: newId).toMap());
insertTasks.add(
_supabase
.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) {
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');
}
}

View File

@@ -1,4 +1,6 @@
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

View File

@@ -115,12 +115,14 @@ class ServiceModel extends Equatable {
factory ServiceModel.fromMap(Map<String, dynamic> 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'] ?? '',
id: map['id'].toString(),
createdAt: map['created_at'] != null
? DateTime.parse(map['created_at'])
: DateTime.now(),
storeId: map['store_id'] ?? '',
employeeId: map['employee_id']?.toString(),
customerId: map['customer_id']?.toString(),
number: map['number']?.toString() ?? '',
isBozza: map['bozza'] ?? true,
note: map['note'] ?? '',
resultOk: map['result_ok'] ?? true,
@@ -130,7 +132,7 @@ class ServiceModel extends Equatable {
unica: map['unica'] ?? 0,
telepass: map['telepass'] ?? 0,
// Mappaggio delle liste collegate (se incluse nella query)
// Estrazione sicura liste collegate
energyServices:
(map['energy_service'] as List?)
?.map((x) => EnergyServiceModel.fromMap(x))
@@ -146,9 +148,12 @@ class ServiceModel extends Equatable {
?.map((x) => EntertainmentServiceModel.fromMap(x))
.toList() ??
const [],
// Display name del cliente con fallback
customerDisplayName: map['customer'] != null
? "${map['customer']['name']} ${map['customer']['surname']}"
: "Cliente sconosciuto",
? "${map['customer']['name'] ?? ''} ${map['customer']['surname'] ?? ''}"
.trim()
: "Cliente non assegnato",
);
}

View 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),
),
),
],
],
),
),
),
);
}
}

View File

@@ -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),
),
],
);
}
}

View File

@@ -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);
},
),
],
),
),
);
}
}

View File

@@ -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"),
),
],
),
],
),
),
);
},
);
}
}

View File

@@ -2,6 +2,7 @@ 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:flux/features/services/utils/service_actions.dart';
import 'package:go_router/go_router.dart';
// Importa i tuoi modelli e cubit
@@ -111,7 +112,7 @@ class _ServicesScreenState extends State<ServicesScreen> {
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.pushNamed('service-form'), // GoRouter
onPressed: () => startNewService(context),
child: const Icon(Icons.add),
),
);

View 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),
],
),
);
},
);
},
);
}