This commit is contained in:
2026-05-11 11:44:14 +02:00
parent 5c86483563
commit a76180497e
11 changed files with 221 additions and 71 deletions

View File

@@ -190,7 +190,7 @@ class HomeScreen extends StatelessWidget {
// Entriamo nel form! Nessun parametro extra = Nuovo Servizio // Entriamo nel form! Nessun parametro extra = Nuovo Servizio
context.pushNamed( context.pushNamed(
Routes.operationForm, 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) // Andiamo alla lista! (Da lì poi aggiungeremo il tasto "+" per il form)
context.pushNamed( context.pushNamed(
Routes.ticketForm, Routes.ticketForm,
pathParameters: {'id': 'New'}, pathParameters: {'id': 'new'},
); );
}, },
), ),

View File

@@ -193,6 +193,6 @@ class _OperationListScreenState extends State<OperationListScreen> {
} }
void startNewOperation(BuildContext context) { void startNewOperation(BuildContext context) {
context.pushNamed('operation-form'); context.pushNamed('operation-form', pathParameters: {'id': 'new'});
} }
} }

View File

@@ -73,14 +73,15 @@ class TicketFormCubit extends Cubit<TicketFormState> {
ticket: state.ticket.copyWith( ticket: state.ticket.copyWith(
customerId: customer.id, customerId: customer.id,
customerName: customer.name, customerName: customer.name,
alternativePhoneNumber: customer.phoneNumber, // Comodo come fallback! alternativePhoneNumber: customer.phoneNumber,
customerEmail: customer.email,
), ),
), ),
); );
} }
/// 3. AGGIORNAMENTO MODELLO (Usato dal nostro SharedModelSection!) /// 3. AGGIORNAMENTO MODELLO (Usato dal nostro SharedModelSection!)
void updateModel({required String modelId, required String modelName}) { void updateTargetModel({required String modelId, required String modelName}) {
emit( emit(
state.copyWith( state.copyWith(
ticket: state.ticket.copyWith( ticket: state.ticket.copyWith(
@@ -143,7 +144,7 @@ class TicketFormCubit extends Cubit<TicketFormState> {
} }
/// 5. SALVATAGGIO /// 5. SALVATAGGIO
Future<void> saveTicket({required bool keepAdding}) async { Future<void> saveTicket() async {
emit(state.copyWith(status: TicketFormStatus.saving)); emit(state.copyWith(status: TicketFormStatus.saving));
try { try {
@@ -159,28 +160,15 @@ class TicketFormCubit extends Cubit<TicketFormState> {
} else { } else {
savedTicket = await _repository.updateTicket(ticketToSave); savedTicket = await _repository.updateTicket(ticketToSave);
} }
if (keepAdding) {
emit( emit(
state.copyWith( state.copyWith(
status: TicketFormStatus.successAndAddAnother, status: TicketFormStatus.success,
// Svuotiamo il form per il prossimo, mantenendo Store e Creatore ATTUALI ticket: ticketToSave.copyWith(
ticket: TicketModel.empty().copyWith( id: savedTicket.id,
companyId: savedTicket.companyId, referenceId: savedTicket.referenceId,
storeId: savedTicket.storeId,
createdById: ticketToSave
.createdById, // Manteniamo quello selezionato nella tendina!
createdByName: ticketToSave.createdByName,
ticketStatus: TicketStatus.open,
ticketType: TicketType.repair,
), ),
), ),
); );
} else {
emit(
state.copyWith(status: TicketFormStatus.success, ticket: savedTicket),
);
}
} catch (e) { } catch (e) {
emit( emit(
state.copyWith( state.copyWith(

View File

@@ -2,15 +2,7 @@ import 'package:equatable/equatable.dart';
import 'package:flux/features/tickets/models/ticket_model.dart'; import 'package:flux/features/tickets/models/ticket_model.dart';
// Adatta gli import al tuo progetto! // Adatta gli import al tuo progetto!
enum TicketFormStatus { enum TicketFormStatus { initial, ready, loading, saving, success, pop, failure }
initial,
ready,
loading,
saving,
success,
successAndAddAnother,
failure,
}
class TicketFormState extends Equatable { class TicketFormState extends Equatable {
final TicketModel ticket; final TicketModel ticket;

View File

@@ -107,6 +107,7 @@ class TicketModel extends Equatable {
final TicketResult? ticketResult; final TicketResult? ticketResult;
final String? resolutionNotes; final String? resolutionNotes;
final String? customerName; final String? customerName;
final String? customerEmail;
final String? targetModelName; final String? targetModelName;
final String? sourceModelName; final String? sourceModelName;
final String? createdById; final String? createdById;
@@ -142,6 +143,7 @@ class TicketModel extends Equatable {
this.ticketResult, this.ticketResult,
this.resolutionNotes, this.resolutionNotes,
this.customerName, this.customerName,
this.customerEmail,
this.targetModelName, this.targetModelName,
this.sourceModelName, this.sourceModelName,
this.createdById, this.createdById,
@@ -192,6 +194,7 @@ class TicketModel extends Equatable {
TicketResult? ticketResult, TicketResult? ticketResult,
String? resolutionNotes, String? resolutionNotes,
String? customerName, String? customerName,
String? customerEmail,
String? targetModelName, String? targetModelName,
String? sourceModelName, String? sourceModelName,
String? createdById, String? createdById,
@@ -228,6 +231,7 @@ class TicketModel extends Equatable {
ticketResult: ticketResult ?? this.ticketResult, ticketResult: ticketResult ?? this.ticketResult,
resolutionNotes: resolutionNotes ?? this.resolutionNotes, resolutionNotes: resolutionNotes ?? this.resolutionNotes,
customerName: customerName ?? this.customerName, customerName: customerName ?? this.customerName,
customerEmail: customerEmail ?? this.customerEmail,
targetModelName: targetModelName ?? this.targetModelName, targetModelName: targetModelName ?? this.targetModelName,
sourceModelName: sourceModelName ?? this.sourceModelName, sourceModelName: sourceModelName ?? this.sourceModelName,
createdById: createdById ?? this.createdById, createdById: createdById ?? this.createdById,
@@ -276,6 +280,7 @@ class TicketModel extends Equatable {
ticketResult: TicketResult.fromString(map['ticket_result'] as String?), ticketResult: TicketResult.fromString(map['ticket_result'] as String?),
resolutionNotes: map['resolution_notes'] as String?, resolutionNotes: map['resolution_notes'] as String?,
customerName: (map['customer']?['name'] as String?).myFormat(), customerName: (map['customer']?['name'] as String?).myFormat(),
customerEmail: (map['customer']?['email'] as String?).myFormat(),
targetModelName: (map['target_model']?['name_with_brand'] as String?) targetModelName: (map['target_model']?['name_with_brand'] as String?)
?.myFormat(), ?.myFormat(),
sourceModelName: (map['source_model']?['name_with_brand'] as String?) sourceModelName: (map['source_model']?['name_with_brand'] as String?)
@@ -350,6 +355,7 @@ class TicketModel extends Equatable {
resolutionNotes, resolutionNotes,
includedAccessories, includedAccessories,
customerName, customerName,
customerEmail,
targetModelName, targetModelName,
sourceModelName, sourceModelName,
createdById, createdById,

View File

@@ -1,14 +1,20 @@
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/blocs/session/session_cubit.dart';
import 'package:flux/core/widgets/shared_forms/customer_section.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/model_section.dart';
import 'package:flux/core/widgets/shared_forms/shared_files_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/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_cubit.dart';
import 'package:flux/features/tickets/blocs/ticket_form_state.dart'; import 'package:flux/features/tickets/blocs/ticket_form_state.dart';
import 'package:flux/features/tickets/models/ticket_model.dart'; import 'package:flux/features/tickets/models/ticket_model.dart';
import 'package:flux/core/widgets/shared_forms/staff_section.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/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 { class TicketFormScreen extends StatefulWidget {
final TicketModel? existingTicket; final TicketModel? existingTicket;
@@ -93,10 +99,10 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
); );
} }
void _saveTicket({required bool keepAdding}) { void _saveTicket() {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
_flushControllersToCubit(); _flushControllersToCubit();
context.read<TicketFormCubit>().saveTicket(keepAdding: keepAdding); context.read<TicketFormCubit>().saveTicket();
} }
} }
@@ -121,6 +127,110 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
return 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 {
// 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -133,22 +243,13 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
} }
if (state.status == TicketFormStatus.success) { if (state.status == TicketFormStatus.success) {
Navigator.of(context).pop(); _showSuccessActions(
} else if (state.status == TicketFormStatus.successAndAddAnother) { context,
ScaffoldMessenger.of(context).showSnackBar( state.ticket,
const SnackBar( GetIt.I.get<SessionCubit>().state.company!,
content: Text('Scheda salvata! Inserisci la prossima.'),
),
); );
_altPhoneCtrl.clear(); } else if (state.status == TicketFormStatus.pop) {
_serialCtrl.clear(); Navigator.of(context).pop();
_requestCtrl.clear();
_accessoriesCtrl.clear();
_publicNotesCtrl.clear();
_internalNotesCtrl.clear();
_priceCtrl.clear();
_costCtrl.clear();
_isInitialized = false;
} else if (state.status == TicketFormStatus.failure) { } else if (state.status == TicketFormStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@@ -229,23 +330,23 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
children: [ children: [
Expanded( Expanded(
flex: 1, flex: 1,
child: OutlinedButton( child: ElevatedButton(
onPressed: state.status == TicketFormStatus.saving onPressed: state.ticket.id == null
? null ? null
: () => _saveTicket(keepAdding: true), : () => _showSuccessActions(
child: const Text( context,
'Salva e Aggiungi Altro', ticket,
textAlign: TextAlign.center, GetIt.I.get<SessionCubit>().state.company!,
),
child: const Text('Ricevuta'),
), ),
), ),
),
const SizedBox(width: 12),
Expanded( Expanded(
flex: 1, flex: 1,
child: ElevatedButton( child: ElevatedButton(
onPressed: state.status == TicketFormStatus.saving onPressed: state.status == TicketFormStatus.saving
? null ? null
: () => _saveTicket(keepAdding: false), : () => _saveTicket(),
child: state.status == TicketFormStatus.saving child: state.status == TicketFormStatus.saving
? const SizedBox( ? const SizedBox(
width: 20, width: 20,
@@ -378,7 +479,7 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
modelName: ticket.targetModelName, modelName: ticket.targetModelName,
onModelSelected: (id, name) => context onModelSelected: (id, name) => context
.read<TicketFormCubit>() .read<TicketFormCubit>()
.updateModel(modelId: id, modelName: name), .updateTargetModel(modelId: id, modelName: name),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
TextFormField( TextFormField(
@@ -597,3 +698,55 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
); );
} }
} }
// 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]),
),
),
],
),
),
);
}
}

View File

@@ -149,7 +149,7 @@ class _TicketListScreenState extends State<TicketListScreen> {
), ),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
onPressed: () { onPressed: () {
context.pushNamed(Routes.ticketForm, pathParameters: {'id': 'New'}); context.pushNamed(Routes.ticketForm, pathParameters: {'id': 'new'});
}, },
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
label: const Text('Nuovo Ticket'), label: const Text('Nuovo Ticket'),

View File

@@ -1,4 +1,3 @@
import 'dart:typed_data';
import 'package:pdf/pdf.dart'; import 'package:pdf/pdf.dart';
import 'package:pdf/widgets.dart' as pw; import 'package:pdf/widgets.dart' as pw;
import 'package:printing/printing.dart'; import 'package:printing/printing.dart';
@@ -7,7 +6,7 @@ import 'package:flux/features/company/models/company_model.dart';
class TicketPdfService { class TicketPdfService {
/// Funzione principale: Genera il PDF A4 con le due metà /// Funzione principale: Genera il PDF A4 con le due metà
Future<Uint8List> generateTicketReceipt( Future<pw.Document> generateTicketReceipt(
TicketModel ticket, TicketModel ticket,
CompanyModel company, CompanyModel company,
) async { ) async {
@@ -16,6 +15,8 @@ class TicketPdfService {
// Carichiamo il font per essere sicuri che i caratteri siano ok // Carichiamo il font per essere sicuri che i caratteri siano ok
final font = await PdfGoogleFonts.robotoRegular(); final font = await PdfGoogleFonts.robotoRegular();
final boldFont = await PdfGoogleFonts.robotoBold(); final boldFont = await PdfGoogleFonts.robotoBold();
/* final font = pw.Font.helvetica();
final boldFont = pw.Font.helveticaBold(); */
pdf.addPage( pdf.addPage(
pw.Page( pw.Page(
@@ -60,7 +61,7 @@ class TicketPdfService {
), ),
); );
return pdf.save(); return pdf;
} }
/// Helper per costruire una singola metà (Cliente o Negozio) /// Helper per costruire una singola metà (Cliente o Negozio)
@@ -71,8 +72,8 @@ class TicketPdfService {
pw.Font boldFont, { pw.Font boldFont, {
required bool isForCustomer, required bool isForCustomer,
}) { }) {
return pw.Container( return pw.Expanded(
height: 380, // Circa metà A4 meno i margini //height: 380, // Circa metà A4 meno i margini
child: pw.Column( child: pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start, crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [ children: [
@@ -276,7 +277,7 @@ class TicketPdfService {
); );
} }
Future<Uint8List> generateLabelPdf( Future<pw.Document> generateLabelPdf(
TicketModel ticket, TicketModel ticket,
CompanyModel company, CompanyModel company,
) async { ) async {
@@ -337,6 +338,6 @@ class TicketPdfService {
), ),
); );
return pdf.save(); return pdf;
} }
} }

View File

@@ -8,6 +8,8 @@ PODS:
- FlutterMacOS (1.0.0) - FlutterMacOS (1.0.0)
- pdfx (1.0.0): - pdfx (1.0.0):
- FlutterMacOS - FlutterMacOS
- printing (1.0.0):
- FlutterMacOS
- shared_preferences_foundation (0.0.1): - shared_preferences_foundation (0.0.1):
- Flutter - Flutter
- FlutterMacOS - FlutterMacOS
@@ -20,6 +22,7 @@ DEPENDENCIES:
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`) - file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
- FlutterMacOS (from `Flutter/ephemeral`) - FlutterMacOS (from `Flutter/ephemeral`)
- pdfx (from `Flutter/ephemeral/.symlinks/plugins/pdfx/macos`) - 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`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
@@ -34,6 +37,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral :path: Flutter/ephemeral
pdfx: pdfx:
:path: Flutter/ephemeral/.symlinks/plugins/pdfx/macos :path: Flutter/ephemeral/.symlinks/plugins/pdfx/macos
printing:
:path: Flutter/ephemeral/.symlinks/plugins/printing/macos
shared_preferences_foundation: shared_preferences_foundation:
:path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin
url_launcher_macos: url_launcher_macos:
@@ -45,6 +50,7 @@ SPEC CHECKSUMS:
file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7 file_selector_macos: 9e9e068e90ebee155097d00e89ae91edb2374db7
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
pdfx: 1e79f57f7a6ce2f4a4c30f21fa54d3dc82441b51 pdfx: 1e79f57f7a6ce2f4a4c30f21fa54d3dc82441b51
printing: c4cf83c78fd684f9bc318e6aadc18972aa48f617
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd

View File

@@ -23,5 +23,7 @@
<key>com.apple.security.device.audio-input</key> <key>com.apple.security.device.audio-input</key>
<true/> <true/>
<key>com.apple.security.print</key>
<true/>
</dict> </dict>
</plist> </plist>

View File

@@ -18,6 +18,8 @@
<key>com.apple.security.files.user-selected.read-write</key> <key>com.apple.security.files.user-selected.read-write</key>
<true/> <true/>
<key>com.apple.security.print</key>
<true/>
</dict> </dict>
</plist> </plist>