From c610d68b9c6ea1afbb0621ae980a3f4e64d2b114 Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Wed, 13 May 2026 15:41:35 +0200 Subject: [PATCH] added singleUserMode and removed StaffSection from forms --- lib/core/routes/app_router.dart | 51 +++++++++---- lib/core/widgets/staff_selector_modal.dart | 11 +-- lib/features/home/ui/home_screen.dart | 8 ++- .../blocs/operation_form_cubit.dart | 72 +++++++++++-------- .../operations/ui/operation_form_screen.dart | 8 ++- .../operations/ui/operation_list_screen.dart | 13 +++- .../tickets/blocs/ticket_form_cubit.dart | 49 ++++++------- .../tickets/ui/ticket_form_screen.dart | 8 ++- .../tickets/ui/ticket_list_screen.dart | 2 +- 9 files changed, 138 insertions(+), 84 deletions(-) diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 7bd2a95..d48893a 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -213,12 +213,23 @@ class AppRouter { builder: (context, state) { // 1. Leggiamo l'ID dall'URL final String pathId = state.pathParameters['id'] ?? 'new'; + + // 2. CAST DA NINJA (Aggiungi i punti interrogativi!) final record = state.extra - as ({StaffMemberModel createdBy, TicketModel ticket}); + as ({StaffMemberModel? createdBy, TicketModel? ticket})?; - final String? realTicketId = pathId == 'new' ? null : pathId; + // 3. LOGICA SOBRIA + final String? realTicketId; + if (pathId == 'new') { + realTicketId = null; + } else if (record?.ticket?.id != null) { + // <-- Parentesi TONDE per la condizione, GRAFFE per il blocco! + realTicketId = record!.ticket!.id; + } else { + realTicketId = pathId; + } context.read().loadCustomers(); context.read().loadModels(); context.read().loadBrands(); @@ -231,12 +242,18 @@ class AppRouter { parentId: realTicketId, ), ), - BlocProvider(create: (context) => TicketFormCubit()), + BlocProvider( + create: (context) => TicketFormCubit( + // Passiamo il creatore e l'eventuale ticket esistente presi dal Record! + createdBy: record?.createdBy, + existingTicket: record?.ticket, + ), + ), ], child: TicketFormScreen( ticketId: realTicketId, - existingTicket: record.ticket, + existingTicket: record?.ticket, ), ); }, @@ -267,9 +284,21 @@ class AppRouter { builder: (context, state) { final String pathId = state.pathParameters['id'] ?? 'new'; - final OperationModel? operationFromExtra = - state.extra as OperationModel?; - final String? realOperationId = pathId == 'new' ? null : pathId; + final record = + state.extra + as ({ + StaffMemberModel? createdBy, + OperationModel? operation, + })?; + + final String? realOperationId; + if (pathId == 'new') { + realOperationId = null; + } else if (record?.operation?.id != null) { + realOperationId = record!.operation!.id; + } else { + realOperationId = pathId; + } final currentStoreId = GetIt.I .get() .state @@ -281,8 +310,6 @@ class AppRouter { ); context.read().loadModels(); context.read().loadBrands(); - context.read().loadStaffForStore(currentStoreId); - return MultiBlocProvider( providers: [ BlocProvider( @@ -293,14 +320,14 @@ class AppRouter { ), BlocProvider( create: (context) => OperationFormCubit( - createdById: createdById, - createdByName: createdByName, + createdBy: record?.createdBy, + existingOperation: record?.operation, ), ), ], child: OperationFormScreen( operationId: realOperationId, - existingOperation: operationFromExtra, + existingOperation: record?.operation, ), ); }, diff --git a/lib/core/widgets/staff_selector_modal.dart b/lib/core/widgets/staff_selector_modal.dart index e77c72c..44e1b67 100644 --- a/lib/core/widgets/staff_selector_modal.dart +++ b/lib/core/widgets/staff_selector_modal.dart @@ -79,7 +79,10 @@ class StaffSelectorModal extends StatelessWidget { ); } - Widget _buildStaffGrid(BuildContext context, List staffList) { + Widget _buildStaffGrid( + BuildContext context, + List staffList, + ) { return Wrap( spacing: 16, runSpacing: 16, @@ -98,7 +101,7 @@ class StaffSelectorModal extends StatelessWidget { decoration: BoxDecoration( color: Theme.of( context, - ).colorScheme.surfaceContainerHighest.withOpacity(0.3), + ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3), borderRadius: BorderRadius.circular(16), border: Border.all(color: Theme.of(context).dividerColor), ), @@ -109,7 +112,7 @@ class StaffSelectorModal extends StatelessWidget { backgroundColor: Theme.of(context).colorScheme.primary, foregroundColor: Theme.of(context).colorScheme.onPrimary, child: Text( - staff['name'].substring(0, 1).toUpperCase(), + staff.name.substring(0, 1).toUpperCase(), style: const TextStyle( fontSize: 24, fontWeight: FontWeight.bold, @@ -118,7 +121,7 @@ class StaffSelectorModal extends StatelessWidget { ), const SizedBox(height: 12), Text( - staff['name'], + staff.name, style: const TextStyle(fontWeight: FontWeight.bold), overflow: TextOverflow.ellipsis, ), diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 2fdd2e9..887fea4 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -188,11 +188,13 @@ class HomeScreen extends StatelessWidget { icon: Icons.add, label: context.l10n.commonOperation, color: Colors.blue, - onTap: () { - // Entriamo nel form! Nessun parametro extra = Nuovo Servizio + onTap: () async { + StaffMemberModel? createdBy = await getStaffMember(context); + if (createdBy == null || !context.mounted) return; context.pushNamed( Routes.operationForm, pathParameters: {'id': 'new'}, + extra: (createdBy: createdBy, operation: null), ); }, ), @@ -207,7 +209,7 @@ class HomeScreen extends StatelessWidget { context.pushNamed( Routes.ticketForm, pathParameters: {'id': 'new'}, - extra: createdBy, + extra: (createdBy: createdBy, ticket: null), ); }, ), diff --git a/lib/features/operations/blocs/operation_form_cubit.dart b/lib/features/operations/blocs/operation_form_cubit.dart index e74ae3a..622460c 100644 --- a/lib/features/operations/blocs/operation_form_cubit.dart +++ b/lib/features/operations/blocs/operation_form_cubit.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; +import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:get_it/get_it.dart'; @@ -13,22 +14,20 @@ class OperationFormCubit extends Cubit { final OperationsRepository _repository = GetIt.I(); final SessionCubit _sessionCubit = GetIt.I(); final Uuid _uuid = const Uuid(); - final String createdById; - final String createdByName; - OperationFormCubit({required this.createdById, required this.createdByName}) - : super( - OperationFormState( - // Inizializziamo con un modello vuoto di sicurezza - operation: OperationModel( - storeId: '', - companyId: '', - reference: '', - status: OperationStatus.draft, - createdAt: DateTime.now(), - ), - ), - ); + OperationFormCubit({ + StaffMemberModel? createdBy, + OperationModel? existingOperation, + }) : super( + OperationFormState( + operation: + existingOperation ?? + OperationModel.empty().copyWith( + staffId: createdBy?.id, + staffDisplayName: createdBy?.name, + ), + ), + ); Future initForm({ OperationModel? existingOperation, @@ -45,26 +44,37 @@ class OperationFormCubit extends Cubit { ), ); } else if (operationId != null) { - // Avendo separato i cubit, se ci passano solo l'ID lo scarichiamo dal DB - final operation = await _repository.fetchOperationById(operationId); - emit( - state.copyWith( - operation: operation, - status: OperationFormStatus.ready, - ), - ); + emit(state.copyWith(status: OperationFormStatus.loading)); + try { + final operation = await _repository.fetchOperationById(operationId); + emit( + state.copyWith( + operation: operation, + status: OperationFormStatus.ready, + ), + ); + } on Exception catch (e) { + emit( + state.copyWith( + status: OperationFormStatus.failure, + errorMessage: e.toString(), + ), + ); + } } else { // NUOVA PRATICA: Creiamo un nuovo Batch UUID + final currentStore = _sessionCubit.state.currentStore; + final companyId = _sessionCubit.state.company?.id ?? ''; + final newOperation = state.operation.copyWith( + companyId: companyId, + storeId: currentStore?.id, + status: OperationStatus.success, + reference: '', + batchUuid: _uuid.v4(), + ); emit( state.copyWith( - operation: OperationModel( - storeId: _sessionCubit.state.currentStore?.id ?? '', - reference: '', - createdAt: DateTime.now(), - companyId: _sessionCubit.state.company!.id!, - status: OperationStatus.draft, - batchUuid: _uuid.v4(), - ), + operation: newOperation, status: OperationFormStatus.ready, ), ); diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index 3e69fe5..718e3c0 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -187,7 +187,9 @@ class _OperationFormScreenState extends State { return Scaffold( appBar: AppBar( title: Text( - state.operation.id == null ? 'Nuova Pratica' : 'Modifica Pratica', + state.operation.id == null + ? 'Nuova Pratica - Operatore: ${state.operation.staffDisplayName}' + : 'Modifica Pratica - Operatore: ${state.operation.staffDisplayName}', ), // Mettiamo un piccolo indicatore visivo anche nella AppBar se non è OK actions: @@ -570,8 +572,8 @@ class _OperationFormScreenState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _buildStaffSection(state), - const Divider(height: 50), + /* _buildStaffSection(state), + const Divider(height: 50), */ _buildOperationStatusSection(state), const Divider(height: 32), _buildCustomerSection(state), diff --git a/lib/features/operations/ui/operation_list_screen.dart b/lib/features/operations/ui/operation_list_screen.dart index a172987..e22d25c 100644 --- a/lib/features/operations/ui/operation_list_screen.dart +++ b/lib/features/operations/ui/operation_list_screen.dart @@ -1,5 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/routes/routes.dart'; +import 'package:flux/core/widgets/staff_selector_modal.dart'; +import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/operations/blocs/operation_list_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:go_router/go_router.dart'; @@ -113,7 +116,15 @@ class _OperationListScreenState extends State { }, ), floatingActionButton: FloatingActionButton( - onPressed: () => startNewOperation(context), + onPressed: () async { + StaffMemberModel? createdBy = await getStaffMember(context); + if (createdBy == null || !context.mounted) return; + context.pushNamed( + Routes.operationForm, + pathParameters: {'id': 'new'}, + extra: (createdBy: createdBy, operation: null), + ); + }, child: const Icon(Icons.add), ), ); diff --git a/lib/features/tickets/blocs/ticket_form_cubit.dart b/lib/features/tickets/blocs/ticket_form_cubit.dart index 1f735f6..773e900 100644 --- a/lib/features/tickets/blocs/ticket_form_cubit.dart +++ b/lib/features/tickets/blocs/ticket_form_cubit.dart @@ -12,37 +12,36 @@ import 'ticket_form_state.dart'; class TicketFormCubit extends Cubit { final TicketRepository _repository = GetIt.I.get(); final SessionCubit _sessionCubit = GetIt.I.get(); - final StaffMemberModel? _createdBy; - TicketFormCubit({StaffMemberModel? createdBy}) + // Costruttore: prepariamo subito il ticket base con i dati di chi lo crea + TicketFormCubit({StaffMemberModel? createdBy, TicketModel? existingTicket}) : super( - // Inizializziamo con un ticket vuoto di default TicketFormState( - ticket: TicketModel.empty().copyWith( - createdById: createdBy?.id, - createdByName: createdBy?.name, - ), + // Se c'è un ticket esistente usa quello, ALTRIMENTI ne crea uno vuoto + // e ci stampa subito il nome del creatore! + ticket: + existingTicket ?? + TicketModel.empty().copyWith( + createdById: createdBy?.id, + createdByName: createdBy?.name, + ), ), ); - /// 1. INIZIALIZZAZIONE (Se stiamo modificando un ticket esistente) + /// 1. INIZIALIZZAZIONE Future initForm({String? id, TicketModel? existingTicket}) async { if (existingTicket != null) { - // SCENARIO 1 (App Native / Navigazione interna Web): - // Abbiamo l'oggetto intero passato via 'extra'. Lo mostriamo all'istante! + // SCENARIO 1: Abbiamo il ticket intero passato via record emit( state.copyWith(ticket: existingTicket, status: TicketFormStatus.ready), ); } else if (id != null) { - // SCENARIO 2 (Web Refresh o Link condiviso): - // L'utente ha premuto F5 su /tickets/form/123. L'extra è andato perso, ma abbiamo l'ID! - emit( - state.copyWith(status: TicketFormStatus.loading), - ); // Mostriamo uno spinner + // SCENARIO 2: QR CODE o Web Refresh! (Hai solo l'ID) + emit(state.copyWith(status: TicketFormStatus.loading)); + try { - final fetchedTicket = await _repository.getTicketById( - id, - ); // Lo scarichiamo! + // Boom! Lo scarica dal database in tempo reale + final fetchedTicket = await _repository.getTicketById(id); emit( state.copyWith(ticket: fetchedTicket, status: TicketFormStatus.ready), ); @@ -55,19 +54,17 @@ class TicketFormCubit extends Cubit { ); } } else { - // SCENARIO 3 (Nuovo Ticket): - // È un nuovo ticket! Inseriamo i default base (Azienda, Negozio, Creatore) + // SCENARIO 3: Nuovo Ticket final currentStore = _sessionCubit.state.currentStore; final companyId = _sessionCubit.state.company?.id ?? ''; - final newTicket = TicketModel.empty().copyWith( + // IL TRUCCO È QUI: Usiamo `state.ticket` invece di `TicketModel.empty()`. + // `state.ticket` HA GIÀ i dati di 'createdBy' settati nel costruttore! + final newTicket = state.ticket.copyWith( companyId: companyId, storeId: currentStore?.id, - createdById: createdBy.id, - createdByName: _createdBy.name, - // Impostiamo lo stato iniziale - ticketStatus: TicketStatus.open, - ticketType: TicketType.repair, // Default + ticketStatus: TicketStatus.open, // <-- O il tuo status di default + ticketType: TicketType.repair, ); emit(state.copyWith(ticket: newTicket, status: TicketFormStatus.ready)); diff --git a/lib/features/tickets/ui/ticket_form_screen.dart b/lib/features/tickets/ui/ticket_form_screen.dart index af2fc06..a83e59d 100644 --- a/lib/features/tickets/ui/ticket_form_screen.dart +++ b/lib/features/tickets/ui/ticket_form_screen.dart @@ -297,7 +297,9 @@ class _TicketFormScreenState extends State { return Scaffold( appBar: AppBar( title: Text( - ticket.id == null ? 'Nuova Scheda Assistenza' : 'Modifica Scheda', + ticket.id == null + ? 'Nuovo Ticket - Operatore: ${state.ticket.createdByName}' + : 'Modifica Ticket - Operatore: ${state.ticket.createdByName}', ), actions: [ BlocBuilder( @@ -559,7 +561,7 @@ class _TicketFormScreenState extends State { icon: Icons.person, themeColor: Colors.indigo, children: [ - StaffSection( + /* StaffSection( label: 'Creato Da', staffId: ticket.createdById, staffName: ticket.createdByName, @@ -567,7 +569,7 @@ class _TicketFormScreenState extends State { .read() .updateCreator(staffId: staff.id!, staffName: staff.name), ), - const Divider(height: 32), + const Divider(height: 32), */ SharedCustomerSection( customer: ticket.customer, onCustomerSelected: (customer) => diff --git a/lib/features/tickets/ui/ticket_list_screen.dart b/lib/features/tickets/ui/ticket_list_screen.dart index b074c28..eeb2350 100644 --- a/lib/features/tickets/ui/ticket_list_screen.dart +++ b/lib/features/tickets/ui/ticket_list_screen.dart @@ -160,7 +160,7 @@ class _TicketListScreenState extends State { context.pushNamed( Routes.ticketForm, pathParameters: {'id': 'new'}, - extra: createdBy, + extra: (createdBy: createdBy, ticket: null), ); }, ),