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/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/features/attachments/blocs/attachments_bloc.dart'; import 'package:flux/features/company/models/company_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/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/utils/ticket_pdf_service.dart'; import 'package:get_it/get_it.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 _serialCtrl = 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(); context.read().initForm( existingTicket: widget.existingTicket, id: widget.ticketId, ); } @override void dispose() { _altPhoneCtrl.dispose(); _serialCtrl.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 (_serialCtrl.text.isEmpty) _serialCtrl.text = model.targetSn ?? ''; 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: _serialCtrl.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({required bool keepAdding}) { if (_formKey.currentState!.validate()) { _flushControllersToCubit(); context.read().saveTicket(keepAdding: keepAdding); } } 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 shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24)), ), builder: (context) => Container( 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 pdf = await TicketPdfService().generateTicketReceipt( ticket, company, ); await Printing.layoutPdf(onLayout: (format) => pdf); }, ), if (company.labelFormat != LabelFormat.none) _ActionButton( icon: Icons.label, label: "Etichetta", onTap: () async { final pdf = await TicketPdfService().generateLabelPdf( ticket, company, ); await Printing.layoutPdf(onLayout: (format) => pdf); }, ), _ActionButton( icon: Icons.email, label: "Invia Email", onTap: ticket.customerEmail != null ? () => _sendEmail(ticket) : null, ), _ActionButton( icon: Icons.close, label: "Chiudi", color: Colors.blue[900], textColor: Colors.white, onTap: () => Navigator.of(context).pop(), // Torna alla lista ), ], ), ], ), ), ); } Future _sendEmail(TicketModel ticket) async { //TODO send ticket receipt via email } @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) { _showSuccessActions( context, state.ticket, GetIt.I.get().state.company!, ); } else if (state.status == TicketFormStatus.successAndAddAnother) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Scheda salvata! Inserisci la prossima.'), ), ); _altPhoneCtrl.clear(); _serialCtrl.clear(); _requestCtrl.clear(); _accessoriesCtrl.clear(); _publicNotesCtrl.clear(); _internalNotesCtrl.clear(); _priceCtrl.clear(); _costCtrl.clear(); _isInitialized = false; } 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 ? 'Nuova Scheda Assistenza' : 'Modifica Scheda', ), actions: [ 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: OutlinedButton( onPressed: state.status == TicketFormStatus.saving ? null : () => _saveTicket(keepAdding: true), child: const Text( 'Salva e Aggiungi Altro', textAlign: TextAlign.center, ), ), ), const SizedBox(width: 12), Expanded( flex: 1, child: ElevatedButton( onPressed: state.status == TicketFormStatus.saving ? null : () => _saveTicket(keepAdding: false), 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: [_cardAnagrafica(ticket), _cardDispositivo(ticket)], ), ), const SizedBox(width: 24), Expanded(child: Column(children: [_cardDettagli(ticket)])), const SizedBox(width: 24), Expanded( child: Column( children: [_cardCosti(ticket), _cardAssegnazione(ticket)], ), ), ], ); } else if (isDesktop) { // 2 COLONNE return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( children: [ _cardAnagrafica(ticket), _cardDispositivo(ticket), _cardAssegnazione(ticket), ], ), ), const SizedBox(width: 24), Expanded( child: Column( children: [_cardDettagli(ticket), _cardCosti(ticket)], ), ), ], ); } else { // 1 COLONNA (Mobile) return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _cardAnagrafica(ticket), _cardDispositivo(ticket), _cardDettagli(ticket), _cardCosti(ticket), _cardAssegnazione(ticket), ], ); } } // --- LE 5 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( customerId: ticket.customerId, customerName: ticket.customerName, onCustomerSelected: (customer) => context.read().updateCustomer(customer), ), const SizedBox(height: 16), TextFormField( controller: _altPhoneCtrl, decoration: const InputDecoration( labelText: 'Recapito Alternativo', prefixIcon: Icon(Icons.phone), ), ), ], ); } Widget _cardDispositivo(TicketModel ticket) { return _buildCard( title: 'Dispositivo', icon: Icons.devices, themeColor: Colors.deepOrange, children: [ SharedModelSection( label: 'Modello da Riparare', modelId: ticket.targetModelId, modelName: ticket.targetModelName, onModelSelected: (id, name) => context .read() .updateModel(modelId: id, modelName: name), ), const SizedBox(height: 16), TextFormField( controller: _serialCtrl, decoration: const InputDecoration( labelText: 'Seriale / IMEI', prefixIcon: Icon(Icons.qr_code), ), ), ], ); } Widget _cardDettagli(TicketModel ticket) { return _buildCard( title: 'Dettagli Riparazione', icon: Icons.build, themeColor: Colors.pink, children: [ Row( children: [ Expanded( child: DropdownButtonFormField( initialValue: ticket.ticketType, decoration: const InputDecoration( labelText: 'Tipo Lavorazione', ), items: TicketType.values .map((t) => DropdownMenuItem(value: t, child: Text(t.name))) .toList(), onChanged: (val) => context .read() .updateFields(ticketType: val), ), ), const SizedBox(width: 16), Expanded( child: DropdownButtonFormField( 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( 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.customerName ?? 'Nuovo Ticket', onGenerateIdForQr: _generateIdForQr, ), /* SharedAttachmentsSection( parentType: AttachmentParentType.ticket, parentId: 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), Text( title, 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]), ), ), ], ), ), ); } }