diff --git a/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_bloc.dart b/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_bloc.dart index 87ee969..4aa3e22 100644 --- a/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_bloc.dart +++ b/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_bloc.dart @@ -53,6 +53,5 @@ class LatestStoreTicketsBloc ); } }); - // TODO: implement event handlers } } diff --git a/lib/features/master_data/staff/ui/staff_screen.dart b/lib/features/master_data/staff/ui/staff_screen.dart index 982db85..5511e1a 100644 --- a/lib/features/master_data/staff/ui/staff_screen.dart +++ b/lib/features/master_data/staff/ui/staff_screen.dart @@ -321,8 +321,9 @@ class _StaffScreenState extends State { ); }).toList(), onChanged: (val) { - if (val != null) + if (val != null) { setModalState(() => selectedRole = val); + } }, ), ), diff --git a/lib/features/tickets/blocs/ticket_form_cubit.dart b/lib/features/tickets/blocs/ticket_form_cubit.dart index f147935..8ad9af3 100644 --- a/lib/features/tickets/blocs/ticket_form_cubit.dart +++ b/lib/features/tickets/blocs/ticket_form_cubit.dart @@ -138,6 +138,8 @@ class TicketFormCubit extends Cubit { String? assignedToId, String? assignedToName, WarrantyType? warrantyType, + DateTime? estimatedDeliveryAt, + bool clearEstimatedDelivery = false, }) { emit( state.copyWith( @@ -162,6 +164,8 @@ class TicketFormCubit extends Cubit { assignedToId: assignedToId ?? state.ticket.assignedToId, assignedToName: assignedToName ?? state.ticket.assignedToName, warrantyType: warrantyType ?? state.ticket.warrantyType, + estimatedDeliveryAt: estimatedDeliveryAt, + clearEstimatedDelivery: clearEstimatedDelivery, ), ), ); diff --git a/lib/features/tickets/models/ticket_model.dart b/lib/features/tickets/models/ticket_model.dart index 27f468a..76ec24b 100644 --- a/lib/features/tickets/models/ticket_model.dart +++ b/lib/features/tickets/models/ticket_model.dart @@ -203,6 +203,7 @@ class TicketModel extends Equatable { TicketType? ticketType, TicketStatus? ticketStatus, DateTime? estimatedDeliveryAt, + bool clearEstimatedDelivery = false, TicketResult? ticketResult, String? resolutionNotes, String? shippingDocumentId, @@ -242,7 +243,9 @@ class TicketModel extends Equatable { hasCourtesyDevice: hasCourtesyDevice ?? this.hasCourtesyDevice, ticketType: ticketType ?? this.ticketType, ticketStatus: ticketStatus ?? this.ticketStatus, - estimatedDeliveryAt: estimatedDeliveryAt ?? this.estimatedDeliveryAt, + estimatedDeliveryAt: clearEstimatedDelivery + ? null + : (estimatedDeliveryAt ?? this.estimatedDeliveryAt), ticketResult: ticketResult ?? this.ticketResult, resolutionNotes: resolutionNotes ?? this.resolutionNotes, shippingDocumentId: shippingDocumentId ?? this.shippingDocumentId, diff --git a/lib/features/tickets/ui/ticket_form_screen.dart b/lib/features/tickets/ui/ticket_form_screen.dart index d3ac948..7f10348 100644 --- a/lib/features/tickets/ui/ticket_form_screen.dart +++ b/lib/features/tickets/ui/ticket_form_screen.dart @@ -141,6 +141,55 @@ class _TicketFormScreenState extends State { } } + // Formatta in "GG/MM/AAAA HH:MM" + String _formatDateTime(DateTime dt) { + return "${dt.day.toString().padLeft(2, '0')}/${dt.month.toString().padLeft(2, '0')}/${dt.year} ${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}"; + } + + // Lancia i popup di Data e poi di Ora + Future _selectDeliveryDate( + BuildContext context, + TicketModel ticket, + ) async { + final initialDate = ticket.estimatedDeliveryAt ?? DateTime.now(); + + // 1. Chiediamo la Data + final pickedDate = await showDatePicker( + context: context, + initialDate: initialDate, + firstDate: DateTime( + 2020, + ), // Oppure DateTime.now() se non vuoi date passate + lastDate: DateTime(2100), + ); + + if (pickedDate == null) return; // L'utente ha annullato + + // 2. Chiediamo l'Ora + if (!context.mounted) return; + final pickedTime = await showTimePicker( + context: context, + initialTime: TimeOfDay.fromDateTime(initialDate), + ); + + if (pickedTime == null) return; // L'utente ha annullato + + // 3. Fondiamo Data e Ora in un unico DateTime + final finalDateTime = DateTime( + pickedDate.year, + pickedDate.month, + pickedDate.day, + pickedTime.hour, + pickedTime.minute, + ); + + // 4. Aggiorniamo il Cubit + if (!context.mounted) return; + context.read().updateFields( + estimatedDeliveryAt: finalDateTime, + ); + } + Future _generateIdForQr() async { // 1. Validiamo i campi obbligatori (es. il cliente) if (!_formKey.currentState!.validate()) return null; @@ -817,6 +866,37 @@ class _TicketFormScreenState extends State { ), ], ), + const SizedBox(height: 16), + TextFormField( + readOnly: true, // MAGIA: Impedisce l'apertura della tastiera + // Creiamo un controller "al volo" solo per mostrargli la stringa + controller: TextEditingController( + text: ticket.estimatedDeliveryAt != null + ? _formatDateTime(ticket.estimatedDeliveryAt!) + : '', + ), + decoration: InputDecoration( + labelText: 'Riconsegna prevista (Data e Ora)', + prefixIcon: const Icon(Icons.event_available), + // Bottone con la X per rimuovere la data se il cliente ti dice "fai con calma" + suffixIcon: ticket.estimatedDeliveryAt != null + ? IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + // NOTA: Dovrai assicurarti che il tuo Cubit gestisca il reset. + // O passi un flag come clearEstimatedDelivery: true, + // o gestisci il null se il tuo updateFields lo permette. + context.read().updateFields( + clearEstimatedDelivery: + true, // Esempio di flag da aggiungere nel Cubit + ); + }, + ) + : null, + ), + // Quando tappi il campo di testo, partono i calendari + onTap: () => _selectDeliveryDate(context, ticket), + ), if (ticket.ticketType == TicketType.repair) ...[ const SizedBox(height: 16), DropdownButtonFormField( diff --git a/lib/features/tickets/ui/widgets/ticket_list.dart b/lib/features/tickets/ui/widgets/ticket_list.dart index 9593ea3..50db9e6 100644 --- a/lib/features/tickets/ui/widgets/ticket_list.dart +++ b/lib/features/tickets/ui/widgets/ticket_list.dart @@ -117,68 +117,83 @@ class TicketList extends StatelessWidget { AnimatedPositioned( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, - bottom: state.selectedTickets.isNotEmpty - ? 90 - : -100, // Nasconde o mostra - left: 16, - right: 16, - child: Card( - elevation: 8, - color: Theme.of(context).colorScheme.inverseSurface, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16.0, - vertical: 8.0, - ), - // ECCO LA MAGIA: Wrap invece di Row! - child: Wrap( - alignment: WrapAlignment.spaceBetween, // Sostituisce lo Spacer! - crossAxisAlignment: WrapCrossAlignment.center, - spacing: 8.0, // Spazio orizzontale - runSpacing: 8.0, // Spazio verticale se va a capo - children: [ - // BLOCCO 1: Icona e Contatore - Row( - mainAxisSize: MainAxisSize - .min, // Fondamentale per non occupare tutto il Wrap - children: [ - IconButton( - icon: const Icon(Icons.close), - onPressed: () => - context.read().clearSelection(), - ), - Text( - '${state.selectedTickets.length} selezionati', - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, + bottom: state.selectedTickets.isNotEmpty ? 90 : -100, + // Mettiamo left e right a 0 per far occupare tutta la larghezza invisibile + left: 0, + right: 0, + child: Align( + alignment: Alignment.bottomCenter, + // 1. IL LIMITE MASSIMO: Su desktop non supererà mai i 600px, su mobile si restringe da solo + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Card( + elevation: 8, + color: Theme.of(context).colorScheme.inverseSurface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + 16, + ), // Qui possiamo giocare coi bordi + ), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + // 2. LA ROW PRINCIPALE: Spinge tutto ai due estremi del nostro "dock" + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // BLOCCO SINISTRO: Chiusura e Contatore + Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: const Icon(Icons.close), + color: Theme.of( + context, + ).colorScheme.onInverseSurface, + onPressed: () => context + .read() + .clearSelection(), + ), + const SizedBox(width: 8), + Text( + '${state.selectedTickets.length} selezionati', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: Theme.of( + context, + ).colorScheme.onInverseSurface, + ), + ), + ], ), - ), - ], - ), - // BLOCCO 2: I Bottoni (Un altro Wrap per farli andare a capo tra loro se serve!) - Wrap( - spacing: 8.0, - runSpacing: 8.0, - alignment: WrapAlignment.end, - children: [ - FilledButton.icon( - onPressed: () => _setStatusClosed(context), - icon: const Icon(Icons.approval), - label: const Text('Riconsegna'), - ), - FilledButton.icon( - onPressed: () => _showShippingModal(context), - icon: const Icon(Icons.local_shipping), - label: const Text('Spedisci'), - ), - ], + // BLOCCO DESTRO: Wrap confinato solo ai bottoni + Wrap( + spacing: 8.0, + runSpacing: 8.0, + alignment: WrapAlignment.end, + children: [ + IconButton.filled( + tooltip: 'Riconsegna', + onPressed: () => _setStatusClosed(context), + icon: const Icon(Icons.approval), + ), + IconButton.filled( + tooltip: 'Spedisci', + onPressed: () => _showShippingModal(context), + icon: const Icon(Icons.local_shipping), + ), + ], + ), + ], + ), ), - ], + ), ), ), ), diff --git a/pubspec.yaml b/pubspec.yaml index d85b64b..dfef7ee 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flux description: "Gestione attività negozio di telefonia" publish_to: 'none' -version: 1.0.14+14 +version: 1.0.15+15 environment: sdk: ^3.11.3