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

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