import 'package:flutter/foundation.dart'; // Per kIsWeb import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/routes/routes.dart'; import 'package:flux/core/widgets/shared_forms/customer_section.dart'; import 'package:flux/core/widgets/shared_forms/model_section.dart'; import 'package:flux/core/widgets/shared_forms/shared_files_section.dart'; import 'package:flux/core/widgets/staff_selector_modal.dart'; import 'package:flux/features/attachments/blocs/attachments_bloc.dart'; import 'package:flux/features/company/models/company_model.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/tickets/blocs/ticket_form_cubit.dart'; import 'package:flux/features/tickets/blocs/ticket_form_state.dart'; import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart'; import 'package:flux/features/tickets/models/ticket_model.dart'; import 'package:flux/core/widgets/shared_forms/staff_section.dart'; import 'package:flux/features/tickets/models/ticket_status_extension.dart'; import 'package:flux/features/tickets/ui/ticket_timeline_section.dart'; import 'package:flux/features/tickets/utils/ticket_pdf_service.dart'; import 'package:flux/features/tracking/blocs/tracking_cubit.dart'; import 'package:flux/features/tracking/models/tracking_model.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:printing/printing.dart'; class TicketFormScreen extends StatefulWidget { final TicketModel? existingTicket; final String? ticketId; const TicketFormScreen({super.key, this.existingTicket, this.ticketId}); @override State createState() => _TicketFormScreenState(); } class _TicketFormScreenState extends State { final _formKey = GlobalKey(); final _altPhoneCtrl = TextEditingController(); final _targetSerialCtrl = TextEditingController(); final _targetPasswordCtrl = TextEditingController(); final _sourceSerialCtrl = TextEditingController(); final _sourcePasswordCtrl = TextEditingController(); final _requestCtrl = TextEditingController(); final _accessoriesCtrl = TextEditingController(); final _publicNotesCtrl = TextEditingController(); final _internalNotesCtrl = TextEditingController(); final _priceCtrl = TextEditingController(); final _costCtrl = TextEditingController(); bool _isInitialized = false; @override void initState() { super.initState(); // TRUCCO ANTI-RACE-CONDITION: // Se il ticket arriva già "pronto" (via extra), popoliamo i controller SUBITO, // senza aspettare il listener del BLoC che si perderebbe l'emissione sincrona. if (widget.existingTicket != null) { _syncTextControllers(widget.existingTicket!); } context.read().initForm( existingTicket: widget.existingTicket, id: widget.ticketId, ); } @override void dispose() { _altPhoneCtrl.dispose(); _targetSerialCtrl.dispose(); _targetPasswordCtrl.dispose(); _sourcePasswordCtrl.dispose(); _sourceSerialCtrl.dispose(); _requestCtrl.dispose(); _accessoriesCtrl.dispose(); _publicNotesCtrl.dispose(); _internalNotesCtrl.dispose(); _priceCtrl.dispose(); _costCtrl.dispose(); super.dispose(); } void _syncTextControllers(TicketModel model) { if (_altPhoneCtrl.text.isEmpty) { _altPhoneCtrl.text = model.alternativePhoneNumber ?? ''; } if (_targetSerialCtrl.text.isEmpty) { _targetSerialCtrl.text = model.targetSn ?? ''; } if (_targetPasswordCtrl.text.isEmpty) { _targetPasswordCtrl.text = model.targetPassword ?? ''; } if (_sourceSerialCtrl.text.isEmpty) { _sourceSerialCtrl.text = model.sourceSn ?? ''; } if (_sourcePasswordCtrl.text.isEmpty) { _sourcePasswordCtrl.text = model.sourcePassword ?? ''; } if (_requestCtrl.text.isEmpty) _requestCtrl.text = model.request; if (_accessoriesCtrl.text.isEmpty) { _accessoriesCtrl.text = model.includedAccessories ?? ''; } if (_publicNotesCtrl.text.isEmpty) { _publicNotesCtrl.text = model.publicNotes ?? ''; } if (_internalNotesCtrl.text.isEmpty) { _internalNotesCtrl.text = model.internalNotes ?? ''; } if (_priceCtrl.text.isEmpty && model.customerPrice > 0) { _priceCtrl.text = model.customerPrice.toString(); } if (_costCtrl.text.isEmpty && model.internalCost > 0) { _costCtrl.text = model.internalCost.toString(); } _isInitialized = true; } void _flushControllersToCubit() { context.read().updateFields( alternativePhoneNumber: _altPhoneCtrl.text, targetSn: _targetSerialCtrl.text, targetPassword: _targetPasswordCtrl.text, sourcePassword: _sourcePasswordCtrl.text, sourceSn: _sourceSerialCtrl.text, request: _requestCtrl.text, includedAccessories: _accessoriesCtrl.text, publicNotes: _publicNotesCtrl.text, internalNotes: _internalNotesCtrl.text, customerPrice: double.tryParse(_priceCtrl.text) ?? 0.0, internalCost: double.tryParse(_costCtrl.text) ?? 0.0, ); } void _saveTicket() { if (_formKey.currentState!.validate()) { _flushControllersToCubit(); context.read().saveTicket(); } } // 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; // 2. Sincronizziamo i testi scritti a mano nel Cubit _flushControllersToCubit(); final attachmentsBloc = context.read(); // 3. Facciamo il salvataggio silenzioso final newId = await context.read().saveTicketDraft(); if (newId != null && context.mounted) { // 4. IL TOCCO DI CLASSE: Diciamo all'AttachmentsBloc che ora la pratica ha un ID! // Questo farà partire l'upload automatico di eventuali file "in bozza" attachmentsBloc.add(ParentEntitySavedEvent(newId)); } return newId; } void _showSuccessActions( BuildContext context, TicketModel ticket, CompanyModel company, ) { showModalBottomSheet( context: context, isDismissible: false, // Costringiamo l'operatore a una scelta conscia isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (context) => SafeArea( child: SingleChildScrollView( padding: const EdgeInsets.all(24), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 40, height: 4, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(10), ), ), const SizedBox(height: 24), const Icon(Icons.check_circle, color: Colors.green, size: 64), const SizedBox(height: 16), Text( "Ticket Salvato!", style: Theme.of(context).textTheme.headlineSmall?.copyWith( fontWeight: FontWeight.bold, ), ), Text( "Rif: ${ticket.referenceId}", style: TextStyle(color: Colors.grey[600], fontSize: 18), ), const SizedBox(height: 32), // Griglia delle Azioni GridView.count( crossAxisCount: 2, shrinkWrap: true, mainAxisSpacing: 16, crossAxisSpacing: 16, childAspectRatio: 1.5, children: [ _ActionButton( icon: Icons.print, label: "Ricevuta A4", onTap: () async { final doc = await TicketPdfService() .generateTicketReceipt(ticket); final bytes = await doc.save(); final fileName = 'Ricevuta_${ticket.referenceId}.pdf'; await Printing.layoutPdf( onLayout: (format) async => bytes, name: fileName, ); /* if (kIsWeb || Platform.isMacOS) { // Forza il download/salvataggio senza passare per il print spooler await Printing.sharePdf( bytes: bytes, filename: fileName, ); } else { // Su Android/iOS continuiamo a usare la stampa diretta che funziona } */ }, ), if (company.labelFormat != LabelFormat.none) _ActionButton( icon: Icons.label, label: "Etichetta", onTap: () async { final doc = await TicketPdfService().generateLabelPdf( ticket, ); final bytes = await doc.save(); final fileName = 'Ricevuta_${ticket.referenceId}.pdf'; if (kIsWeb || Platform.isMacOS) { // Forza il download/salvataggio senza passare per il print spooler await Printing.sharePdf( bytes: bytes, filename: fileName, ); } else { // Su Android/iOS continuiamo a usare la stampa diretta che funziona await Printing.layoutPdf( onLayout: (format) async => bytes, name: fileName, ); } }, ), _ActionButton( icon: Icons.email, label: "Invia Email", onTap: ticket.customer!.email.isNotEmpty ? () {} : null, ), _ActionButton( icon: Icons.close, label: "Chiudi", color: Colors.blue[900], textColor: Colors.white, onTap: () { Navigator.of(context).pop(); Navigator.of(context).pop(); }, ), ], ), ], ), ), ), ); } void _navigateToWorkspace(String ticketId) async { final formCubit = context.read(); final trackingCubit = context.read(); _flushControllersToCubit(); await context.pushNamed( Routes.ticketWorkspace, // Assicurati di aver definito questo nome! pathParameters: {'id': ticketId}, extra: formCubit, // Passiamo l'intero Cubit come extra! ); if (!context.mounted) return; trackingCubit.loadTrackings(ticketId, TrackingParentType.ticket); } @override Widget build(BuildContext context) { final theme = Theme.of(context); return BlocConsumer( listenWhen: (previous, current) => previous.status != current.status, listener: (context, state) { if (state.status == TicketFormStatus.ready && !_isInitialized) { _syncTextControllers(state.ticket); } if (state.status == TicketFormStatus.success) { context.read().loadTickets(refresh: true); _showSuccessActions( context, state.ticket, GetIt.I.get().state.company!, ); } else if (state.status == TicketFormStatus.pop) { Navigator.of(context).pop(); } else if (state.status == TicketFormStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.errorMessage ?? 'Errore di salvataggio'), backgroundColor: theme.colorScheme.error, ), ); } }, builder: (context, state) { final ticket = state.ticket; return Scaffold( appBar: AppBar( title: Text( ticket.id == null ? 'Nuovo Ticket - Operatore: ${state.ticket.createdByName}' : 'Modifica Ticket - Operatore: ${state.ticket.createdByName}', ), actions: [ BlocBuilder( builder: (context, state) { final ticket = state.ticket; // Se il ticket non è ancora salvato, niente azioni rapide if (ticket.id == null || ticket.id!.isEmpty) { return const SizedBox.shrink(); } // CONDIZIONE A: Da iniziare if (ticket.ticketStatus == TicketStatus.open || ticket.ticketStatus == TicketStatus.waitingForParts) { return Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 8.0, ), child: FilledButton.icon( style: FilledButton.styleFrom( backgroundColor: Colors.amber.shade700, // Colore Action ), onPressed: () async { StaffMemberModel? takenBy = await getStaffMember( context, ); if (takenBy == null || !context.mounted) return; context.read().takeInCharge( staffId: takenBy.id!, staffName: takenBy.name, ); _navigateToWorkspace(ticket.id!); }, icon: const Icon(Icons.play_arrow, color: Colors.white), label: const Text( 'Prendi in Carico', style: TextStyle(color: Colors.white), ), ), ); } // CONDIZIONE B: Già in lavorazione else if (ticket.ticketStatus == TicketStatus.inProgress) { return Padding( padding: const EdgeInsets.symmetric( horizontal: 16.0, vertical: 8.0, ), child: FilledButton.icon( onPressed: () => _navigateToWorkspace(ticket.id!), icon: const Icon(Icons.handyman), label: const Text('Vai a Lavorazione'), ), ); } // Se è chiuso o in altri stati strani, nascondiamo il bottone return const SizedBox.shrink(); }, ), Padding( padding: const EdgeInsets.only(right: 16.0), child: Chip( label: Text( ticket.ticketStatus.name.toUpperCase(), style: const TextStyle(color: Colors.white, fontSize: 10), ), backgroundColor: ticket.ticketStatus.color, ), ), ], ), body: Form( key: _formKey, // IL TRUCCO PER LA TASTIERA: Obblighiamo il tab a seguire il DOM child: FocusTraversalGroup( policy: WidgetOrderTraversalPolicy(), child: LayoutBuilder( builder: (context, constraints) { final isUltraWide = constraints.maxWidth > 1400; final isDesktop = constraints.maxWidth > 900; return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Center( child: ConstrainedBox( constraints: BoxConstraints( maxWidth: isUltraWide ? 1600 : (isDesktop ? 1200 : 800), ), child: _buildResponsiveLayout( isUltraWide, isDesktop, ticket, ), ), ), ); }, ), ), ), bottomNavigationBar: SafeArea( child: Container( padding: const EdgeInsets.all(16.0), decoration: BoxDecoration( color: theme.scaffoldBackgroundColor, boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 10, offset: const Offset(0, -3), ), ], ), child: FocusTraversalGroup( // Un gruppo a parte per il footer, così viene visitato per ultimo child: Row( children: [ Expanded( flex: 1, child: ElevatedButton( onPressed: state.ticket.id == null ? null : () => _showSuccessActions( context, ticket, GetIt.I.get().state.company!, ), child: const Text('Ricevuta'), ), ), const SizedBox(width: 12), Expanded( flex: 1, child: ElevatedButton( onPressed: state.status == TicketFormStatus.saving ? null : () => _saveTicket(), child: state.status == TicketFormStatus.saving ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2, ), ) : const Text('Salva ed Esci'), ), ), ], ), ), ), ), ); }, ); } // --- LOGICA DI IMPAGINAZIONE RESPONSIVE --- Widget _buildResponsiveLayout( bool isUltraWide, bool isDesktop, TicketModel ticket, ) { if (isUltraWide) { // 3 COLONNE return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( children: [_cardTimeline(ticket), _cardAnagrafica(ticket)], ), ), const SizedBox(width: 24), Expanded( child: Column( children: [_cardDettagli(ticket), _cardCosti(ticket)], ), ), const SizedBox(width: 24), Expanded( child: Column( children: [_cardDispositivi(ticket), _cardAssegnazione(ticket)], ), ), ], ); } else if (isDesktop) { // 2 COLONNE return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( children: [ _cardAnagrafica(ticket), _cardDispositivi(ticket), _cardAssegnazione(ticket), ], ), ), const SizedBox(width: 24), Expanded( child: Column( children: [ _cardTimeline(ticket), _cardDettagli(ticket), _cardCosti(ticket), ], ), ), ], ); } else { // 1 COLONNA (Mobile) return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _cardTimeline(ticket), _cardAnagrafica(ticket), _cardDispositivi(ticket), _cardDettagli(ticket), _cardCosti(ticket), _cardAssegnazione(ticket), ], ); } } // --- LE 6 CARD (MODULARIZZATE E COLORATE) --- Widget _cardAnagrafica(TicketModel ticket) { return _buildCard( title: 'Anagrafica', icon: Icons.person, themeColor: Colors.indigo, children: [ /* StaffSection( label: 'Creato Da', staffId: ticket.createdById, staffName: ticket.createdByName, onStaffSelected: (staff) => context .read() .updateCreator(staffId: staff.id!, staffName: staff.name), ), const Divider(height: 32), */ SharedCustomerSection( customer: ticket.customer, onCustomerSelected: (customer) => context.read().updateCustomer(customer), ), const SizedBox(height: 16), TextFormField( controller: _altPhoneCtrl, decoration: const InputDecoration( labelText: 'Recapito Alternativo', prefixIcon: Icon(Icons.phone), ), ), ], ); } Widget _cardDispositivi(TicketModel ticket) { final bool isDataTransfer = ticket.ticketType == TicketType.dataTransfer; return _buildCard( title: isDataTransfer ? 'Dispositivi' : 'Dispositivo', icon: Icons.devices, themeColor: Colors.deepOrange, children: [ // --- DISPOSITIVO TARGET (Nuovo/Ricevente) --- SharedModelSection( label: isDataTransfer ? 'Dispositivo Target (Nuovo/Ricevente)' : 'Modello da Riparare', modelId: ticket.targetModelId, modelName: ticket.targetModelName, onModelSelected: (id, name) => context .read() .updateTargetModel(modelId: id, modelName: name), ), const SizedBox(height: 16), TextFormField( controller: _targetSerialCtrl, // Controller per il seriale TARGET decoration: const InputDecoration( labelText: 'Seriale / IMEI', prefixIcon: Icon(Icons.qr_code), ), ), const SizedBox(height: 16), TextFormField( controller: _targetPasswordCtrl, // Controller per il seriale TARGET decoration: const InputDecoration( labelText: 'PIN / Password', prefixIcon: Icon(Icons.qr_code), ), ), // --- DISPOSITIVO SORGENTE (Animato per Passaggio Dati) --- AnimatedSize( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, alignment: Alignment.topCenter, child: isDataTransfer ? Padding( padding: const EdgeInsets.only(top: 24.0), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( // Bordo trasparente e delicato per definire la card border: Border.all( color: Colors.orange.shade300.withValues(alpha: 0.2), ), borderRadius: BorderRadius.circular(8), // SFONDO RIMOSSO: vedi direttamente il tema scuro sotto! // color: Colors.transparent, // opzionale ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.devices_fold, color: Colors.orange.shade700, ), const SizedBox(width: 12), Text( 'Dispositivo Sorgente', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.orange.shade900, ), ), ], ), const SizedBox(height: 16), // LA SHARED SECTION "SOFT" SharedModelSection( label: 'Modello Sorgente (Da cui copiare)', modelId: ticket.sourceModelId, modelName: ticket.sourceModelName, // Sfondo quasi trasparente per non appesantire backgroundColor: Colors.white.withValues(alpha: 0.1), // Bordo delicato borderColor: Colors.orange.shade300.withValues( alpha: 0.2, ), onModelSelected: (id, name) => context .read() .updateSourceModel(modelId: id, modelName: name), ), const SizedBox(height: 16), TextFormField( controller: _sourceSerialCtrl, decoration: InputDecoration( labelText: 'Seriale / IMEI Sorgente', prefixIcon: Icon( Icons.qr_code, color: Colors.orange.shade700.withValues( alpha: 0.7, ), ), // Usiamo lo stesso riempimento tenue per coerenza fillColor: Colors.white.withValues(alpha: 0.1), filled: true, enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.orange.shade300.withValues( alpha: 0.2, ), ), borderRadius: BorderRadius.circular(8), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.orange.shade500.withValues( alpha: 0.5, ), ), borderRadius: BorderRadius.circular(8), ), ), ), const SizedBox(height: 16), TextFormField( controller: _sourcePasswordCtrl, decoration: InputDecoration( labelText: 'PIN / Password Sorgente', prefixIcon: Icon( Icons.qr_code, color: Colors.orange.shade700.withValues( alpha: 0.7, ), ), // Usiamo lo stesso riempimento tenue per coerenza fillColor: Colors.white.withValues(alpha: 0.1), filled: true, enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.orange.shade300.withValues( alpha: 0.2, ), ), borderRadius: BorderRadius.circular(8), ), focusedBorder: OutlineInputBorder( borderSide: BorderSide( color: Colors.orange.shade500.withValues( alpha: 0.5, ), ), borderRadius: BorderRadius.circular(8), ), ), ), ], ), ), ) : const SizedBox.shrink(), ), ], ); } Widget _cardDettagli(TicketModel ticket) { return _buildCard( title: 'Dettagli Riparazione', icon: Icons.build, themeColor: Colors.pink, children: [ Row( children: [ Expanded( child: DropdownButtonFormField( isExpanded: true, initialValue: ticket.ticketType, decoration: const InputDecoration( labelText: 'Tipo Lavorazione', ), items: TicketType.values .map( (t) => DropdownMenuItem( value: t, child: Text(t.displayValue), ), ) .toList(), onChanged: (val) { context.read().updateFields(ticketType: val); }, ), ), const SizedBox(width: 16), Expanded( child: DropdownButtonFormField( isExpanded: true, initialValue: ticket.ticketStatus, decoration: const InputDecoration(labelText: 'Stato Attuale'), items: TicketStatus.values .map((s) => DropdownMenuItem(value: s, child: Text(s.name))) .toList(), onChanged: (val) => context.read().updateFields(status: val), ), ), ], ), 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( initialValue: ticket .warrantyType, // Assicurati di avere questo campo nel TicketModel decoration: const InputDecoration( labelText: 'Tipo Garanzia', prefixIcon: Icon(Icons.verified_user_outlined), ), items: WarrantyType.values .map( (w) => DropdownMenuItem(value: w, child: Text(w.displayValue)), ) .toList(), onChanged: (val) { context.read().updateFields(warrantyType: val); }, ), ], const SizedBox(height: 16), TextFormField( controller: _requestCtrl, maxLines: 4, decoration: const InputDecoration( labelText: 'Difetto dichiarato o Richiesta del cliente', alignLabelWithHint: true, ), ), const SizedBox(height: 16), TextFormField( controller: _accessoriesCtrl, decoration: const InputDecoration( labelText: 'Accessori Consegnati', prefixIcon: Icon(Icons.cable), ), ), const SizedBox(height: 16), SwitchListTile( title: const Text('Prestato Telefono di Cortesia?'), value: ticket.hasCourtesyDevice, onChanged: (val) => context.read().updateFields( hasCourtesyDevice: val, ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), side: BorderSide(color: Theme.of(context).dividerColor), ), ), ], ); } Widget _cardCosti(TicketModel ticket) { return _buildCard( title: 'Costi & Note', icon: Icons.euro, themeColor: Colors.teal, children: [ Row( children: [ Expanded( child: TextFormField( controller: _priceCtrl, keyboardType: const TextInputType.numberWithOptions( decimal: true, ), decoration: const InputDecoration( labelText: 'Preventivo Cliente (€)', prefixIcon: Icon(Icons.sell_outlined), ), ), ), const SizedBox(width: 16), Expanded( child: TextFormField( controller: _costCtrl, keyboardType: const TextInputType.numberWithOptions( decimal: true, ), decoration: const InputDecoration( labelText: 'Nostro Costo (€)', prefixIcon: Icon(Icons.shopping_cart_outlined), ), ), ), ], ), const SizedBox(height: 16), TextFormField( controller: _publicNotesCtrl, maxLines: 2, decoration: const InputDecoration( labelText: 'Note Pubbliche (Visibili su ricevuta)', ), ), const SizedBox(height: 16), TextFormField( controller: _internalNotesCtrl, maxLines: 3, decoration: InputDecoration( labelText: 'Note Interne (Solo per lo Staff)', fillColor: Colors.amber.withValues(alpha: 0.1), filled: true, ), ), ], ); } Widget _cardAssegnazione(TicketModel ticket) { return _buildCard( title: 'Assegnazione e Allegati', icon: Icons.engineering, themeColor: Colors.deepPurple, children: [ StaffSection( label: 'Assegnato A', staffId: ticket.assignedToId, staffName: ticket.assignedToName, onStaffSelected: (staff) => context .read() .updateFields(assignedToId: staff.id, assignedToName: staff.name), ), const Divider(height: 32), // ECCO LA MAGIA: SharedFilesSection( titleNameForUpload: ticket.customer?.name ?? 'Nuovo Ticket', onGenerateIdForQr: _generateIdForQr, ), /* SharedAttachmentsSection( parentType: AttachmentParentType.ticket, parentId: ticket.id, ), */ ], ); } Widget _cardTimeline(TicketModel ticket) { // Se il ticket è nuovo (non ha ancora un ID salvato a DB), nascondiamo la timeline if (ticket.id == null || ticket.id!.isEmpty) { return const SizedBox.shrink(); } return _buildCard( title: 'Timeline & Note', icon: Icons.history, themeColor: Colors.blueGrey, children: [TicketTimelineSection(ticketId: ticket.id!)], ); } // --- WIDGET BASE PER LA CARD --- Widget _buildCard({ required String title, required IconData icon, required Color themeColor, required List children, }) { return Card( margin: const EdgeInsets.only(bottom: 24), elevation: 0, // Tolta l'ombra standard shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), side: BorderSide( color: themeColor.withValues(alpha: 0.3), width: 1, ), // Bordo colorato delicato ), child: Padding( padding: const EdgeInsets.all(20.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ // Pallino colorato con l'icona dentro Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: themeColor.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: themeColor), ), const SizedBox(width: 12), Expanded( child: Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleLarge?.copyWith( fontWeight: FontWeight.bold, color: themeColor, ), ), ), ], ), const Divider(height: 32), ...children, ], ), ), ); } } // Widget helper per i bottoni dell'Action Hub class _ActionButton extends StatelessWidget { final IconData icon; final String label; final VoidCallback? onTap; final Color? color; final Color? textColor; const _ActionButton({ required this.icon, required this.label, this.onTap, this.color, this.textColor, }); @override Widget build(BuildContext context) { return InkWell( onTap: onTap, borderRadius: BorderRadius.circular(16), child: Container( decoration: BoxDecoration( color: onTap == null ? Colors.grey[100] : (color ?? Colors.grey[200]), borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( icon, color: onTap == null ? Colors.grey : (textColor ?? Colors.blue[900]), ), const SizedBox(height: 8), Text( label, style: TextStyle( fontWeight: FontWeight.bold, color: onTap == null ? Colors.grey : (textColor ?? Colors.blue[900]), ), ), ], ), ), ); } }