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
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'},
);
},
),

View File

@@ -193,6 +193,6 @@ class _OperationListScreenState extends State<OperationListScreen> {
}
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(
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<TicketFormState> {
}
/// 5. SALVATAGGIO
Future<void> saveTicket({required bool keepAdding}) async {
Future<void> saveTicket() async {
emit(state.copyWith(status: TicketFormStatus.saving));
try {
@@ -159,28 +160,15 @@ class TicketFormCubit extends Cubit<TicketFormState> {
} 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(

View File

@@ -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;

View File

@@ -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,

View File

@@ -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<TicketFormScreen> {
);
}
void _saveTicket({required bool keepAdding}) {
void _saveTicket() {
if (_formKey.currentState!.validate()) {
_flushControllersToCubit();
context.read<TicketFormCubit>().saveTicket(keepAdding: keepAdding);
context.read<TicketFormCubit>().saveTicket();
}
}
@@ -121,6 +127,110 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
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<TicketFormScreen> {
}
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<SessionCubit>().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<TicketFormScreen> {
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<SessionCubit>().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<TicketFormScreen> {
modelName: ticket.targetModelName,
onModelSelected: (id, name) => context
.read<TicketFormCubit>()
.updateModel(modelId: id, modelName: name),
.updateTargetModel(modelId: id, modelName: name),
),
const SizedBox(height: 16),
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(
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'),

View File

@@ -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<Uint8List> generateTicketReceipt(
Future<pw.Document> 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<Uint8List> generateLabelPdf(
Future<pw.Document> generateLabelPdf(
TicketModel ticket,
CompanyModel company,
) async {
@@ -337,6 +338,6 @@ class TicketPdfService {
),
);
return pdf.save();
return pdf;
}
}

View File

@@ -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

View File

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

View File

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