ticket labels e ticket receipt

This commit is contained in:
2026-05-10 14:09:57 +02:00
parent 385c3da0a5
commit 5c86483563
20 changed files with 1024 additions and 157 deletions

View File

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

View File

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

View File

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

View File

@@ -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(
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),
],
),
),
),
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),
),
),
],
);
}
Widget _buildStaffSection(OperationFormState state) {
return StaffSection(
staffId: state.operation.staffId,
staffName: state.operation.staffDisplayName,
onStaffSelected: (staff) => { onStaffSelected: (staff) => {
context.read<OperationFormCubit>().updateFields( context.read<OperationFormCubit>().updateFields(
staffId: staff.id, staffId: staff.id,
staffDisplayName: staff.name, staffDisplayName: staff.name,
), ),
}, },
), );
const Divider(height: 50), }
// --- SEZIONE STATO OPERAZIONE --- 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(
Widget _buildReferenceSection(OperationFormState state) {
return TextFormField(
controller: _referenceController, controller: _referenceController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Riferimento (es. numero di telefono, targa...)', labelText: 'Riferimento (es. numero di telefono, targa...)',
prefixIcon: Icon(Icons.tag), prefixIcon: Icon(Icons.tag),
), ),
), );
const Divider(height: 32), }
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),
), ),
],
);
}
// QUANTITÀ Widget _buildQuantitySection(OperationFormState state) {
Row( return Row(
children: [ children: [
const Text('Quantità: '), const Text('Quantità: '),
IconButton( IconButton(
icon: const Icon(Icons.remove), icon: const Icon(Icons.remove),
onPressed: () { onPressed: () {
final q = currentOp.quantity; final q = state.operation.quantity;
if (q > 1) { if (q > 1) {
context.read<OperationFormCubit>().updateFields( context.read<OperationFormCubit>().updateFields(quantity: q - 1);
quantity: q - 1,
);
} }
}, },
), ),
Text( Text(
'${currentOp.quantity}', '${state.operation.quantity}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
onPressed: () { onPressed: () {
final q = currentOp.quantity; final q = state.operation.quantity;
context.read<OperationFormCubit>().updateFields( context.read<OperationFormCubit>().updateFields(quantity: q + 1);
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À
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,
), */
],
], ],
); );
} }

View File

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

View File

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

View File

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

View File

@@ -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"),
),
),
],
);
},
);
}
}

View File

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

View File

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

View File

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

View 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();
}
}

View File

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

View File

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

View File

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

View File

@@ -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"))
} }

View File

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

View File

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

View File

@@ -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"));
} }

View File

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