ticket labels e ticket receipt
This commit is contained in:
@@ -37,6 +37,11 @@ class CompanySettingsCubit extends Cubit<CompanySettingsState> {
|
||||
String? zipCode,
|
||||
String? phone,
|
||||
String? email,
|
||||
String? ticketDisclaimer,
|
||||
LabelFormat? labelFormat,
|
||||
double? labelWidth,
|
||||
double? labelHeight,
|
||||
bool? isVertical,
|
||||
}) {
|
||||
if (state.company == null) return;
|
||||
|
||||
@@ -51,6 +56,11 @@ class CompanySettingsCubit extends Cubit<CompanySettingsState> {
|
||||
zipCode: zipCode ?? state.company!.zipCode,
|
||||
phone: phone ?? state.company!.phone,
|
||||
email: email ?? state.company!.email,
|
||||
ticketDisclaimer: ticketDisclaimer ?? state.company!.ticketDisclaimer,
|
||||
labelFormat: labelFormat ?? state.company!.labelFormat,
|
||||
labelWidth: labelWidth ?? state.company!.labelWidth,
|
||||
labelHeight: labelHeight ?? state.company!.labelHeight,
|
||||
isLabelVertical: isVertical ?? state.company!.isLabelVertical,
|
||||
);
|
||||
emit(state.copyWith(company: updated));
|
||||
}
|
||||
|
||||
@@ -35,6 +35,21 @@ enum SubscriptionStatus {
|
||||
}
|
||||
}
|
||||
|
||||
enum LabelFormat {
|
||||
none,
|
||||
small_62x29,
|
||||
medium_54x101,
|
||||
large_102x152,
|
||||
custom;
|
||||
|
||||
static LabelFormat fromString(String? value) {
|
||||
return LabelFormat.values.firstWhere(
|
||||
(e) => e.name == value,
|
||||
orElse: () => LabelFormat.none,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ===================================================================
|
||||
// IL MODELLO ESATTO
|
||||
// ===================================================================
|
||||
@@ -56,7 +71,11 @@ class CompanyModel extends Equatable {
|
||||
final String? phone;
|
||||
final String? email;
|
||||
final String? logoUrl;
|
||||
|
||||
final String? ticketDisclaimer;
|
||||
final LabelFormat labelFormat;
|
||||
final double? labelWidth;
|
||||
final double? labelHeight;
|
||||
final bool isLabelVertical;
|
||||
// Stato Pagamenti (Ibride: manuale + Stripe)
|
||||
final bool isPaid;
|
||||
final DateTime? paymentExpiration;
|
||||
@@ -83,6 +102,11 @@ class CompanyModel extends Equatable {
|
||||
this.phone,
|
||||
this.email,
|
||||
this.logoUrl,
|
||||
this.ticketDisclaimer,
|
||||
this.labelFormat = LabelFormat.none,
|
||||
this.labelWidth,
|
||||
this.labelHeight,
|
||||
this.isLabelVertical = false,
|
||||
this.isPaid = false,
|
||||
this.paymentExpiration,
|
||||
this.subscriptionTier = SubscriptionTier.free,
|
||||
@@ -105,6 +129,11 @@ class CompanyModel extends Equatable {
|
||||
String? fiscalCode,
|
||||
String? sdi,
|
||||
String? logoUrl,
|
||||
String? ticketDisclaimer,
|
||||
LabelFormat? labelFormat,
|
||||
double? labelWidth,
|
||||
double? labelHeight,
|
||||
bool? isLabelVertical,
|
||||
String? phone,
|
||||
String? email,
|
||||
bool? isPaid,
|
||||
@@ -130,6 +159,11 @@ class CompanyModel extends Equatable {
|
||||
logoUrl: logoUrl ?? this.logoUrl,
|
||||
phone: phone ?? this.phone,
|
||||
email: email ?? this.email,
|
||||
ticketDisclaimer: ticketDisclaimer ?? this.ticketDisclaimer,
|
||||
labelFormat: labelFormat ?? this.labelFormat,
|
||||
labelWidth: labelWidth ?? this.labelWidth,
|
||||
labelHeight: labelHeight ?? this.labelHeight,
|
||||
isLabelVertical: isLabelVertical ?? this.isLabelVertical,
|
||||
isPaid: isPaid ?? this.isPaid,
|
||||
paymentExpiration: paymentExpiration ?? this.paymentExpiration,
|
||||
subscriptionTier: subscriptionTier ?? this.subscriptionTier,
|
||||
@@ -171,9 +205,18 @@ class CompanyModel extends Equatable {
|
||||
vatId: map['vat_id'] ?? '',
|
||||
fiscalCode: map['fiscal_code'] ?? '',
|
||||
sdi: map['sdi'] ?? '',
|
||||
logoUrl: map['company_logo'],
|
||||
logoUrl: map['logo_url'],
|
||||
phone: map['phone'] ?? '',
|
||||
email: map['email'] ?? '',
|
||||
ticketDisclaimer: map['ticket_disclaimer'],
|
||||
labelFormat: LabelFormat.fromString(map['label_format']),
|
||||
labelWidth: map['label_width'] != null
|
||||
? (map['label_width'] as num).toDouble()
|
||||
: null,
|
||||
labelHeight: map['label_height'] != null
|
||||
? (map['label_height'] as num).toDouble()
|
||||
: null,
|
||||
isLabelVertical: map['is_label_vertical'] ?? false,
|
||||
isPaid: map['is_paid'] ?? false,
|
||||
paymentExpiration: map['payment_expiration'] != null
|
||||
? DateTime.tryParse(map['payment_expiration'])
|
||||
@@ -203,9 +246,14 @@ class CompanyModel extends Equatable {
|
||||
'vat_id': vatId,
|
||||
'fiscal_code': fiscalCode,
|
||||
'sdi': sdi,
|
||||
'company_logo': logoUrl,
|
||||
'logo_url': logoUrl,
|
||||
'phone': phone,
|
||||
'email': email,
|
||||
'ticket_disclaimer': ticketDisclaimer,
|
||||
'label_format': labelFormat.name,
|
||||
'label_width': labelWidth,
|
||||
'label_height': labelHeight,
|
||||
'is_label_vertical': isLabelVertical,
|
||||
'is_paid': isPaid,
|
||||
if (paymentExpiration != null)
|
||||
'payment_expiration': paymentExpiration!.toIso8601String(),
|
||||
@@ -236,6 +284,11 @@ class CompanyModel extends Equatable {
|
||||
logoUrl,
|
||||
phone,
|
||||
email,
|
||||
ticketDisclaimer,
|
||||
labelFormat,
|
||||
labelWidth,
|
||||
labelHeight,
|
||||
isLabelVertical,
|
||||
isPaid,
|
||||
paymentExpiration,
|
||||
subscriptionTier,
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/company/bloc/company_settings_cubit.dart';
|
||||
import 'package:flux/features/company/models/company_model.dart';
|
||||
import 'package:flux/features/settings/document_sequence/blocs/document_sequence_cubit.dart';
|
||||
import 'package:flux/features/settings/document_sequence/ui/document_sequence_section.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class CompanySettingsScreen extends StatefulWidget {
|
||||
@@ -24,6 +26,7 @@ class _CompanySettingsScreenState extends State<CompanySettingsScreen> {
|
||||
final _zipCtrl = TextEditingController();
|
||||
final _phoneCtrl = TextEditingController();
|
||||
final _emailCtrl = TextEditingController();
|
||||
final _disclaimerCtrl = TextEditingController();
|
||||
|
||||
bool _isInitialized = false;
|
||||
|
||||
@@ -50,6 +53,8 @@ class _CompanySettingsScreenState extends State<CompanySettingsScreen> {
|
||||
_zipCtrl.dispose();
|
||||
_phoneCtrl.dispose();
|
||||
_emailCtrl.dispose();
|
||||
_disclaimerCtrl.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -69,6 +74,9 @@ class _CompanySettingsScreenState extends State<CompanySettingsScreen> {
|
||||
if (_phoneCtrl.text.isEmpty) _phoneCtrl.text = company.phone ?? '';
|
||||
if (_emailCtrl.text.isEmpty) _emailCtrl.text = company.email ?? '';
|
||||
_isInitialized = true;
|
||||
if (_disclaimerCtrl.text.isEmpty) {
|
||||
_disclaimerCtrl.text = company.ticketDisclaimer ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
void _flushToCubit() {
|
||||
@@ -83,6 +91,7 @@ class _CompanySettingsScreenState extends State<CompanySettingsScreen> {
|
||||
zipCode: _zipCtrl.text,
|
||||
phone: _phoneCtrl.text,
|
||||
email: _emailCtrl.text,
|
||||
ticketDisclaimer: _disclaimerCtrl.text,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -99,6 +108,36 @@ class _CompanySettingsScreenState extends State<CompanySettingsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _onLabelFormatChanged(LabelFormat selectedFormat) {
|
||||
double? w;
|
||||
double? h;
|
||||
|
||||
switch (selectedFormat) {
|
||||
case LabelFormat.small_62x29:
|
||||
w = 62.0;
|
||||
h = 29.0;
|
||||
break;
|
||||
case LabelFormat.medium_54x101:
|
||||
w = 54.0;
|
||||
h = 101.0;
|
||||
break;
|
||||
case LabelFormat.large_102x152:
|
||||
w = 102.0;
|
||||
h = 152.0;
|
||||
break;
|
||||
case LabelFormat.custom:
|
||||
case LabelFormat.none:
|
||||
// Lasciamo i valori null o quelli vecchi
|
||||
break;
|
||||
}
|
||||
|
||||
context.read<CompanySettingsCubit>().updateFields(
|
||||
labelFormat: selectedFormat,
|
||||
labelWidth: w,
|
||||
labelHeight: h,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -312,6 +351,63 @@ class _CompanySettingsScreenState extends State<CompanySettingsScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
DocumentSequenceCubit(state.company!.id!)
|
||||
..loadSequences(),
|
||||
child: const DocumentSequenceSection(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Sezione Disclaimer
|
||||
Text(
|
||||
"Note Legali Ricevuta",
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
TextFormField(
|
||||
controller: _disclaimerCtrl,
|
||||
maxLines: 5,
|
||||
decoration: const InputDecoration(
|
||||
hintText:
|
||||
"Inserisci qui la liberatoria legale che apparirà sulla ricevuta dei ticket...",
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (val) => context
|
||||
.read<CompanySettingsCubit>()
|
||||
.updateFields(ticketDisclaimer: val),
|
||||
),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Sezione Etichette
|
||||
Text(
|
||||
"Configurazione Etichette",
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<LabelFormat>(
|
||||
initialValue: company.labelFormat,
|
||||
decoration: const InputDecoration(
|
||||
prefixIcon: Icon(Icons.label_outline),
|
||||
labelText: "Formato Stampa Etichetta",
|
||||
),
|
||||
items: LabelFormat.values
|
||||
.map(
|
||||
(f) => DropdownMenuItem(
|
||||
value: f,
|
||||
child: Text(
|
||||
f.name.replaceAll('_', ' ').toUpperCase(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (val) {
|
||||
if (val != null) {
|
||||
_onLabelFormatChanged(val);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
|
||||
// --- PULSANTE SALVATAGGIO ---
|
||||
|
||||
@@ -217,46 +217,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
final isUltraWide = constraints.maxWidth > 1400;
|
||||
final isDesktop = constraints.maxWidth > 900;
|
||||
if (isUltraWide) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: _buildMainFormContent(
|
||||
theme,
|
||||
state,
|
||||
displayStatus,
|
||||
showFiles: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
VerticalDivider(width: 1, color: theme.dividerColor),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: _buildNotesSection(isDesktop: true),
|
||||
),
|
||||
),
|
||||
VerticalDivider(width: 1, color: theme.dividerColor),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: SharedAttachmentsSection(
|
||||
parentType: AttachmentParentType.operation,
|
||||
parentId: state.operation.id,
|
||||
titleForUpload:
|
||||
state.operation.customerDisplayName ??
|
||||
'Nuova pratica',
|
||||
onGenerateIdForQr: _generateIdForQr,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
return _buildUltraWide(state, theme);
|
||||
} else if (isDesktop) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -365,48 +326,92 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMainFormContent(
|
||||
ThemeData theme,
|
||||
OperationFormState state,
|
||||
OperationStatus displayStatus, {
|
||||
bool showFiles = true,
|
||||
}) {
|
||||
final currentOp = state.operation;
|
||||
final currentType = currentOp.type;
|
||||
|
||||
return Column(
|
||||
Widget _buildUltraWide(OperationFormState state, ThemeData theme) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
StaffSection(
|
||||
staffId: currentOp.staffId,
|
||||
staffName: currentOp.staffDisplayName,
|
||||
onStaffSelected: (staff) => {
|
||||
context.read<OperationFormCubit>().updateFields(
|
||||
staffId: staff.id,
|
||||
staffDisplayName: staff.name,
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildStaffSection(state),
|
||||
const Divider(height: 50),
|
||||
_buildOperationStatusSection(state),
|
||||
const Divider(height: 32),
|
||||
_buildCustomerSection(state),
|
||||
const SizedBox(height: 16),
|
||||
_buildReferenceSection(state),
|
||||
const Divider(height: 50),
|
||||
_buildOperationTypeSection(state),
|
||||
const SizedBox(height: 16),
|
||||
_buildQuantitySection(state),
|
||||
const Divider(height: 50),
|
||||
_buildDetailsSection(state),
|
||||
const Divider(height: 50),
|
||||
],
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(height: 50),
|
||||
VerticalDivider(width: 1, color: theme.dividerColor),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: _buildNotesSection(isDesktop: true),
|
||||
),
|
||||
),
|
||||
VerticalDivider(width: 1, color: theme.dividerColor),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: _buildAttachmentSection(state),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// --- SEZIONE STATO OPERAZIONE ---
|
||||
Widget _buildStaffSection(OperationFormState state) {
|
||||
return StaffSection(
|
||||
staffId: state.operation.staffId,
|
||||
staffName: state.operation.staffDisplayName,
|
||||
onStaffSelected: (staff) => {
|
||||
context.read<OperationFormCubit>().updateFields(
|
||||
staffId: staff.id,
|
||||
staffDisplayName: staff.name,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOperationStatusSection(OperationFormState state) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildSectionTitle('Esito / Stato Operazione'),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(displayStatus).withValues(alpha: 0.1),
|
||||
color: _getStatusColor(
|
||||
state.operation.status,
|
||||
).withValues(alpha: 0.1),
|
||||
border: Border.all(
|
||||
color: _getStatusColor(displayStatus).withValues(alpha: 0.3),
|
||||
color: _getStatusColor(
|
||||
state.operation.status,
|
||||
).withValues(alpha: 0.3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: DropdownButtonHideUnderline(
|
||||
child: DropdownButton<OperationStatus>(
|
||||
isExpanded: true,
|
||||
value: displayStatus,
|
||||
value: state.operation.status,
|
||||
icon: Icon(
|
||||
Icons.arrow_drop_down,
|
||||
color: _getStatusColor(displayStatus),
|
||||
color: _getStatusColor(state.operation.status),
|
||||
),
|
||||
items: OperationStatus.values
|
||||
/* .where(
|
||||
@@ -450,34 +455,41 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
displayStatus == OperationStatus.success
|
||||
state.operation.status == OperationStatus.success
|
||||
? 'Lascia OK se la pratica è stata caricata con successo.'
|
||||
: 'Attenzione: la pratica verrà salvata come ${displayStatus.displayName}.',
|
||||
: 'Attenzione: la pratica verrà salvata come ${state.operation.status.displayName}.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//_buildSectionTitle('Cliente & Riferimento'),
|
||||
SharedCustomerSection(
|
||||
customerId: currentOp.customerId,
|
||||
customerName: currentOp.customerDisplayName,
|
||||
onCustomerSelected: (customer) {
|
||||
context.read<OperationFormCubit>().updateFields(
|
||||
customerId: customer.id,
|
||||
customerDisplayName: customer.name,
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _referenceController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Riferimento (es. numero di telefono, targa...)',
|
||||
prefixIcon: Icon(Icons.tag),
|
||||
),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
Widget _buildCustomerSection(OperationFormState state) {
|
||||
return SharedCustomerSection(
|
||||
customerId: state.operation.customerId,
|
||||
customerName: state.operation.customerDisplayName,
|
||||
onCustomerSelected: (customer) {
|
||||
context.read<OperationFormCubit>().updateFields(
|
||||
customerId: customer.id,
|
||||
customerDisplayName: customer.name,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildReferenceSection(OperationFormState state) {
|
||||
return TextFormField(
|
||||
controller: _referenceController,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Riferimento (es. numero di telefono, targa...)',
|
||||
prefixIcon: Icon(Icons.tag),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOperationTypeSection(OperationFormState state) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildSectionTitle('Cosa stiamo facendo?'),
|
||||
Wrap(
|
||||
spacing: 8.0,
|
||||
@@ -485,7 +497,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
children: _availableTypes.map((type) {
|
||||
return ChoiceChip(
|
||||
label: Text(type),
|
||||
selected: currentType == type,
|
||||
selected: state.operation.type == type,
|
||||
onSelected: (selected) {
|
||||
if (selected) {
|
||||
context.read<OperationFormCubit>().setTypeWithSmartDefault(
|
||||
@@ -496,63 +508,90 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailsSection(OperationFormState state) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildSectionTitle('Dettagli Servizio'),
|
||||
DetailsSection(
|
||||
currentOp: currentOp,
|
||||
currentType: currentType,
|
||||
currentOp: state.operation,
|
||||
currentType: state.operation.type,
|
||||
freeTextSubtypeController: _freeTextSubtypeController,
|
||||
freeTextDescriptionController: _freeTextDescriptionController,
|
||||
durationQuickPicks: _buildDurationQuickPicks(currentOp),
|
||||
durationQuickPicks: _buildDurationQuickPicks(state.operation),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQuantitySection(OperationFormState state) {
|
||||
return Row(
|
||||
children: [
|
||||
const Text('Quantità: '),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () {
|
||||
final q = state.operation.quantity;
|
||||
if (q > 1) {
|
||||
context.read<OperationFormCubit>().updateFields(quantity: q - 1);
|
||||
}
|
||||
},
|
||||
),
|
||||
Text(
|
||||
'${state.operation.quantity}',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
final q = state.operation.quantity;
|
||||
context.read<OperationFormCubit>().updateFields(quantity: q + 1);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAttachmentSection(OperationFormState state) {
|
||||
return SharedAttachmentsSection(
|
||||
parentType: AttachmentParentType.operation,
|
||||
parentId: state.operation.id,
|
||||
titleForUpload: state.operation.customerDisplayName ?? 'Nuova pratica',
|
||||
onGenerateIdForQr: _generateIdForQr,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMainFormContent(
|
||||
ThemeData theme,
|
||||
OperationFormState state,
|
||||
OperationStatus displayStatus, {
|
||||
bool showFiles = true,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildStaffSection(state),
|
||||
const Divider(height: 50),
|
||||
_buildOperationStatusSection(state),
|
||||
const Divider(height: 32),
|
||||
_buildCustomerSection(state),
|
||||
const SizedBox(height: 16),
|
||||
_buildReferenceSection(state),
|
||||
const Divider(height: 50),
|
||||
_buildOperationTypeSection(state),
|
||||
const SizedBox(height: 16),
|
||||
_buildQuantitySection(state),
|
||||
const Divider(height: 50),
|
||||
_buildDetailsSection(state),
|
||||
const Divider(height: 50),
|
||||
|
||||
// QUANTITÀ
|
||||
Row(
|
||||
children: [
|
||||
const Text('Quantità: '),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.remove),
|
||||
onPressed: () {
|
||||
final q = currentOp.quantity;
|
||||
if (q > 1) {
|
||||
context.read<OperationFormCubit>().updateFields(
|
||||
quantity: q - 1,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
Text(
|
||||
'${currentOp.quantity}',
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () {
|
||||
final q = currentOp.quantity;
|
||||
context.read<OperationFormCubit>().updateFields(
|
||||
quantity: q + 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(height: 32),
|
||||
|
||||
if (showFiles) ...[
|
||||
SharedAttachmentsSection(
|
||||
parentType: AttachmentParentType.operation,
|
||||
parentId: currentOp.id,
|
||||
titleForUpload:
|
||||
state.operation.customerDisplayName ?? 'Nuova pratica',
|
||||
onGenerateIdForQr: _generateIdForQr,
|
||||
),
|
||||
/* SharedFilesSection(
|
||||
titleNameForUpload:
|
||||
state.operation.customerDisplayName ?? 'Nuova pratica',
|
||||
onGenerateIdForQr: _generateIdForQr,
|
||||
), */
|
||||
],
|
||||
if (showFiles) ...[_buildAttachmentSection(state)],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/settings/document_sequence/models/document_sequence_model.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class DocumentSequenceState {
|
||||
final List<DocumentSequence> sequences;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
DocumentSequenceState({
|
||||
this.sequences = const [],
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
}
|
||||
|
||||
class DocumentSequenceCubit extends Cubit<DocumentSequenceState> {
|
||||
final String companyId;
|
||||
final _supabase = Supabase.instance.client;
|
||||
|
||||
DocumentSequenceCubit(this.companyId) : super(DocumentSequenceState());
|
||||
|
||||
Future<void> loadSequences() async {
|
||||
emit(DocumentSequenceState(isLoading: true));
|
||||
try {
|
||||
final data = await _supabase
|
||||
.from('document_sequences')
|
||||
.select()
|
||||
.eq('company_id', companyId);
|
||||
|
||||
final list = (data as List)
|
||||
.map((e) => DocumentSequence.fromMap(e))
|
||||
.toList();
|
||||
emit(DocumentSequenceState(sequences: list));
|
||||
} catch (e) {
|
||||
emit(DocumentSequenceState(error: e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void updateLocalSequence(String docType, {String? prefix, int? nextValue}) {
|
||||
final newList = state.sequences.map((s) {
|
||||
if (s.docType == docType) {
|
||||
return s.copyWith(prefix: prefix, nextValue: nextValue);
|
||||
}
|
||||
return s;
|
||||
}).toList();
|
||||
emit(DocumentSequenceState(sequences: newList));
|
||||
}
|
||||
|
||||
Future<void> saveSequences() async {
|
||||
try {
|
||||
for (var seq in state.sequences) {
|
||||
await _supabase.from('document_sequences').upsert({
|
||||
'company_id': companyId,
|
||||
'doc_type': seq.docType,
|
||||
'next_value': seq.nextValue,
|
||||
'prefix': seq.prefix,
|
||||
});
|
||||
}
|
||||
// Opzionale: mostra un feedback di successo
|
||||
} catch (e) {
|
||||
emit(
|
||||
DocumentSequenceState(
|
||||
sequences: state.sequences,
|
||||
error: "Errore nel salvataggio",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:flux/features/settings/document_sequence/models/document_sequence_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
|
||||
class DocumentSequenceRepository {
|
||||
final _supabase = GetIt.I.get<SupabaseClient>();
|
||||
|
||||
Future<List<DocumentSequence>> getDocumentSequences(String companyId) async {
|
||||
final response = await _supabase
|
||||
.from('document_sequences')
|
||||
.select()
|
||||
.eq('company_id', companyId);
|
||||
|
||||
return (response as List).map((e) => DocumentSequence.fromMap(e)).toList();
|
||||
}
|
||||
|
||||
Future<void> updateSequence({
|
||||
required String companyId,
|
||||
required String docType,
|
||||
required int nextValue,
|
||||
required String prefix,
|
||||
}) async {
|
||||
await _supabase.from('document_sequences').upsert({
|
||||
'company_id': companyId,
|
||||
'doc_type': docType,
|
||||
'next_value': nextValue,
|
||||
'prefix': prefix,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
enum DocumentType { ticket, ddt, invoice }
|
||||
|
||||
class DocumentSequence {
|
||||
final String docType;
|
||||
final int nextValue;
|
||||
final String prefix;
|
||||
|
||||
DocumentSequence({
|
||||
required this.docType,
|
||||
required this.nextValue,
|
||||
required this.prefix,
|
||||
});
|
||||
|
||||
DocumentSequence copyWith({int? nextValue, String? prefix}) {
|
||||
return DocumentSequence(
|
||||
docType: docType,
|
||||
nextValue: nextValue ?? this.nextValue,
|
||||
prefix: prefix ?? this.prefix,
|
||||
);
|
||||
}
|
||||
|
||||
factory DocumentSequence.fromMap(Map<String, dynamic> map) {
|
||||
return DocumentSequence(
|
||||
docType: map['doc_type'],
|
||||
nextValue: map['next_value'],
|
||||
prefix: map['prefix'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/settings/document_sequence/blocs/document_sequence_cubit.dart';
|
||||
|
||||
class DocumentSequenceSection extends StatelessWidget {
|
||||
const DocumentSequenceSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final year = DateTime.now().year;
|
||||
|
||||
return BlocBuilder<DocumentSequenceCubit, DocumentSequenceState>(
|
||||
builder: (context, state) {
|
||||
if (state.isLoading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
child: Text(
|
||||
"Protocolli e Numerazione",
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
...state.sequences.map((seq) {
|
||||
// Anteprima dinamica
|
||||
final preview =
|
||||
"${seq.prefix.isNotEmpty ? '${seq.prefix}-' : ''}$year-${seq.nextValue.toString().padLeft(6, '0')}";
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
seq.docType.toUpperCase(),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blue,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: TextFormField(
|
||||
initialValue: seq.prefix,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Prefisso',
|
||||
hintText: 'es. TCK',
|
||||
),
|
||||
onChanged: (val) => context
|
||||
.read<DocumentSequenceCubit>()
|
||||
.updateLocalSequence(
|
||||
seq.docType,
|
||||
prefix: val,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: TextFormField(
|
||||
initialValue: seq.nextValue.toString(),
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Prossimo Numero',
|
||||
),
|
||||
onChanged: (val) => context
|
||||
.read<DocumentSequenceCubit>()
|
||||
.updateLocalSequence(
|
||||
seq.docType,
|
||||
nextValue: int.tryParse(val) ?? 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.visibility,
|
||||
size: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
"Anteprima prossimo: ",
|
||||
style: TextStyle(
|
||||
color: Colors.grey.shade700,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
preview,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () =>
|
||||
context.read<DocumentSequenceCubit>().saveSequences(),
|
||||
icon: const Icon(Icons.save),
|
||||
label: const Text("SALVA PROTOCOLLI"),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -153,8 +153,12 @@ class TicketFormCubit extends Cubit<TicketFormState> {
|
||||
if (ticketToSave.customerId == null || ticketToSave.customerId!.isEmpty) {
|
||||
throw Exception("Seleziona un cliente prima di salvare.");
|
||||
}
|
||||
|
||||
final savedTicket = await _repository.saveTicket(ticketToSave);
|
||||
TicketModel? savedTicket;
|
||||
if (ticketToSave.id == null) {
|
||||
savedTicket = await _repository.insertTicket(ticketToSave);
|
||||
} else {
|
||||
savedTicket = await _repository.updateTicket(ticketToSave);
|
||||
}
|
||||
|
||||
if (keepAdding) {
|
||||
emit(
|
||||
@@ -198,7 +202,7 @@ class TicketFormCubit extends Cubit<TicketFormState> {
|
||||
throw Exception("Seleziona un cliente prima di poter usare il QR.");
|
||||
}
|
||||
|
||||
final savedTicket = await _repository.saveTicket(ticketToSave);
|
||||
final savedTicket = await _repository.insertTicket(ticketToSave);
|
||||
|
||||
// Aggiorniamo silenziosamente lo stato con il ticket che ora ha un ID!
|
||||
emit(state.copyWith(ticket: savedTicket, status: TicketFormStatus.ready));
|
||||
|
||||
@@ -192,12 +192,42 @@ class TicketRepository {
|
||||
}
|
||||
}
|
||||
|
||||
/// Salva il ticket con upsert
|
||||
Future<TicketModel> saveTicket(TicketModel ticket) async {
|
||||
Future<String> generateTicketReference(String companyId) async {
|
||||
final response = await Supabase.instance.client.rpc(
|
||||
'get_next_document_number',
|
||||
params: {'p_company_id': companyId, 'p_doc_type': 'ticket'},
|
||||
);
|
||||
|
||||
// Estraiamo i dati dal JSON
|
||||
final int nextValue = response['next_value'];
|
||||
final String prefix = response['prefix'] ?? '';
|
||||
|
||||
final year = DateTime.now().year; // 2026
|
||||
|
||||
// Formattazione con zeri iniziali (es. 000125)
|
||||
final paddedNumber = nextValue.toString().padLeft(6, '0');
|
||||
|
||||
// Costruiamo la stringa. Se c'è un prefisso mette "TCK-2026-000125",
|
||||
// altrimenti solo "2026-000125"
|
||||
if (prefix.isNotEmpty) {
|
||||
return '$prefix-$year-$paddedNumber';
|
||||
} else {
|
||||
return '$year-$paddedNumber';
|
||||
}
|
||||
}
|
||||
|
||||
/// Salva il ticket
|
||||
Future<TicketModel> insertTicket(TicketModel ticket) async {
|
||||
if (ticket.id != null) {
|
||||
throw Exception('Impossibile creare un ticket esistente, id not null');
|
||||
}
|
||||
try {
|
||||
final ticketToSave = ticket.copyWith(
|
||||
referenceId: await generateTicketReference(ticket.companyId),
|
||||
);
|
||||
final response = await _supabase
|
||||
.from(_tableName)
|
||||
.upsert(ticket.toMap())
|
||||
.insert(ticketToSave.toMap())
|
||||
.select()
|
||||
.single();
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ class TicketModel extends Equatable {
|
||||
final WarrantyType? warrantyType;
|
||||
final String? publicNotes;
|
||||
final String? internalNotes;
|
||||
final int? referenceNumber;
|
||||
final String? referenceId;
|
||||
final String? alternativePhoneNumber;
|
||||
final bool hasCourtesyDevice;
|
||||
final TicketType ticketType;
|
||||
@@ -106,7 +106,6 @@ class TicketModel extends Equatable {
|
||||
final DateTime? estimatedDeliveryAt;
|
||||
final TicketResult? ticketResult;
|
||||
final String? resolutionNotes;
|
||||
final String? legacyId;
|
||||
final String? customerName;
|
||||
final String? targetModelName;
|
||||
final String? sourceModelName;
|
||||
@@ -134,7 +133,7 @@ class TicketModel extends Equatable {
|
||||
this.warrantyType,
|
||||
this.publicNotes,
|
||||
this.internalNotes,
|
||||
this.referenceNumber,
|
||||
this.referenceId,
|
||||
this.alternativePhoneNumber,
|
||||
this.hasCourtesyDevice = false,
|
||||
required this.ticketType,
|
||||
@@ -142,7 +141,6 @@ class TicketModel extends Equatable {
|
||||
this.estimatedDeliveryAt,
|
||||
this.ticketResult,
|
||||
this.resolutionNotes,
|
||||
this.legacyId,
|
||||
this.customerName,
|
||||
this.targetModelName,
|
||||
this.sourceModelName,
|
||||
@@ -185,7 +183,7 @@ class TicketModel extends Equatable {
|
||||
WarrantyType? warrantyType,
|
||||
String? publicNotes,
|
||||
String? internalNotes,
|
||||
int? referenceNumber,
|
||||
String? referenceId,
|
||||
String? alternativePhoneNumber,
|
||||
bool? hasCourtesyDevice,
|
||||
TicketType? ticketType,
|
||||
@@ -193,7 +191,6 @@ class TicketModel extends Equatable {
|
||||
DateTime? estimatedDeliveryAt,
|
||||
TicketResult? ticketResult,
|
||||
String? resolutionNotes,
|
||||
String? legacyId,
|
||||
String? customerName,
|
||||
String? targetModelName,
|
||||
String? sourceModelName,
|
||||
@@ -221,7 +218,7 @@ class TicketModel extends Equatable {
|
||||
warrantyType: warrantyType ?? this.warrantyType,
|
||||
publicNotes: publicNotes ?? this.publicNotes,
|
||||
internalNotes: internalNotes ?? this.internalNotes,
|
||||
referenceNumber: referenceNumber ?? this.referenceNumber,
|
||||
referenceId: referenceId ?? this.referenceId,
|
||||
alternativePhoneNumber:
|
||||
alternativePhoneNumber ?? this.alternativePhoneNumber,
|
||||
hasCourtesyDevice: hasCourtesyDevice ?? this.hasCourtesyDevice,
|
||||
@@ -230,7 +227,6 @@ class TicketModel extends Equatable {
|
||||
estimatedDeliveryAt: estimatedDeliveryAt ?? this.estimatedDeliveryAt,
|
||||
ticketResult: ticketResult ?? this.ticketResult,
|
||||
resolutionNotes: resolutionNotes ?? this.resolutionNotes,
|
||||
legacyId: legacyId ?? this.legacyId,
|
||||
customerName: customerName ?? this.customerName,
|
||||
targetModelName: targetModelName ?? this.targetModelName,
|
||||
sourceModelName: sourceModelName ?? this.sourceModelName,
|
||||
@@ -269,7 +265,7 @@ class TicketModel extends Equatable {
|
||||
warrantyType: WarrantyType.fromString(map['warranty_type'] as String?),
|
||||
publicNotes: map['public_notes'] as String?,
|
||||
internalNotes: map['internal_notes'] as String?,
|
||||
referenceNumber: map['reference_number'] as int?,
|
||||
referenceId: map['reference_id'] as String?,
|
||||
alternativePhoneNumber: map['alternative_phone_number'] as String?,
|
||||
hasCourtesyDevice: map['has_courtesy_device'] as bool? ?? false,
|
||||
ticketType: TicketType.fromString(map['ticket_type'] as String),
|
||||
@@ -279,7 +275,6 @@ class TicketModel extends Equatable {
|
||||
: null,
|
||||
ticketResult: TicketResult.fromString(map['ticket_result'] as String?),
|
||||
resolutionNotes: map['resolution_notes'] as String?,
|
||||
legacyId: map['legacy_id'] as String?,
|
||||
customerName: (map['customer']?['name'] as String?).myFormat(),
|
||||
targetModelName: (map['target_model']?['name_with_brand'] as String?)
|
||||
?.myFormat(),
|
||||
@@ -314,6 +309,7 @@ class TicketModel extends Equatable {
|
||||
'warranty_type': warrantyType,
|
||||
'public_notes': publicNotes,
|
||||
'internal_notes': internalNotes,
|
||||
'reference_id': referenceId,
|
||||
'alternative_phone_number': alternativePhoneNumber,
|
||||
'has_courtesy_device': hasCourtesyDevice,
|
||||
'ticket_type': ticketType.value,
|
||||
@@ -322,7 +318,6 @@ class TicketModel extends Equatable {
|
||||
'estimated_delivery_at': estimatedDeliveryAt!.toUtc().toIso8601String(),
|
||||
if (ticketResult != null) 'ticket_result': ticketResult!.value,
|
||||
'resolution_notes': resolutionNotes,
|
||||
'legacy_id': legacyId,
|
||||
'included_accessories': includedAccessories,
|
||||
};
|
||||
}
|
||||
@@ -346,7 +341,6 @@ class TicketModel extends Equatable {
|
||||
warrantyType,
|
||||
publicNotes,
|
||||
internalNotes,
|
||||
referenceNumber,
|
||||
alternativePhoneNumber,
|
||||
hasCourtesyDevice,
|
||||
ticketType,
|
||||
@@ -354,7 +348,6 @@ class TicketModel extends Equatable {
|
||||
estimatedDeliveryAt,
|
||||
ticketResult,
|
||||
resolutionNotes,
|
||||
legacyId,
|
||||
includedAccessories,
|
||||
customerName,
|
||||
targetModelName,
|
||||
|
||||
342
lib/features/tickets/utils/ticket_pdf_service.dart
Normal file
342
lib/features/tickets/utils/ticket_pdf_service.dart
Normal file
@@ -0,0 +1,342 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:pdf/pdf.dart';
|
||||
import 'package:pdf/widgets.dart' as pw;
|
||||
import 'package:printing/printing.dart';
|
||||
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||
import 'package:flux/features/company/models/company_model.dart';
|
||||
|
||||
class TicketPdfService {
|
||||
/// Funzione principale: Genera il PDF A4 con le due metà
|
||||
Future<Uint8List> generateTicketReceipt(
|
||||
TicketModel ticket,
|
||||
CompanyModel company,
|
||||
) async {
|
||||
final pdf = pw.Document();
|
||||
|
||||
// Carichiamo il font per essere sicuri che i caratteri siano ok
|
||||
final font = await PdfGoogleFonts.robotoRegular();
|
||||
final boldFont = await PdfGoogleFonts.robotoBold();
|
||||
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
pageFormat: PdfPageFormat.a4,
|
||||
margin: const pw.EdgeInsets.all(20),
|
||||
build: (context) {
|
||||
return pw.Column(
|
||||
children: [
|
||||
// 1. METÀ SUPERIORE: CLIENTE
|
||||
_buildTicketHalf(
|
||||
ticket,
|
||||
company,
|
||||
font,
|
||||
boldFont,
|
||||
isForCustomer: true,
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 10),
|
||||
|
||||
// Linea tratteggiata per il taglio
|
||||
pw.Container(
|
||||
margin: const pw.EdgeInsets.symmetric(vertical: 10),
|
||||
child: pw.Text(
|
||||
'-' * 100,
|
||||
style: const pw.TextStyle(color: PdfColors.grey400),
|
||||
),
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 10),
|
||||
|
||||
// 2. METÀ INFERIORE: NEGOZIO
|
||||
_buildTicketHalf(
|
||||
ticket,
|
||||
company,
|
||||
font,
|
||||
boldFont,
|
||||
isForCustomer: false,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return pdf.save();
|
||||
}
|
||||
|
||||
/// Helper per costruire una singola metà (Cliente o Negozio)
|
||||
pw.Widget _buildTicketHalf(
|
||||
TicketModel ticket,
|
||||
CompanyModel company,
|
||||
pw.Font font,
|
||||
pw.Font boldFont, {
|
||||
required bool isForCustomer,
|
||||
}) {
|
||||
return pw.Container(
|
||||
height: 380, // Circa metà A4 meno i margini
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
// HEADER: Logo e Dati Azienda (Solo per cliente o ID per negozio)
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text(
|
||||
company.name,
|
||||
style: pw.TextStyle(font: boldFont, fontSize: 16),
|
||||
),
|
||||
if (isForCustomer) ...[
|
||||
pw.Text(
|
||||
"${company.address}, ${company.city}",
|
||||
style: const pw.TextStyle(fontSize: 10),
|
||||
),
|
||||
pw.Text(
|
||||
"P.IVA: ${company.vatId}",
|
||||
style: const pw.TextStyle(fontSize: 10),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
||||
children: [
|
||||
pw.Text(
|
||||
isForCustomer
|
||||
? "RICEVUTA CLIENTE"
|
||||
: "COPIA INTERNA NEGOZIO",
|
||||
style: pw.TextStyle(
|
||||
font: boldFont,
|
||||
fontSize: 12,
|
||||
color: PdfColors.grey700,
|
||||
),
|
||||
),
|
||||
pw.Text(
|
||||
"Rif: ${ticket.referenceId}",
|
||||
style: pw.TextStyle(font: boldFont, fontSize: 14),
|
||||
),
|
||||
pw.Text(
|
||||
"Data: ${ticket.createdAt?.toString().substring(0, 10) ?? ''}",
|
||||
style: const pw.TextStyle(fontSize: 10),
|
||||
),
|
||||
],
|
||||
),
|
||||
pw.SizedBox(width: 10),
|
||||
// IL NOSTRO QR CODE MAGICO
|
||||
pw.BarcodeWidget(
|
||||
barcode: pw.Barcode.qrCode(),
|
||||
data: ticket.id!, // Salviamo l'ID univoco nel QR!
|
||||
width: 45,
|
||||
height: 45,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
pw.Divider(thickness: 1),
|
||||
pw.SizedBox(height: 10),
|
||||
|
||||
// DATI CLIENTE
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: _infoBlock(
|
||||
"CLIENTE",
|
||||
ticket.customerName ?? 'Cliente Sconosciuto',
|
||||
font,
|
||||
boldFont,
|
||||
),
|
||||
),
|
||||
pw.Expanded(
|
||||
child: _infoBlock(
|
||||
"CONTATTO ALTERNATIVO",
|
||||
ticket.alternativePhoneNumber ?? 'N/D',
|
||||
font,
|
||||
boldFont,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 15),
|
||||
|
||||
// DETTAGLI LAVORAZIONE
|
||||
_infoBlock(
|
||||
"DESCRIZIONE PROBLEMA / LAVORAZIONE RICHIESTA",
|
||||
ticket.request,
|
||||
font,
|
||||
boldFont,
|
||||
),
|
||||
pw.SizedBox(height: 8),
|
||||
|
||||
pw.Row(
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: _infoBlock(
|
||||
"ACCESSORI CONSEGNATI",
|
||||
ticket.includedAccessories ?? 'Nessuno',
|
||||
font,
|
||||
boldFont,
|
||||
),
|
||||
),
|
||||
pw.Expanded(
|
||||
child: _infoBlock(
|
||||
"GARANZIA",
|
||||
ticket.warrantyType?.displayValue ?? 'Standard',
|
||||
font,
|
||||
boldFont,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
pw.SizedBox(height: 15),
|
||||
|
||||
// NOTE (Pubbliche o Private a seconda della copia)
|
||||
if (isForCustomer)
|
||||
_infoBlock("NOTE", ticket.publicNotes ?? '-', font, boldFont)
|
||||
else
|
||||
_infoBlock(
|
||||
"NOTE INTERNE (PRIVATE)",
|
||||
ticket.internalNotes ?? '-',
|
||||
font,
|
||||
boldFont,
|
||||
),
|
||||
|
||||
pw.Spacer(),
|
||||
|
||||
// FOOTER: Disclaimer e Firma
|
||||
if (!isForCustomer) ...[
|
||||
pw.Text(
|
||||
"CONDIZIONI E LIBERATORIA:",
|
||||
style: pw.TextStyle(font: boldFont, fontSize: 8),
|
||||
),
|
||||
pw.Text(
|
||||
company.ticketDisclaimer ??
|
||||
'Firma per accettazione delle condizioni di riparazione.',
|
||||
style: const pw.TextStyle(fontSize: 7),
|
||||
textAlign: pw.TextAlign.justify,
|
||||
),
|
||||
pw.SizedBox(height: 20),
|
||||
pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Container(
|
||||
width: 150,
|
||||
decoration: pw.BoxDecoration(
|
||||
border: const pw.Border(top: pw.BorderSide(width: 0.5)),
|
||||
),
|
||||
),
|
||||
pw.Text(
|
||||
"Firma del Cliente per accettazione",
|
||||
style: const pw.TextStyle(fontSize: 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
] else
|
||||
pw.Align(
|
||||
alignment: pw.Alignment.centerRight,
|
||||
child: pw.Text(
|
||||
"Grazie per averci scelto!",
|
||||
style: pw.TextStyle(
|
||||
font: font,
|
||||
fontSize: 10,
|
||||
fontStyle: pw.FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
pw.Widget _infoBlock(
|
||||
String label,
|
||||
String value,
|
||||
pw.Font font,
|
||||
pw.Font boldFont,
|
||||
) {
|
||||
return pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
children: [
|
||||
pw.Text(
|
||||
label,
|
||||
style: pw.TextStyle(
|
||||
font: boldFont,
|
||||
fontSize: 8,
|
||||
color: PdfColors.grey600,
|
||||
),
|
||||
),
|
||||
pw.Text(value, style: pw.TextStyle(font: font, fontSize: 11)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<Uint8List> generateLabelPdf(
|
||||
TicketModel ticket,
|
||||
CompanyModel company,
|
||||
) async {
|
||||
final pdf = pw.Document();
|
||||
final font = await PdfGoogleFonts.robotoRegular();
|
||||
final boldFont = await PdfGoogleFonts.robotoBold();
|
||||
|
||||
// Prendiamo le misure salvate (se custom) o usiamo default
|
||||
final widthMm = company.labelWidth ?? 62.0;
|
||||
final heightMm = company.labelHeight ?? 29.0;
|
||||
|
||||
// Creiamo il formato fisico esatto!
|
||||
final format = company.isLabelVertical
|
||||
? PdfPageFormat(heightMm * PdfPageFormat.mm, widthMm * PdfPageFormat.mm)
|
||||
: PdfPageFormat(
|
||||
widthMm * PdfPageFormat.mm,
|
||||
heightMm * PdfPageFormat.mm,
|
||||
);
|
||||
|
||||
pdf.addPage(
|
||||
pw.Page(
|
||||
pageFormat: format,
|
||||
margin: const pw.EdgeInsets.all(2), // Margini minimi per le etichette
|
||||
build: (context) {
|
||||
return pw.Row(
|
||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
pw.Expanded(
|
||||
child: pw.Column(
|
||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||
mainAxisAlignment: pw.MainAxisAlignment.center,
|
||||
children: [
|
||||
pw.Text(
|
||||
ticket.referenceId ?? '',
|
||||
style: pw.TextStyle(font: boldFont, fontSize: 10),
|
||||
),
|
||||
pw.Text(
|
||||
ticket.customerName ?? 'Cliente sconosciuto',
|
||||
style: pw.TextStyle(font: font, fontSize: 9),
|
||||
),
|
||||
pw.Text(
|
||||
ticket.createdAt?.toString().substring(0, 10) ?? '',
|
||||
style: const pw.TextStyle(fontSize: 7),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// QR Code compatto
|
||||
pw.BarcodeWidget(
|
||||
barcode: pw.Barcode.qrCode(),
|
||||
data: ticket.id!,
|
||||
width: 20,
|
||||
height: 20,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return pdf.save();
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:flux/features/auth/bloc/auth_cubit.dart';
|
||||
import 'package:flux/features/company/data/company_repository.dart';
|
||||
import 'package:flux/features/operations/blocs/operation_list_cubit.dart';
|
||||
import 'package:flux/features/operations/data/operations_repository.dart';
|
||||
import 'package:flux/features/settings/document_sequence/data/document_sequence_repository.dart';
|
||||
import 'package:flux/features/tickets/data/ticket_repository.dart';
|
||||
import 'package:flux/l10n/app_localizations.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
@@ -97,6 +98,9 @@ Future<void> setupLocator() async {
|
||||
() => AttachmentsRepository(),
|
||||
);
|
||||
getIt.registerLazySingleton<TicketRepository>(() => TicketRepository());
|
||||
getIt.registerLazySingleton<DocumentSequenceRepository>(
|
||||
() => DocumentSequenceRepository(),
|
||||
);
|
||||
|
||||
// NOTA: CompanyRepository l'ho tolto perché la logica della Company
|
||||
// ora è gestita dal CoreRepository durante l'Onboarding.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <printing/printing_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
@@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||
g_autoptr(FlPluginRegistrar) printing_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
|
||||
printing_plugin_register_with_registrar(printing_registrar);
|
||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
gtk
|
||||
printing
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import app_links
|
||||
import file_picker
|
||||
import file_selector_macos
|
||||
import pdfx
|
||||
import printing
|
||||
import shared_preferences_foundation
|
||||
import url_launcher_macos
|
||||
|
||||
@@ -17,6 +18,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||
PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin"))
|
||||
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
}
|
||||
|
||||
16
pubspec.lock
16
pubspec.lock
@@ -677,6 +677,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.12.0"
|
||||
pdf_widget_wrapper:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pdf_widget_wrapper
|
||||
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
pdfx:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -789,6 +797,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.7.0"
|
||||
printing:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: printing
|
||||
sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.14.3"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -33,6 +33,7 @@ dependencies:
|
||||
uuid: ^4.5.3
|
||||
pdf: ^3.12.0
|
||||
universal_io: ^2.3.1
|
||||
printing: ^5.14.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <file_selector_windows/file_selector_windows.h>
|
||||
#include <pdfx/pdfx_plugin.h>
|
||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||
#include <printing/printing_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
@@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("PdfxPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
PrintingPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
pdfx
|
||||
permission_handler_windows
|
||||
printing
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user