diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 7d8c380..254e8b8 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -190,7 +190,7 @@ class HomeScreen extends StatelessWidget { // Entriamo nel form! Nessun parametro extra = Nuovo Servizio context.pushNamed( Routes.operationForm, - pathParameters: {'id': 'New'}, + pathParameters: {'id': 'new'}, ); }, ), @@ -203,7 +203,7 @@ class HomeScreen extends StatelessWidget { // Andiamo alla lista! (Da lì poi aggiungeremo il tasto "+" per il form) context.pushNamed( Routes.ticketForm, - pathParameters: {'id': 'New'}, + pathParameters: {'id': 'new'}, ); }, ), diff --git a/lib/features/operations/ui/operation_list_screen.dart b/lib/features/operations/ui/operation_list_screen.dart index 72db6df..3d091ff 100644 --- a/lib/features/operations/ui/operation_list_screen.dart +++ b/lib/features/operations/ui/operation_list_screen.dart @@ -193,6 +193,6 @@ class _OperationListScreenState extends State { } void startNewOperation(BuildContext context) { - context.pushNamed('operation-form'); + context.pushNamed('operation-form', pathParameters: {'id': 'new'}); } } diff --git a/lib/features/tickets/blocs/ticket_form_cubit.dart b/lib/features/tickets/blocs/ticket_form_cubit.dart index 2c12ae3..8a3a028 100644 --- a/lib/features/tickets/blocs/ticket_form_cubit.dart +++ b/lib/features/tickets/blocs/ticket_form_cubit.dart @@ -73,14 +73,15 @@ class TicketFormCubit extends Cubit { ticket: state.ticket.copyWith( customerId: customer.id, customerName: customer.name, - alternativePhoneNumber: customer.phoneNumber, // Comodo come fallback! + alternativePhoneNumber: customer.phoneNumber, + customerEmail: customer.email, ), ), ); } /// 3. AGGIORNAMENTO MODELLO (Usato dal nostro SharedModelSection!) - void updateModel({required String modelId, required String modelName}) { + void updateTargetModel({required String modelId, required String modelName}) { emit( state.copyWith( ticket: state.ticket.copyWith( @@ -143,7 +144,7 @@ class TicketFormCubit extends Cubit { } /// 5. SALVATAGGIO - Future saveTicket({required bool keepAdding}) async { + Future saveTicket() async { emit(state.copyWith(status: TicketFormStatus.saving)); try { @@ -159,28 +160,15 @@ class TicketFormCubit extends Cubit { } else { savedTicket = await _repository.updateTicket(ticketToSave); } - - if (keepAdding) { - emit( - state.copyWith( - status: TicketFormStatus.successAndAddAnother, - // Svuotiamo il form per il prossimo, mantenendo Store e Creatore ATTUALI - ticket: TicketModel.empty().copyWith( - companyId: savedTicket.companyId, - storeId: savedTicket.storeId, - createdById: ticketToSave - .createdById, // Manteniamo quello selezionato nella tendina! - createdByName: ticketToSave.createdByName, - ticketStatus: TicketStatus.open, - ticketType: TicketType.repair, - ), + emit( + state.copyWith( + status: TicketFormStatus.success, + ticket: ticketToSave.copyWith( + id: savedTicket.id, + referenceId: savedTicket.referenceId, ), - ); - } else { - emit( - state.copyWith(status: TicketFormStatus.success, ticket: savedTicket), - ); - } + ), + ); } catch (e) { emit( state.copyWith( diff --git a/lib/features/tickets/blocs/ticket_form_state.dart b/lib/features/tickets/blocs/ticket_form_state.dart index ecf277d..493ff03 100644 --- a/lib/features/tickets/blocs/ticket_form_state.dart +++ b/lib/features/tickets/blocs/ticket_form_state.dart @@ -2,15 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:flux/features/tickets/models/ticket_model.dart'; // Adatta gli import al tuo progetto! -enum TicketFormStatus { - initial, - ready, - loading, - saving, - success, - successAndAddAnother, - failure, -} +enum TicketFormStatus { initial, ready, loading, saving, success, pop, failure } class TicketFormState extends Equatable { final TicketModel ticket; diff --git a/lib/features/tickets/models/ticket_model.dart b/lib/features/tickets/models/ticket_model.dart index 3f307c8..55bb53d 100644 --- a/lib/features/tickets/models/ticket_model.dart +++ b/lib/features/tickets/models/ticket_model.dart @@ -107,6 +107,7 @@ class TicketModel extends Equatable { final TicketResult? ticketResult; final String? resolutionNotes; final String? customerName; + final String? customerEmail; final String? targetModelName; final String? sourceModelName; final String? createdById; @@ -142,6 +143,7 @@ class TicketModel extends Equatable { this.ticketResult, this.resolutionNotes, this.customerName, + this.customerEmail, this.targetModelName, this.sourceModelName, this.createdById, @@ -192,6 +194,7 @@ class TicketModel extends Equatable { TicketResult? ticketResult, String? resolutionNotes, String? customerName, + String? customerEmail, String? targetModelName, String? sourceModelName, String? createdById, @@ -228,6 +231,7 @@ class TicketModel extends Equatable { ticketResult: ticketResult ?? this.ticketResult, resolutionNotes: resolutionNotes ?? this.resolutionNotes, customerName: customerName ?? this.customerName, + customerEmail: customerEmail ?? this.customerEmail, targetModelName: targetModelName ?? this.targetModelName, sourceModelName: sourceModelName ?? this.sourceModelName, createdById: createdById ?? this.createdById, @@ -276,6 +280,7 @@ class TicketModel extends Equatable { ticketResult: TicketResult.fromString(map['ticket_result'] as String?), resolutionNotes: map['resolution_notes'] as String?, customerName: (map['customer']?['name'] as String?).myFormat(), + customerEmail: (map['customer']?['email'] as String?).myFormat(), targetModelName: (map['target_model']?['name_with_brand'] as String?) ?.myFormat(), sourceModelName: (map['source_model']?['name_with_brand'] as String?) @@ -350,6 +355,7 @@ class TicketModel extends Equatable { resolutionNotes, includedAccessories, customerName, + customerEmail, targetModelName, sourceModelName, createdById, diff --git a/lib/features/tickets/ui/ticket_form_screen.dart b/lib/features/tickets/ui/ticket_form_screen.dart index 75f34e3..f3dc2ed 100644 --- a/lib/features/tickets/ui/ticket_form_screen.dart +++ b/lib/features/tickets/ui/ticket_form_screen.dart @@ -1,14 +1,20 @@ 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:pdf/pdf.dart'; +import 'package:printing/printing.dart'; class TicketFormScreen extends StatefulWidget { final TicketModel? existingTicket; @@ -93,10 +99,10 @@ class _TicketFormScreenState extends State { ); } - void _saveTicket({required bool keepAdding}) { + void _saveTicket() { if (_formKey.currentState!.validate()) { _flushControllersToCubit(); - context.read().saveTicket(keepAdding: keepAdding); + context.read().saveTicket(); } } @@ -121,6 +127,110 @@ class _TicketFormScreenState extends State { 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 { + // 1. Costruiamo la struttura (velocissimo) + final doc = await TicketPdfService() + .generateTicketReceipt(ticket, company); + + // 2. Lanciamo layoutPdf esattamente come facevi tu! + await Printing.layoutPdf( + name: 'Ricevuta_${ticket.referenceId}.pdf', + onLayout: (PdfPageFormat format) async => + doc.save(), // La magia è qui! + ); + }, + ), + if (company.labelFormat != LabelFormat.none) + _ActionButton( + icon: Icons.label, + label: "Etichetta", + onTap: () async { + final doc = await TicketPdfService().generateLabelPdf( + ticket, + company, + ); + await Printing.layoutPdf( + name: 'Etichetta_${ticket.referenceId}.pdf', + onLayout: (PdfPageFormat format) async => doc.save(), + ); + }, + ), + _ActionButton( + icon: Icons.email, + label: "Invia Email", + onTap: ticket.customerEmail != null ? () {} : null, + ), + _ActionButton( + icon: Icons.close, + label: "Chiudi", + color: Colors.blue[900], + textColor: Colors.white, + onTap: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + }, + ), + ], + ), + ], + ), + ), + ), + ); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -133,22 +243,13 @@ class _TicketFormScreenState extends State { } if (state.status == TicketFormStatus.success) { - Navigator.of(context).pop(); - } else if (state.status == TicketFormStatus.successAndAddAnother) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Scheda salvata! Inserisci la prossima.'), - ), + _showSuccessActions( + context, + state.ticket, + GetIt.I.get().state.company!, ); - _altPhoneCtrl.clear(); - _serialCtrl.clear(); - _requestCtrl.clear(); - _accessoriesCtrl.clear(); - _publicNotesCtrl.clear(); - _internalNotesCtrl.clear(); - _priceCtrl.clear(); - _costCtrl.clear(); - _isInitialized = false; + } else if (state.status == TicketFormStatus.pop) { + Navigator.of(context).pop(); } else if (state.status == TicketFormStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -229,23 +330,23 @@ class _TicketFormScreenState extends State { children: [ Expanded( flex: 1, - child: OutlinedButton( - onPressed: state.status == TicketFormStatus.saving + child: ElevatedButton( + onPressed: state.ticket.id == null ? null - : () => _saveTicket(keepAdding: true), - child: const Text( - 'Salva e Aggiungi Altro', - textAlign: TextAlign.center, - ), + : () => _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(keepAdding: false), + : () => _saveTicket(), child: state.status == TicketFormStatus.saving ? const SizedBox( width: 20, @@ -378,7 +479,7 @@ class _TicketFormScreenState extends State { modelName: ticket.targetModelName, onModelSelected: (id, name) => context .read() - .updateModel(modelId: id, modelName: name), + .updateTargetModel(modelId: id, modelName: name), ), const SizedBox(height: 16), TextFormField( @@ -597,3 +698,55 @@ class _TicketFormScreenState extends State { ); } } + +// 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]), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/tickets/ui/ticket_list_screen.dart b/lib/features/tickets/ui/ticket_list_screen.dart index fec7eb1..7d2d51d 100644 --- a/lib/features/tickets/ui/ticket_list_screen.dart +++ b/lib/features/tickets/ui/ticket_list_screen.dart @@ -149,7 +149,7 @@ class _TicketListScreenState extends State { ), floatingActionButton: FloatingActionButton.extended( onPressed: () { - context.pushNamed(Routes.ticketForm, pathParameters: {'id': 'New'}); + context.pushNamed(Routes.ticketForm, pathParameters: {'id': 'new'}); }, icon: const Icon(Icons.add), label: const Text('Nuovo Ticket'), diff --git a/lib/features/tickets/utils/ticket_pdf_service.dart b/lib/features/tickets/utils/ticket_pdf_service.dart index 9d96699..37c3efd 100644 --- a/lib/features/tickets/utils/ticket_pdf_service.dart +++ b/lib/features/tickets/utils/ticket_pdf_service.dart @@ -1,4 +1,3 @@ -import 'dart:typed_data'; import 'package:pdf/pdf.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:printing/printing.dart'; @@ -7,7 +6,7 @@ import 'package:flux/features/company/models/company_model.dart'; class TicketPdfService { /// Funzione principale: Genera il PDF A4 con le due metà - Future generateTicketReceipt( + Future generateTicketReceipt( TicketModel ticket, CompanyModel company, ) async { @@ -16,6 +15,8 @@ class TicketPdfService { // Carichiamo il font per essere sicuri che i caratteri siano ok final font = await PdfGoogleFonts.robotoRegular(); final boldFont = await PdfGoogleFonts.robotoBold(); + /* final font = pw.Font.helvetica(); + final boldFont = pw.Font.helveticaBold(); */ pdf.addPage( pw.Page( @@ -60,7 +61,7 @@ class TicketPdfService { ), ); - return pdf.save(); + return pdf; } /// Helper per costruire una singola metà (Cliente o Negozio) @@ -71,8 +72,8 @@ class TicketPdfService { pw.Font boldFont, { required bool isForCustomer, }) { - return pw.Container( - height: 380, // Circa metà A4 meno i margini + return pw.Expanded( + //height: 380, // Circa metà A4 meno i margini child: pw.Column( crossAxisAlignment: pw.CrossAxisAlignment.start, children: [ @@ -276,7 +277,7 @@ class TicketPdfService { ); } - Future generateLabelPdf( + Future generateLabelPdf( TicketModel ticket, CompanyModel company, ) async { @@ -337,6 +338,6 @@ class TicketPdfService { ), ); - return pdf.save(); + return pdf; } } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index ea697c6..e5e7fa5 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -8,6 +8,8 @@ PODS: - FlutterMacOS (1.0.0) - pdfx (1.0.0): - FlutterMacOS + - printing (1.0.0): + - FlutterMacOS - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS @@ -20,6 +22,7 @@ DEPENDENCIES: - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - pdfx (from `Flutter/ephemeral/.symlinks/plugins/pdfx/macos`) + - printing (from `Flutter/ephemeral/.symlinks/plugins/printing/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) @@ -34,6 +37,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral pdfx: :path: Flutter/ephemeral/.symlinks/plugins/pdfx/macos + printing: + :path: Flutter/ephemeral/.symlinks/plugins/printing/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin url_launcher_macos: @@ -45,6 +50,7 @@ SPEC CHECKSUMS: file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 pdfx: 1e79f57f7a6ce2f4a4c30f21fa54d3dc82441b51 + printing: c4cf83c78fd684f9bc318e6aadc18972aa48f617 shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements index b37f846..5dbc9d9 100644 --- a/macos/Runner/DebugProfile.entitlements +++ b/macos/Runner/DebugProfile.entitlements @@ -23,5 +23,7 @@ com.apple.security.device.audio-input + com.apple.security.print + diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements index c487157..514ef18 100644 --- a/macos/Runner/Release.entitlements +++ b/macos/Runner/Release.entitlements @@ -18,6 +18,8 @@ com.apple.security.files.user-selected.read-write + com.apple.security.print +