added singleUserMode and removed StaffSection from forms

This commit is contained in:
2026-05-13 15:41:35 +02:00
parent efb82b0d4a
commit c610d68b9c
9 changed files with 138 additions and 84 deletions

View File

@@ -213,12 +213,23 @@ class AppRouter {
builder: (context, state) { builder: (context, state) {
// 1. Leggiamo l'ID dall'URL // 1. Leggiamo l'ID dall'URL
final String pathId = state.pathParameters['id'] ?? 'new'; final String pathId = state.pathParameters['id'] ?? 'new';
// 2. CAST DA NINJA (Aggiungi i punti interrogativi!)
final record = final record =
state.extra 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<CustomersCubit>().loadCustomers(); context.read<CustomersCubit>().loadCustomers();
context.read<ProductsCubit>().loadModels(); context.read<ProductsCubit>().loadModels();
context.read<ProductsCubit>().loadBrands(); context.read<ProductsCubit>().loadBrands();
@@ -231,12 +242,18 @@ class AppRouter {
parentId: realTicketId, 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( child: TicketFormScreen(
ticketId: realTicketId, ticketId: realTicketId,
existingTicket: record.ticket, existingTicket: record?.ticket,
), ),
); );
}, },
@@ -267,9 +284,21 @@ class AppRouter {
builder: (context, state) { builder: (context, state) {
final String pathId = state.pathParameters['id'] ?? 'new'; final String pathId = state.pathParameters['id'] ?? 'new';
final OperationModel? operationFromExtra = final record =
state.extra as OperationModel?; state.extra
final String? realOperationId = pathId == 'new' ? null : pathId; 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 final currentStoreId = GetIt.I
.get<SessionCubit>() .get<SessionCubit>()
.state .state
@@ -281,8 +310,6 @@ class AppRouter {
); );
context.read<ProductsCubit>().loadModels(); context.read<ProductsCubit>().loadModels();
context.read<ProductsCubit>().loadBrands(); context.read<ProductsCubit>().loadBrands();
context.read<StaffCubit>().loadStaffForStore(currentStoreId);
return MultiBlocProvider( return MultiBlocProvider(
providers: [ providers: [
BlocProvider( BlocProvider(
@@ -293,14 +320,14 @@ class AppRouter {
), ),
BlocProvider( BlocProvider(
create: (context) => OperationFormCubit( create: (context) => OperationFormCubit(
createdById: createdById, createdBy: record?.createdBy,
createdByName: createdByName, existingOperation: record?.operation,
), ),
), ),
], ],
child: OperationFormScreen( child: OperationFormScreen(
operationId: realOperationId, operationId: realOperationId,
existingOperation: operationFromExtra, existingOperation: record?.operation,
), ),
); );
}, },

View File

@@ -79,7 +79,10 @@ class StaffSelectorModal extends StatelessWidget {
); );
} }
Widget _buildStaffGrid(BuildContext context, List<dynamic> staffList) { Widget _buildStaffGrid(
BuildContext context,
List<StaffMemberModel> staffList,
) {
return Wrap( return Wrap(
spacing: 16, spacing: 16,
runSpacing: 16, runSpacing: 16,
@@ -98,7 +101,7 @@ class StaffSelectorModal extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of( color: Theme.of(
context, context,
).colorScheme.surfaceContainerHighest.withOpacity(0.3), ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
border: Border.all(color: Theme.of(context).dividerColor), border: Border.all(color: Theme.of(context).dividerColor),
), ),
@@ -109,7 +112,7 @@ class StaffSelectorModal extends StatelessWidget {
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Theme.of(context).colorScheme.onPrimary, foregroundColor: Theme.of(context).colorScheme.onPrimary,
child: Text( child: Text(
staff['name'].substring(0, 1).toUpperCase(), staff.name.substring(0, 1).toUpperCase(),
style: const TextStyle( style: const TextStyle(
fontSize: 24, fontSize: 24,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
@@ -118,7 +121,7 @@ class StaffSelectorModal extends StatelessWidget {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
staff['name'], staff.name,
style: const TextStyle(fontWeight: FontWeight.bold), style: const TextStyle(fontWeight: FontWeight.bold),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),

View File

@@ -188,11 +188,13 @@ class HomeScreen extends StatelessWidget {
icon: Icons.add, icon: Icons.add,
label: context.l10n.commonOperation, label: context.l10n.commonOperation,
color: Colors.blue, color: Colors.blue,
onTap: () { onTap: () async {
// Entriamo nel form! Nessun parametro extra = Nuovo Servizio StaffMemberModel? createdBy = await getStaffMember(context);
if (createdBy == null || !context.mounted) return;
context.pushNamed( context.pushNamed(
Routes.operationForm, Routes.operationForm,
pathParameters: {'id': 'new'}, pathParameters: {'id': 'new'},
extra: (createdBy: createdBy, operation: null),
); );
}, },
), ),
@@ -207,7 +209,7 @@ class HomeScreen extends StatelessWidget {
context.pushNamed( context.pushNamed(
Routes.ticketForm, Routes.ticketForm,
pathParameters: {'id': 'new'}, pathParameters: {'id': 'new'},
extra: createdBy, extra: (createdBy: createdBy, ticket: null),
); );
}, },
), ),

View File

@@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/customers/models/customer_model.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/data/operations_repository.dart';
import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/models/operation_model.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@@ -13,22 +14,20 @@ class OperationFormCubit extends Cubit<OperationFormState> {
final OperationsRepository _repository = GetIt.I<OperationsRepository>(); final OperationsRepository _repository = GetIt.I<OperationsRepository>();
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>(); final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
final Uuid _uuid = const Uuid(); final Uuid _uuid = const Uuid();
final String createdById;
final String createdByName;
OperationFormCubit({required this.createdById, required this.createdByName}) OperationFormCubit({
: super( StaffMemberModel? createdBy,
OperationFormState( OperationModel? existingOperation,
// Inizializziamo con un modello vuoto di sicurezza }) : super(
operation: OperationModel( OperationFormState(
storeId: '', operation:
companyId: '', existingOperation ??
reference: '', OperationModel.empty().copyWith(
status: OperationStatus.draft, staffId: createdBy?.id,
createdAt: DateTime.now(), staffDisplayName: createdBy?.name,
), ),
), ),
); );
Future<void> initForm({ Future<void> initForm({
OperationModel? existingOperation, OperationModel? existingOperation,
@@ -45,26 +44,37 @@ class OperationFormCubit extends Cubit<OperationFormState> {
), ),
); );
} else if (operationId != null) { } else if (operationId != null) {
// Avendo separato i cubit, se ci passano solo l'ID lo scarichiamo dal DB emit(state.copyWith(status: OperationFormStatus.loading));
final operation = await _repository.fetchOperationById(operationId); try {
emit( final operation = await _repository.fetchOperationById(operationId);
state.copyWith( emit(
operation: operation, state.copyWith(
status: OperationFormStatus.ready, operation: operation,
), status: OperationFormStatus.ready,
); ),
);
} on Exception catch (e) {
emit(
state.copyWith(
status: OperationFormStatus.failure,
errorMessage: e.toString(),
),
);
}
} else { } else {
// NUOVA PRATICA: Creiamo un nuovo Batch UUID // 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( emit(
state.copyWith( state.copyWith(
operation: OperationModel( operation: newOperation,
storeId: _sessionCubit.state.currentStore?.id ?? '',
reference: '',
createdAt: DateTime.now(),
companyId: _sessionCubit.state.company!.id!,
status: OperationStatus.draft,
batchUuid: _uuid.v4(),
),
status: OperationFormStatus.ready, status: OperationFormStatus.ready,
), ),
); );

View File

@@ -187,7 +187,9 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( 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 // Mettiamo un piccolo indicatore visivo anche nella AppBar se non è OK
actions: actions:
@@ -570,8 +572,8 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildStaffSection(state), /* _buildStaffSection(state),
const Divider(height: 50), const Divider(height: 50), */
_buildOperationStatusSection(state), _buildOperationStatusSection(state),
const Divider(height: 32), const Divider(height: 32),
_buildCustomerSection(state), _buildCustomerSection(state),

View File

@@ -1,5 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/blocs/operation_list_cubit.dart';
import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/models/operation_model.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -113,7 +116,15 @@ class _OperationListScreenState extends State<OperationListScreen> {
}, },
), ),
floatingActionButton: FloatingActionButton( 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), child: const Icon(Icons.add),
), ),
); );

View File

@@ -12,37 +12,36 @@ import 'ticket_form_state.dart';
class TicketFormCubit extends Cubit<TicketFormState> { class TicketFormCubit extends Cubit<TicketFormState> {
final TicketRepository _repository = GetIt.I.get<TicketRepository>(); final TicketRepository _repository = GetIt.I.get<TicketRepository>();
final SessionCubit _sessionCubit = GetIt.I.get<SessionCubit>(); final SessionCubit _sessionCubit = GetIt.I.get<SessionCubit>();
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( : super(
// Inizializziamo con un ticket vuoto di default
TicketFormState( TicketFormState(
ticket: TicketModel.empty().copyWith( // Se c'è un ticket esistente usa quello, ALTRIMENTI ne crea uno vuoto
createdById: createdBy?.id, // e ci stampa subito il nome del creatore!
createdByName: createdBy?.name, ticket:
), existingTicket ??
TicketModel.empty().copyWith(
createdById: createdBy?.id,
createdByName: createdBy?.name,
),
), ),
); );
/// 1. INIZIALIZZAZIONE (Se stiamo modificando un ticket esistente) /// 1. INIZIALIZZAZIONE
Future<void> initForm({String? id, TicketModel? existingTicket}) async { Future<void> initForm({String? id, TicketModel? existingTicket}) async {
if (existingTicket != null) { if (existingTicket != null) {
// SCENARIO 1 (App Native / Navigazione interna Web): // SCENARIO 1: Abbiamo il ticket intero passato via record
// Abbiamo l'oggetto intero passato via 'extra'. Lo mostriamo all'istante!
emit( emit(
state.copyWith(ticket: existingTicket, status: TicketFormStatus.ready), state.copyWith(ticket: existingTicket, status: TicketFormStatus.ready),
); );
} else if (id != null) { } else if (id != null) {
// SCENARIO 2 (Web Refresh o Link condiviso): // SCENARIO 2: QR CODE o Web Refresh! (Hai solo l'ID)
// L'utente ha premuto F5 su /tickets/form/123. L'extra è andato perso, ma abbiamo l'ID! emit(state.copyWith(status: TicketFormStatus.loading));
emit(
state.copyWith(status: TicketFormStatus.loading),
); // Mostriamo uno spinner
try { try {
final fetchedTicket = await _repository.getTicketById( // Boom! Lo scarica dal database in tempo reale
id, final fetchedTicket = await _repository.getTicketById(id);
); // Lo scarichiamo!
emit( emit(
state.copyWith(ticket: fetchedTicket, status: TicketFormStatus.ready), state.copyWith(ticket: fetchedTicket, status: TicketFormStatus.ready),
); );
@@ -55,19 +54,17 @@ class TicketFormCubit extends Cubit<TicketFormState> {
); );
} }
} else { } else {
// SCENARIO 3 (Nuovo Ticket): // SCENARIO 3: Nuovo Ticket
// È un nuovo ticket! Inseriamo i default base (Azienda, Negozio, Creatore)
final currentStore = _sessionCubit.state.currentStore; final currentStore = _sessionCubit.state.currentStore;
final companyId = _sessionCubit.state.company?.id ?? ''; 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, companyId: companyId,
storeId: currentStore?.id, storeId: currentStore?.id,
createdById: createdBy.id, ticketStatus: TicketStatus.open, // <-- O il tuo status di default
createdByName: _createdBy.name, ticketType: TicketType.repair,
// Impostiamo lo stato iniziale
ticketStatus: TicketStatus.open,
ticketType: TicketType.repair, // Default
); );
emit(state.copyWith(ticket: newTicket, status: TicketFormStatus.ready)); emit(state.copyWith(ticket: newTicket, status: TicketFormStatus.ready));

View File

@@ -297,7 +297,9 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text( 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: [ actions: [
BlocBuilder<TicketFormCubit, TicketFormState>( BlocBuilder<TicketFormCubit, TicketFormState>(
@@ -559,7 +561,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
icon: Icons.person, icon: Icons.person,
themeColor: Colors.indigo, themeColor: Colors.indigo,
children: [ children: [
StaffSection( /* StaffSection(
label: 'Creato Da', label: 'Creato Da',
staffId: ticket.createdById, staffId: ticket.createdById,
staffName: ticket.createdByName, staffName: ticket.createdByName,
@@ -567,7 +569,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
.read<TicketFormCubit>() .read<TicketFormCubit>()
.updateCreator(staffId: staff.id!, staffName: staff.name), .updateCreator(staffId: staff.id!, staffName: staff.name),
), ),
const Divider(height: 32), const Divider(height: 32), */
SharedCustomerSection( SharedCustomerSection(
customer: ticket.customer, customer: ticket.customer,
onCustomerSelected: (customer) => onCustomerSelected: (customer) =>

View File

@@ -160,7 +160,7 @@ class _TicketListScreenState extends State<TicketListScreen> {
context.pushNamed( context.pushNamed(
Routes.ticketForm, Routes.ticketForm,
pathParameters: {'id': 'new'}, pathParameters: {'id': 'new'},
extra: createdBy, extra: (createdBy: createdBy, ticket: null),
); );
}, },
), ),