ticket labels e ticket receipt
This commit is contained in:
@@ -37,6 +37,11 @@ class CompanySettingsCubit extends Cubit<CompanySettingsState> {
|
|||||||
String? zipCode,
|
String? zipCode,
|
||||||
String? phone,
|
String? phone,
|
||||||
String? email,
|
String? email,
|
||||||
|
String? ticketDisclaimer,
|
||||||
|
LabelFormat? labelFormat,
|
||||||
|
double? labelWidth,
|
||||||
|
double? labelHeight,
|
||||||
|
bool? isVertical,
|
||||||
}) {
|
}) {
|
||||||
if (state.company == null) return;
|
if (state.company == null) return;
|
||||||
|
|
||||||
@@ -51,6 +56,11 @@ class CompanySettingsCubit extends Cubit<CompanySettingsState> {
|
|||||||
zipCode: zipCode ?? state.company!.zipCode,
|
zipCode: zipCode ?? state.company!.zipCode,
|
||||||
phone: phone ?? state.company!.phone,
|
phone: phone ?? state.company!.phone,
|
||||||
email: email ?? state.company!.email,
|
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));
|
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
|
// IL MODELLO ESATTO
|
||||||
// ===================================================================
|
// ===================================================================
|
||||||
@@ -56,7 +71,11 @@ class CompanyModel extends Equatable {
|
|||||||
final String? phone;
|
final String? phone;
|
||||||
final String? email;
|
final String? email;
|
||||||
final String? logoUrl;
|
final String? logoUrl;
|
||||||
|
final String? ticketDisclaimer;
|
||||||
|
final LabelFormat labelFormat;
|
||||||
|
final double? labelWidth;
|
||||||
|
final double? labelHeight;
|
||||||
|
final bool isLabelVertical;
|
||||||
// Stato Pagamenti (Ibride: manuale + Stripe)
|
// Stato Pagamenti (Ibride: manuale + Stripe)
|
||||||
final bool isPaid;
|
final bool isPaid;
|
||||||
final DateTime? paymentExpiration;
|
final DateTime? paymentExpiration;
|
||||||
@@ -83,6 +102,11 @@ class CompanyModel extends Equatable {
|
|||||||
this.phone,
|
this.phone,
|
||||||
this.email,
|
this.email,
|
||||||
this.logoUrl,
|
this.logoUrl,
|
||||||
|
this.ticketDisclaimer,
|
||||||
|
this.labelFormat = LabelFormat.none,
|
||||||
|
this.labelWidth,
|
||||||
|
this.labelHeight,
|
||||||
|
this.isLabelVertical = false,
|
||||||
this.isPaid = false,
|
this.isPaid = false,
|
||||||
this.paymentExpiration,
|
this.paymentExpiration,
|
||||||
this.subscriptionTier = SubscriptionTier.free,
|
this.subscriptionTier = SubscriptionTier.free,
|
||||||
@@ -105,6 +129,11 @@ class CompanyModel extends Equatable {
|
|||||||
String? fiscalCode,
|
String? fiscalCode,
|
||||||
String? sdi,
|
String? sdi,
|
||||||
String? logoUrl,
|
String? logoUrl,
|
||||||
|
String? ticketDisclaimer,
|
||||||
|
LabelFormat? labelFormat,
|
||||||
|
double? labelWidth,
|
||||||
|
double? labelHeight,
|
||||||
|
bool? isLabelVertical,
|
||||||
String? phone,
|
String? phone,
|
||||||
String? email,
|
String? email,
|
||||||
bool? isPaid,
|
bool? isPaid,
|
||||||
@@ -130,6 +159,11 @@ class CompanyModel extends Equatable {
|
|||||||
logoUrl: logoUrl ?? this.logoUrl,
|
logoUrl: logoUrl ?? this.logoUrl,
|
||||||
phone: phone ?? this.phone,
|
phone: phone ?? this.phone,
|
||||||
email: email ?? this.email,
|
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,
|
isPaid: isPaid ?? this.isPaid,
|
||||||
paymentExpiration: paymentExpiration ?? this.paymentExpiration,
|
paymentExpiration: paymentExpiration ?? this.paymentExpiration,
|
||||||
subscriptionTier: subscriptionTier ?? this.subscriptionTier,
|
subscriptionTier: subscriptionTier ?? this.subscriptionTier,
|
||||||
@@ -171,9 +205,18 @@ class CompanyModel extends Equatable {
|
|||||||
vatId: map['vat_id'] ?? '',
|
vatId: map['vat_id'] ?? '',
|
||||||
fiscalCode: map['fiscal_code'] ?? '',
|
fiscalCode: map['fiscal_code'] ?? '',
|
||||||
sdi: map['sdi'] ?? '',
|
sdi: map['sdi'] ?? '',
|
||||||
logoUrl: map['company_logo'],
|
logoUrl: map['logo_url'],
|
||||||
phone: map['phone'] ?? '',
|
phone: map['phone'] ?? '',
|
||||||
email: map['email'] ?? '',
|
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,
|
isPaid: map['is_paid'] ?? false,
|
||||||
paymentExpiration: map['payment_expiration'] != null
|
paymentExpiration: map['payment_expiration'] != null
|
||||||
? DateTime.tryParse(map['payment_expiration'])
|
? DateTime.tryParse(map['payment_expiration'])
|
||||||
@@ -203,9 +246,14 @@ class CompanyModel extends Equatable {
|
|||||||
'vat_id': vatId,
|
'vat_id': vatId,
|
||||||
'fiscal_code': fiscalCode,
|
'fiscal_code': fiscalCode,
|
||||||
'sdi': sdi,
|
'sdi': sdi,
|
||||||
'company_logo': logoUrl,
|
'logo_url': logoUrl,
|
||||||
'phone': phone,
|
'phone': phone,
|
||||||
'email': email,
|
'email': email,
|
||||||
|
'ticket_disclaimer': ticketDisclaimer,
|
||||||
|
'label_format': labelFormat.name,
|
||||||
|
'label_width': labelWidth,
|
||||||
|
'label_height': labelHeight,
|
||||||
|
'is_label_vertical': isLabelVertical,
|
||||||
'is_paid': isPaid,
|
'is_paid': isPaid,
|
||||||
if (paymentExpiration != null)
|
if (paymentExpiration != null)
|
||||||
'payment_expiration': paymentExpiration!.toIso8601String(),
|
'payment_expiration': paymentExpiration!.toIso8601String(),
|
||||||
@@ -236,6 +284,11 @@ class CompanyModel extends Equatable {
|
|||||||
logoUrl,
|
logoUrl,
|
||||||
phone,
|
phone,
|
||||||
email,
|
email,
|
||||||
|
ticketDisclaimer,
|
||||||
|
labelFormat,
|
||||||
|
labelWidth,
|
||||||
|
labelHeight,
|
||||||
|
isLabelVertical,
|
||||||
isPaid,
|
isPaid,
|
||||||
paymentExpiration,
|
paymentExpiration,
|
||||||
subscriptionTier,
|
subscriptionTier,
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/features/company/bloc/company_settings_cubit.dart';
|
import 'package:flux/features/company/bloc/company_settings_cubit.dart';
|
||||||
import 'package:flux/features/company/models/company_model.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';
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
class CompanySettingsScreen extends StatefulWidget {
|
class CompanySettingsScreen extends StatefulWidget {
|
||||||
@@ -24,6 +26,7 @@ class _CompanySettingsScreenState extends State<CompanySettingsScreen> {
|
|||||||
final _zipCtrl = TextEditingController();
|
final _zipCtrl = TextEditingController();
|
||||||
final _phoneCtrl = TextEditingController();
|
final _phoneCtrl = TextEditingController();
|
||||||
final _emailCtrl = TextEditingController();
|
final _emailCtrl = TextEditingController();
|
||||||
|
final _disclaimerCtrl = TextEditingController();
|
||||||
|
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
|
||||||
@@ -50,6 +53,8 @@ class _CompanySettingsScreenState extends State<CompanySettingsScreen> {
|
|||||||
_zipCtrl.dispose();
|
_zipCtrl.dispose();
|
||||||
_phoneCtrl.dispose();
|
_phoneCtrl.dispose();
|
||||||
_emailCtrl.dispose();
|
_emailCtrl.dispose();
|
||||||
|
_disclaimerCtrl.dispose();
|
||||||
|
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,6 +74,9 @@ class _CompanySettingsScreenState extends State<CompanySettingsScreen> {
|
|||||||
if (_phoneCtrl.text.isEmpty) _phoneCtrl.text = company.phone ?? '';
|
if (_phoneCtrl.text.isEmpty) _phoneCtrl.text = company.phone ?? '';
|
||||||
if (_emailCtrl.text.isEmpty) _emailCtrl.text = company.email ?? '';
|
if (_emailCtrl.text.isEmpty) _emailCtrl.text = company.email ?? '';
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
|
if (_disclaimerCtrl.text.isEmpty) {
|
||||||
|
_disclaimerCtrl.text = company.ticketDisclaimer ?? '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _flushToCubit() {
|
void _flushToCubit() {
|
||||||
@@ -83,6 +91,7 @@ class _CompanySettingsScreenState extends State<CompanySettingsScreen> {
|
|||||||
zipCode: _zipCtrl.text,
|
zipCode: _zipCtrl.text,
|
||||||
phone: _phoneCtrl.text,
|
phone: _phoneCtrl.text,
|
||||||
email: _emailCtrl.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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(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),
|
const SizedBox(height: 48),
|
||||||
|
|
||||||
// --- PULSANTE SALVATAGGIO ---
|
// --- PULSANTE SALVATAGGIO ---
|
||||||
|
|||||||
@@ -217,46 +217,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
final isUltraWide = constraints.maxWidth > 1400;
|
final isUltraWide = constraints.maxWidth > 1400;
|
||||||
final isDesktop = constraints.maxWidth > 900;
|
final isDesktop = constraints.maxWidth > 900;
|
||||||
if (isUltraWide) {
|
if (isUltraWide) {
|
||||||
return Row(
|
return _buildUltraWide(state, theme);
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else if (isDesktop) {
|
} else if (isDesktop) {
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -365,48 +326,92 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMainFormContent(
|
Widget _buildUltraWide(OperationFormState state, ThemeData theme) {
|
||||||
ThemeData theme,
|
return Row(
|
||||||
OperationFormState state,
|
|
||||||
OperationStatus displayStatus, {
|
|
||||||
bool showFiles = true,
|
|
||||||
}) {
|
|
||||||
final currentOp = state.operation;
|
|
||||||
final currentType = currentOp.type;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
StaffSection(
|
Expanded(
|
||||||
staffId: currentOp.staffId,
|
flex: 4,
|
||||||
staffName: currentOp.staffDisplayName,
|
child: SingleChildScrollView(
|
||||||
onStaffSelected: (staff) => {
|
padding: const EdgeInsets.all(16.0),
|
||||||
context.read<OperationFormCubit>().updateFields(
|
child: Column(
|
||||||
staffId: staff.id,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
staffDisplayName: staff.name,
|
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'),
|
_buildSectionTitle('Esito / Stato Operazione'),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _getStatusColor(displayStatus).withValues(alpha: 0.1),
|
color: _getStatusColor(
|
||||||
|
state.operation.status,
|
||||||
|
).withValues(alpha: 0.1),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: _getStatusColor(displayStatus).withValues(alpha: 0.3),
|
color: _getStatusColor(
|
||||||
|
state.operation.status,
|
||||||
|
).withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: DropdownButtonHideUnderline(
|
child: DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<OperationStatus>(
|
child: DropdownButton<OperationStatus>(
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
value: displayStatus,
|
value: state.operation.status,
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
Icons.arrow_drop_down,
|
Icons.arrow_drop_down,
|
||||||
color: _getStatusColor(displayStatus),
|
color: _getStatusColor(state.operation.status),
|
||||||
),
|
),
|
||||||
items: OperationStatus.values
|
items: OperationStatus.values
|
||||||
/* .where(
|
/* .where(
|
||||||
@@ -450,34 +455,41 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
displayStatus == OperationStatus.success
|
state.operation.status == OperationStatus.success
|
||||||
? 'Lascia OK se la pratica è stata caricata con successo.'
|
? '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),
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||||
),
|
),
|
||||||
const Divider(height: 32),
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
//_buildSectionTitle('Cliente & Riferimento'),
|
Widget _buildCustomerSection(OperationFormState state) {
|
||||||
SharedCustomerSection(
|
return SharedCustomerSection(
|
||||||
customerId: currentOp.customerId,
|
customerId: state.operation.customerId,
|
||||||
customerName: currentOp.customerDisplayName,
|
customerName: state.operation.customerDisplayName,
|
||||||
onCustomerSelected: (customer) {
|
onCustomerSelected: (customer) {
|
||||||
context.read<OperationFormCubit>().updateFields(
|
context.read<OperationFormCubit>().updateFields(
|
||||||
customerId: customer.id,
|
customerId: customer.id,
|
||||||
customerDisplayName: customer.name,
|
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 _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?'),
|
_buildSectionTitle('Cosa stiamo facendo?'),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8.0,
|
spacing: 8.0,
|
||||||
@@ -485,7 +497,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
children: _availableTypes.map((type) {
|
children: _availableTypes.map((type) {
|
||||||
return ChoiceChip(
|
return ChoiceChip(
|
||||||
label: Text(type),
|
label: Text(type),
|
||||||
selected: currentType == type,
|
selected: state.operation.type == type,
|
||||||
onSelected: (selected) {
|
onSelected: (selected) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
context.read<OperationFormCubit>().setTypeWithSmartDefault(
|
context.read<OperationFormCubit>().setTypeWithSmartDefault(
|
||||||
@@ -496,63 +508,90 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
const Divider(height: 32),
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDetailsSection(OperationFormState state) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
_buildSectionTitle('Dettagli Servizio'),
|
_buildSectionTitle('Dettagli Servizio'),
|
||||||
DetailsSection(
|
DetailsSection(
|
||||||
currentOp: currentOp,
|
currentOp: state.operation,
|
||||||
currentType: currentType,
|
currentType: state.operation.type,
|
||||||
freeTextSubtypeController: _freeTextSubtypeController,
|
freeTextSubtypeController: _freeTextSubtypeController,
|
||||||
freeTextDescriptionController: _freeTextDescriptionController,
|
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À
|
// 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),
|
const Divider(height: 32),
|
||||||
|
|
||||||
if (showFiles) ...[
|
if (showFiles) ...[_buildAttachmentSection(state)],
|
||||||
SharedAttachmentsSection(
|
|
||||||
parentType: AttachmentParentType.operation,
|
|
||||||
parentId: currentOp.id,
|
|
||||||
titleForUpload:
|
|
||||||
state.operation.customerDisplayName ?? 'Nuova pratica',
|
|
||||||
onGenerateIdForQr: _generateIdForQr,
|
|
||||||
),
|
|
||||||
/* SharedFilesSection(
|
|
||||||
titleNameForUpload:
|
|
||||||
state.operation.customerDisplayName ?? 'Nuova pratica',
|
|
||||||
onGenerateIdForQr: _generateIdForQr,
|
|
||||||
), */
|
|
||||||
],
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
if (ticketToSave.customerId == null || ticketToSave.customerId!.isEmpty) {
|
||||||
throw Exception("Seleziona un cliente prima di salvare.");
|
throw Exception("Seleziona un cliente prima di salvare.");
|
||||||
}
|
}
|
||||||
|
TicketModel? savedTicket;
|
||||||
final savedTicket = await _repository.saveTicket(ticketToSave);
|
if (ticketToSave.id == null) {
|
||||||
|
savedTicket = await _repository.insertTicket(ticketToSave);
|
||||||
|
} else {
|
||||||
|
savedTicket = await _repository.updateTicket(ticketToSave);
|
||||||
|
}
|
||||||
|
|
||||||
if (keepAdding) {
|
if (keepAdding) {
|
||||||
emit(
|
emit(
|
||||||
@@ -198,7 +202,7 @@ class TicketFormCubit extends Cubit<TicketFormState> {
|
|||||||
throw Exception("Seleziona un cliente prima di poter usare il QR.");
|
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!
|
// Aggiorniamo silenziosamente lo stato con il ticket che ora ha un ID!
|
||||||
emit(state.copyWith(ticket: savedTicket, status: TicketFormStatus.ready));
|
emit(state.copyWith(ticket: savedTicket, status: TicketFormStatus.ready));
|
||||||
|
|||||||
@@ -192,12 +192,42 @@ class TicketRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Salva il ticket con upsert
|
Future<String> generateTicketReference(String companyId) async {
|
||||||
Future<TicketModel> saveTicket(TicketModel ticket) 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 {
|
try {
|
||||||
|
final ticketToSave = ticket.copyWith(
|
||||||
|
referenceId: await generateTicketReference(ticket.companyId),
|
||||||
|
);
|
||||||
final response = await _supabase
|
final response = await _supabase
|
||||||
.from(_tableName)
|
.from(_tableName)
|
||||||
.upsert(ticket.toMap())
|
.insert(ticketToSave.toMap())
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ class TicketModel extends Equatable {
|
|||||||
final WarrantyType? warrantyType;
|
final WarrantyType? warrantyType;
|
||||||
final String? publicNotes;
|
final String? publicNotes;
|
||||||
final String? internalNotes;
|
final String? internalNotes;
|
||||||
final int? referenceNumber;
|
final String? referenceId;
|
||||||
final String? alternativePhoneNumber;
|
final String? alternativePhoneNumber;
|
||||||
final bool hasCourtesyDevice;
|
final bool hasCourtesyDevice;
|
||||||
final TicketType ticketType;
|
final TicketType ticketType;
|
||||||
@@ -106,7 +106,6 @@ class TicketModel extends Equatable {
|
|||||||
final DateTime? estimatedDeliveryAt;
|
final DateTime? estimatedDeliveryAt;
|
||||||
final TicketResult? ticketResult;
|
final TicketResult? ticketResult;
|
||||||
final String? resolutionNotes;
|
final String? resolutionNotes;
|
||||||
final String? legacyId;
|
|
||||||
final String? customerName;
|
final String? customerName;
|
||||||
final String? targetModelName;
|
final String? targetModelName;
|
||||||
final String? sourceModelName;
|
final String? sourceModelName;
|
||||||
@@ -134,7 +133,7 @@ class TicketModel extends Equatable {
|
|||||||
this.warrantyType,
|
this.warrantyType,
|
||||||
this.publicNotes,
|
this.publicNotes,
|
||||||
this.internalNotes,
|
this.internalNotes,
|
||||||
this.referenceNumber,
|
this.referenceId,
|
||||||
this.alternativePhoneNumber,
|
this.alternativePhoneNumber,
|
||||||
this.hasCourtesyDevice = false,
|
this.hasCourtesyDevice = false,
|
||||||
required this.ticketType,
|
required this.ticketType,
|
||||||
@@ -142,7 +141,6 @@ class TicketModel extends Equatable {
|
|||||||
this.estimatedDeliveryAt,
|
this.estimatedDeliveryAt,
|
||||||
this.ticketResult,
|
this.ticketResult,
|
||||||
this.resolutionNotes,
|
this.resolutionNotes,
|
||||||
this.legacyId,
|
|
||||||
this.customerName,
|
this.customerName,
|
||||||
this.targetModelName,
|
this.targetModelName,
|
||||||
this.sourceModelName,
|
this.sourceModelName,
|
||||||
@@ -185,7 +183,7 @@ class TicketModel extends Equatable {
|
|||||||
WarrantyType? warrantyType,
|
WarrantyType? warrantyType,
|
||||||
String? publicNotes,
|
String? publicNotes,
|
||||||
String? internalNotes,
|
String? internalNotes,
|
||||||
int? referenceNumber,
|
String? referenceId,
|
||||||
String? alternativePhoneNumber,
|
String? alternativePhoneNumber,
|
||||||
bool? hasCourtesyDevice,
|
bool? hasCourtesyDevice,
|
||||||
TicketType? ticketType,
|
TicketType? ticketType,
|
||||||
@@ -193,7 +191,6 @@ class TicketModel extends Equatable {
|
|||||||
DateTime? estimatedDeliveryAt,
|
DateTime? estimatedDeliveryAt,
|
||||||
TicketResult? ticketResult,
|
TicketResult? ticketResult,
|
||||||
String? resolutionNotes,
|
String? resolutionNotes,
|
||||||
String? legacyId,
|
|
||||||
String? customerName,
|
String? customerName,
|
||||||
String? targetModelName,
|
String? targetModelName,
|
||||||
String? sourceModelName,
|
String? sourceModelName,
|
||||||
@@ -221,7 +218,7 @@ class TicketModel extends Equatable {
|
|||||||
warrantyType: warrantyType ?? this.warrantyType,
|
warrantyType: warrantyType ?? this.warrantyType,
|
||||||
publicNotes: publicNotes ?? this.publicNotes,
|
publicNotes: publicNotes ?? this.publicNotes,
|
||||||
internalNotes: internalNotes ?? this.internalNotes,
|
internalNotes: internalNotes ?? this.internalNotes,
|
||||||
referenceNumber: referenceNumber ?? this.referenceNumber,
|
referenceId: referenceId ?? this.referenceId,
|
||||||
alternativePhoneNumber:
|
alternativePhoneNumber:
|
||||||
alternativePhoneNumber ?? this.alternativePhoneNumber,
|
alternativePhoneNumber ?? this.alternativePhoneNumber,
|
||||||
hasCourtesyDevice: hasCourtesyDevice ?? this.hasCourtesyDevice,
|
hasCourtesyDevice: hasCourtesyDevice ?? this.hasCourtesyDevice,
|
||||||
@@ -230,7 +227,6 @@ class TicketModel extends Equatable {
|
|||||||
estimatedDeliveryAt: estimatedDeliveryAt ?? this.estimatedDeliveryAt,
|
estimatedDeliveryAt: estimatedDeliveryAt ?? this.estimatedDeliveryAt,
|
||||||
ticketResult: ticketResult ?? this.ticketResult,
|
ticketResult: ticketResult ?? this.ticketResult,
|
||||||
resolutionNotes: resolutionNotes ?? this.resolutionNotes,
|
resolutionNotes: resolutionNotes ?? this.resolutionNotes,
|
||||||
legacyId: legacyId ?? this.legacyId,
|
|
||||||
customerName: customerName ?? this.customerName,
|
customerName: customerName ?? this.customerName,
|
||||||
targetModelName: targetModelName ?? this.targetModelName,
|
targetModelName: targetModelName ?? this.targetModelName,
|
||||||
sourceModelName: sourceModelName ?? this.sourceModelName,
|
sourceModelName: sourceModelName ?? this.sourceModelName,
|
||||||
@@ -269,7 +265,7 @@ class TicketModel extends Equatable {
|
|||||||
warrantyType: WarrantyType.fromString(map['warranty_type'] as String?),
|
warrantyType: WarrantyType.fromString(map['warranty_type'] as String?),
|
||||||
publicNotes: map['public_notes'] as String?,
|
publicNotes: map['public_notes'] as String?,
|
||||||
internalNotes: map['internal_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?,
|
alternativePhoneNumber: map['alternative_phone_number'] as String?,
|
||||||
hasCourtesyDevice: map['has_courtesy_device'] as bool? ?? false,
|
hasCourtesyDevice: map['has_courtesy_device'] as bool? ?? false,
|
||||||
ticketType: TicketType.fromString(map['ticket_type'] as String),
|
ticketType: TicketType.fromString(map['ticket_type'] as String),
|
||||||
@@ -279,7 +275,6 @@ class TicketModel extends Equatable {
|
|||||||
: null,
|
: null,
|
||||||
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?,
|
||||||
legacyId: map['legacy_id'] as String?,
|
|
||||||
customerName: (map['customer']?['name'] as String?).myFormat(),
|
customerName: (map['customer']?['name'] as String?).myFormat(),
|
||||||
targetModelName: (map['target_model']?['name_with_brand'] as String?)
|
targetModelName: (map['target_model']?['name_with_brand'] as String?)
|
||||||
?.myFormat(),
|
?.myFormat(),
|
||||||
@@ -314,6 +309,7 @@ class TicketModel extends Equatable {
|
|||||||
'warranty_type': warrantyType,
|
'warranty_type': warrantyType,
|
||||||
'public_notes': publicNotes,
|
'public_notes': publicNotes,
|
||||||
'internal_notes': internalNotes,
|
'internal_notes': internalNotes,
|
||||||
|
'reference_id': referenceId,
|
||||||
'alternative_phone_number': alternativePhoneNumber,
|
'alternative_phone_number': alternativePhoneNumber,
|
||||||
'has_courtesy_device': hasCourtesyDevice,
|
'has_courtesy_device': hasCourtesyDevice,
|
||||||
'ticket_type': ticketType.value,
|
'ticket_type': ticketType.value,
|
||||||
@@ -322,7 +318,6 @@ class TicketModel extends Equatable {
|
|||||||
'estimated_delivery_at': estimatedDeliveryAt!.toUtc().toIso8601String(),
|
'estimated_delivery_at': estimatedDeliveryAt!.toUtc().toIso8601String(),
|
||||||
if (ticketResult != null) 'ticket_result': ticketResult!.value,
|
if (ticketResult != null) 'ticket_result': ticketResult!.value,
|
||||||
'resolution_notes': resolutionNotes,
|
'resolution_notes': resolutionNotes,
|
||||||
'legacy_id': legacyId,
|
|
||||||
'included_accessories': includedAccessories,
|
'included_accessories': includedAccessories,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -346,7 +341,6 @@ class TicketModel extends Equatable {
|
|||||||
warrantyType,
|
warrantyType,
|
||||||
publicNotes,
|
publicNotes,
|
||||||
internalNotes,
|
internalNotes,
|
||||||
referenceNumber,
|
|
||||||
alternativePhoneNumber,
|
alternativePhoneNumber,
|
||||||
hasCourtesyDevice,
|
hasCourtesyDevice,
|
||||||
ticketType,
|
ticketType,
|
||||||
@@ -354,7 +348,6 @@ class TicketModel extends Equatable {
|
|||||||
estimatedDeliveryAt,
|
estimatedDeliveryAt,
|
||||||
ticketResult,
|
ticketResult,
|
||||||
resolutionNotes,
|
resolutionNotes,
|
||||||
legacyId,
|
|
||||||
includedAccessories,
|
includedAccessories,
|
||||||
customerName,
|
customerName,
|
||||||
targetModelName,
|
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/company/data/company_repository.dart';
|
||||||
import 'package:flux/features/operations/blocs/operation_list_cubit.dart';
|
import 'package:flux/features/operations/blocs/operation_list_cubit.dart';
|
||||||
import 'package:flux/features/operations/data/operations_repository.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/features/tickets/data/ticket_repository.dart';
|
||||||
import 'package:flux/l10n/app_localizations.dart';
|
import 'package:flux/l10n/app_localizations.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
@@ -97,6 +98,9 @@ Future<void> setupLocator() async {
|
|||||||
() => AttachmentsRepository(),
|
() => AttachmentsRepository(),
|
||||||
);
|
);
|
||||||
getIt.registerLazySingleton<TicketRepository>(() => TicketRepository());
|
getIt.registerLazySingleton<TicketRepository>(() => TicketRepository());
|
||||||
|
getIt.registerLazySingleton<DocumentSequenceRepository>(
|
||||||
|
() => DocumentSequenceRepository(),
|
||||||
|
);
|
||||||
|
|
||||||
// NOTA: CompanyRepository l'ho tolto perché la logica della Company
|
// NOTA: CompanyRepository l'ho tolto perché la logica della Company
|
||||||
// ora è gestita dal CoreRepository durante l'Onboarding.
|
// ora è gestita dal CoreRepository durante l'Onboarding.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <gtk/gtk_plugin.h>
|
#include <gtk/gtk_plugin.h>
|
||||||
|
#include <printing/printing_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
@@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
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 =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
file_selector_linux
|
file_selector_linux
|
||||||
gtk
|
gtk
|
||||||
|
printing
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import app_links
|
|||||||
import file_picker
|
import file_picker
|
||||||
import file_selector_macos
|
import file_selector_macos
|
||||||
import pdfx
|
import pdfx
|
||||||
|
import printing
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
|
||||||
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
|
||||||
PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin"))
|
PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin"))
|
||||||
|
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
16
pubspec.lock
16
pubspec.lock
@@ -677,6 +677,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.12.0"
|
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:
|
pdfx:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -789,6 +797,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.0"
|
version: "2.7.0"
|
||||||
|
printing:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: printing
|
||||||
|
sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.14.3"
|
||||||
provider:
|
provider:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ dependencies:
|
|||||||
uuid: ^4.5.3
|
uuid: ^4.5.3
|
||||||
pdf: ^3.12.0
|
pdf: ^3.12.0
|
||||||
universal_io: ^2.3.1
|
universal_io: ^2.3.1
|
||||||
|
printing: ^5.14.3
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <pdfx/pdfx_plugin.h>
|
#include <pdfx/pdfx_plugin.h>
|
||||||
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
|
#include <printing/printing_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
@@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("PdfxPlugin"));
|
registry->GetRegistrarForPlugin("PdfxPlugin"));
|
||||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
|
PrintingPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
file_selector_windows
|
file_selector_windows
|
||||||
pdfx
|
pdfx
|
||||||
permission_handler_windows
|
permission_handler_windows
|
||||||
|
printing
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user