diff --git a/lib/features/company/bloc/company_settings_cubit.dart b/lib/features/company/bloc/company_settings_cubit.dart index 2adf083..dfbf133 100644 --- a/lib/features/company/bloc/company_settings_cubit.dart +++ b/lib/features/company/bloc/company_settings_cubit.dart @@ -37,6 +37,11 @@ class CompanySettingsCubit extends Cubit { String? zipCode, String? phone, String? email, + String? ticketDisclaimer, + LabelFormat? labelFormat, + double? labelWidth, + double? labelHeight, + bool? isVertical, }) { if (state.company == null) return; @@ -51,6 +56,11 @@ class CompanySettingsCubit extends Cubit { zipCode: zipCode ?? state.company!.zipCode, phone: phone ?? state.company!.phone, email: email ?? state.company!.email, + ticketDisclaimer: ticketDisclaimer ?? state.company!.ticketDisclaimer, + labelFormat: labelFormat ?? state.company!.labelFormat, + labelWidth: labelWidth ?? state.company!.labelWidth, + labelHeight: labelHeight ?? state.company!.labelHeight, + isLabelVertical: isVertical ?? state.company!.isLabelVertical, ); emit(state.copyWith(company: updated)); } diff --git a/lib/features/company/models/company_model.dart b/lib/features/company/models/company_model.dart index 419cee6..701d2e0 100644 --- a/lib/features/company/models/company_model.dart +++ b/lib/features/company/models/company_model.dart @@ -35,6 +35,21 @@ enum SubscriptionStatus { } } +enum LabelFormat { + none, + small_62x29, + medium_54x101, + large_102x152, + custom; + + static LabelFormat fromString(String? value) { + return LabelFormat.values.firstWhere( + (e) => e.name == value, + orElse: () => LabelFormat.none, + ); + } +} + // =================================================================== // IL MODELLO ESATTO // =================================================================== @@ -56,7 +71,11 @@ class CompanyModel extends Equatable { final String? phone; final String? email; final String? logoUrl; - + final String? ticketDisclaimer; + final LabelFormat labelFormat; + final double? labelWidth; + final double? labelHeight; + final bool isLabelVertical; // Stato Pagamenti (Ibride: manuale + Stripe) final bool isPaid; final DateTime? paymentExpiration; @@ -83,6 +102,11 @@ class CompanyModel extends Equatable { this.phone, this.email, this.logoUrl, + this.ticketDisclaimer, + this.labelFormat = LabelFormat.none, + this.labelWidth, + this.labelHeight, + this.isLabelVertical = false, this.isPaid = false, this.paymentExpiration, this.subscriptionTier = SubscriptionTier.free, @@ -105,6 +129,11 @@ class CompanyModel extends Equatable { String? fiscalCode, String? sdi, String? logoUrl, + String? ticketDisclaimer, + LabelFormat? labelFormat, + double? labelWidth, + double? labelHeight, + bool? isLabelVertical, String? phone, String? email, bool? isPaid, @@ -130,6 +159,11 @@ class CompanyModel extends Equatable { logoUrl: logoUrl ?? this.logoUrl, phone: phone ?? this.phone, email: email ?? this.email, + ticketDisclaimer: ticketDisclaimer ?? this.ticketDisclaimer, + labelFormat: labelFormat ?? this.labelFormat, + labelWidth: labelWidth ?? this.labelWidth, + labelHeight: labelHeight ?? this.labelHeight, + isLabelVertical: isLabelVertical ?? this.isLabelVertical, isPaid: isPaid ?? this.isPaid, paymentExpiration: paymentExpiration ?? this.paymentExpiration, subscriptionTier: subscriptionTier ?? this.subscriptionTier, @@ -171,9 +205,18 @@ class CompanyModel extends Equatable { vatId: map['vat_id'] ?? '', fiscalCode: map['fiscal_code'] ?? '', sdi: map['sdi'] ?? '', - logoUrl: map['company_logo'], + logoUrl: map['logo_url'], phone: map['phone'] ?? '', email: map['email'] ?? '', + ticketDisclaimer: map['ticket_disclaimer'], + labelFormat: LabelFormat.fromString(map['label_format']), + labelWidth: map['label_width'] != null + ? (map['label_width'] as num).toDouble() + : null, + labelHeight: map['label_height'] != null + ? (map['label_height'] as num).toDouble() + : null, + isLabelVertical: map['is_label_vertical'] ?? false, isPaid: map['is_paid'] ?? false, paymentExpiration: map['payment_expiration'] != null ? DateTime.tryParse(map['payment_expiration']) @@ -203,9 +246,14 @@ class CompanyModel extends Equatable { 'vat_id': vatId, 'fiscal_code': fiscalCode, 'sdi': sdi, - 'company_logo': logoUrl, + 'logo_url': logoUrl, 'phone': phone, 'email': email, + 'ticket_disclaimer': ticketDisclaimer, + 'label_format': labelFormat.name, + 'label_width': labelWidth, + 'label_height': labelHeight, + 'is_label_vertical': isLabelVertical, 'is_paid': isPaid, if (paymentExpiration != null) 'payment_expiration': paymentExpiration!.toIso8601String(), @@ -236,6 +284,11 @@ class CompanyModel extends Equatable { logoUrl, phone, email, + ticketDisclaimer, + labelFormat, + labelWidth, + labelHeight, + isLabelVertical, isPaid, paymentExpiration, subscriptionTier, diff --git a/lib/features/company/ui/company_settings_screen.dart b/lib/features/company/ui/company_settings_screen.dart index 62bcefd..8d97e11 100644 --- a/lib/features/company/ui/company_settings_screen.dart +++ b/lib/features/company/ui/company_settings_screen.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/company/bloc/company_settings_cubit.dart'; import 'package:flux/features/company/models/company_model.dart'; +import 'package:flux/features/settings/document_sequence/blocs/document_sequence_cubit.dart'; +import 'package:flux/features/settings/document_sequence/ui/document_sequence_section.dart'; import 'package:image_picker/image_picker.dart'; class CompanySettingsScreen extends StatefulWidget { @@ -24,6 +26,7 @@ class _CompanySettingsScreenState extends State { final _zipCtrl = TextEditingController(); final _phoneCtrl = TextEditingController(); final _emailCtrl = TextEditingController(); + final _disclaimerCtrl = TextEditingController(); bool _isInitialized = false; @@ -50,6 +53,8 @@ class _CompanySettingsScreenState extends State { _zipCtrl.dispose(); _phoneCtrl.dispose(); _emailCtrl.dispose(); + _disclaimerCtrl.dispose(); + super.dispose(); } @@ -69,6 +74,9 @@ class _CompanySettingsScreenState extends State { if (_phoneCtrl.text.isEmpty) _phoneCtrl.text = company.phone ?? ''; if (_emailCtrl.text.isEmpty) _emailCtrl.text = company.email ?? ''; _isInitialized = true; + if (_disclaimerCtrl.text.isEmpty) { + _disclaimerCtrl.text = company.ticketDisclaimer ?? ''; + } } void _flushToCubit() { @@ -83,6 +91,7 @@ class _CompanySettingsScreenState extends State { zipCode: _zipCtrl.text, phone: _phoneCtrl.text, email: _emailCtrl.text, + ticketDisclaimer: _disclaimerCtrl.text, ); } @@ -99,6 +108,36 @@ class _CompanySettingsScreenState extends State { } } + 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().updateFields( + labelFormat: selectedFormat, + labelWidth: w, + labelHeight: h, + ); + } + @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -312,6 +351,63 @@ class _CompanySettingsScreenState extends State { ), ], ), + 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() + .updateFields(ticketDisclaimer: val), + ), + + const SizedBox(height: 24), + + // Sezione Etichette + Text( + "Configurazione Etichette", + style: theme.textTheme.titleMedium, + ), + const SizedBox(height: 8), + DropdownButtonFormField( + initialValue: company.labelFormat, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.label_outline), + labelText: "Formato Stampa Etichetta", + ), + items: LabelFormat.values + .map( + (f) => DropdownMenuItem( + value: f, + child: Text( + f.name.replaceAll('_', ' ').toUpperCase(), + ), + ), + ) + .toList(), + onChanged: (val) { + if (val != null) { + _onLabelFormatChanged(val); + } + }, + ), const SizedBox(height: 48), // --- PULSANTE SALVATAGGIO --- diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index 321dbb7..86a0f4b 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -217,46 +217,7 @@ class _OperationFormScreenState extends State { final isUltraWide = constraints.maxWidth > 1400; final isDesktop = constraints.maxWidth > 900; if (isUltraWide) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - flex: 4, - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: _buildMainFormContent( - theme, - state, - displayStatus, - showFiles: false, - ), - ), - ), - VerticalDivider(width: 1, color: theme.dividerColor), - Expanded( - flex: 3, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: _buildNotesSection(isDesktop: true), - ), - ), - VerticalDivider(width: 1, color: theme.dividerColor), - Expanded( - flex: 3, - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: SharedAttachmentsSection( - parentType: AttachmentParentType.operation, - parentId: state.operation.id, - titleForUpload: - state.operation.customerDisplayName ?? - 'Nuova pratica', - onGenerateIdForQr: _generateIdForQr, - ), - ), - ), - ], - ); + return _buildUltraWide(state, theme); } else if (isDesktop) { return Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -365,48 +326,92 @@ class _OperationFormScreenState extends State { ); } - Widget _buildMainFormContent( - ThemeData theme, - OperationFormState state, - OperationStatus displayStatus, { - bool showFiles = true, - }) { - final currentOp = state.operation; - final currentType = currentOp.type; - - return Column( + Widget _buildUltraWide(OperationFormState state, ThemeData theme) { + return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - StaffSection( - staffId: currentOp.staffId, - staffName: currentOp.staffDisplayName, - onStaffSelected: (staff) => { - context.read().updateFields( - staffId: staff.id, - staffDisplayName: staff.name, + Expanded( + flex: 4, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildStaffSection(state), + const Divider(height: 50), + _buildOperationStatusSection(state), + const Divider(height: 32), + _buildCustomerSection(state), + const SizedBox(height: 16), + _buildReferenceSection(state), + const Divider(height: 50), + _buildOperationTypeSection(state), + const SizedBox(height: 16), + _buildQuantitySection(state), + const Divider(height: 50), + _buildDetailsSection(state), + const Divider(height: 50), + ], ), - }, + ), ), - const Divider(height: 50), + VerticalDivider(width: 1, color: theme.dividerColor), + Expanded( + flex: 3, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: _buildNotesSection(isDesktop: true), + ), + ), + VerticalDivider(width: 1, color: theme.dividerColor), + Expanded( + flex: 3, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: _buildAttachmentSection(state), + ), + ), + ], + ); + } - // --- SEZIONE STATO OPERAZIONE --- + Widget _buildStaffSection(OperationFormState state) { + return StaffSection( + staffId: state.operation.staffId, + staffName: state.operation.staffDisplayName, + onStaffSelected: (staff) => { + context.read().updateFields( + staffId: staff.id, + staffDisplayName: staff.name, + ), + }, + ); + } + + Widget _buildOperationStatusSection(OperationFormState state) { + return Column( + children: [ _buildSectionTitle('Esito / Stato Operazione'), Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), decoration: BoxDecoration( - color: _getStatusColor(displayStatus).withValues(alpha: 0.1), + color: _getStatusColor( + state.operation.status, + ).withValues(alpha: 0.1), border: Border.all( - color: _getStatusColor(displayStatus).withValues(alpha: 0.3), + color: _getStatusColor( + state.operation.status, + ).withValues(alpha: 0.3), ), borderRadius: BorderRadius.circular(12), ), child: DropdownButtonHideUnderline( child: DropdownButton( isExpanded: true, - value: displayStatus, + value: state.operation.status, icon: Icon( Icons.arrow_drop_down, - color: _getStatusColor(displayStatus), + color: _getStatusColor(state.operation.status), ), items: OperationStatus.values /* .where( @@ -450,34 +455,41 @@ class _OperationFormScreenState extends State { ), const SizedBox(height: 8), Text( - displayStatus == OperationStatus.success + state.operation.status == OperationStatus.success ? 'Lascia OK se la pratica è stata caricata con successo.' - : 'Attenzione: la pratica verrà salvata come ${displayStatus.displayName}.', + : 'Attenzione: la pratica verrà salvata come ${state.operation.status.displayName}.', style: TextStyle(fontSize: 12, color: Colors.grey.shade600), ), - const Divider(height: 32), + ], + ); + } - //_buildSectionTitle('Cliente & Riferimento'), - SharedCustomerSection( - customerId: currentOp.customerId, - customerName: currentOp.customerDisplayName, - onCustomerSelected: (customer) { - context.read().updateFields( - customerId: customer.id, - customerDisplayName: customer.name, - ); - }, - ), - const SizedBox(height: 16), - TextFormField( - controller: _referenceController, - decoration: const InputDecoration( - labelText: 'Riferimento (es. numero di telefono, targa...)', - prefixIcon: Icon(Icons.tag), - ), - ), - const Divider(height: 32), + Widget _buildCustomerSection(OperationFormState state) { + return SharedCustomerSection( + customerId: state.operation.customerId, + customerName: state.operation.customerDisplayName, + onCustomerSelected: (customer) { + context.read().updateFields( + customerId: customer.id, + customerDisplayName: customer.name, + ); + }, + ); + } + Widget _buildReferenceSection(OperationFormState state) { + return TextFormField( + controller: _referenceController, + decoration: const InputDecoration( + labelText: 'Riferimento (es. numero di telefono, targa...)', + prefixIcon: Icon(Icons.tag), + ), + ); + } + + Widget _buildOperationTypeSection(OperationFormState state) { + return Column( + children: [ _buildSectionTitle('Cosa stiamo facendo?'), Wrap( spacing: 8.0, @@ -485,7 +497,7 @@ class _OperationFormScreenState extends State { children: _availableTypes.map((type) { return ChoiceChip( label: Text(type), - selected: currentType == type, + selected: state.operation.type == type, onSelected: (selected) { if (selected) { context.read().setTypeWithSmartDefault( @@ -496,63 +508,90 @@ class _OperationFormScreenState extends State { ); }).toList(), ), - const Divider(height: 32), + ], + ); + } + Widget _buildDetailsSection(OperationFormState state) { + return Column( + children: [ _buildSectionTitle('Dettagli Servizio'), DetailsSection( - currentOp: currentOp, - currentType: currentType, + currentOp: state.operation, + currentType: state.operation.type, freeTextSubtypeController: _freeTextSubtypeController, freeTextDescriptionController: _freeTextDescriptionController, - durationQuickPicks: _buildDurationQuickPicks(currentOp), + durationQuickPicks: _buildDurationQuickPicks(state.operation), ), + ], + ); + } + + Widget _buildQuantitySection(OperationFormState state) { + return Row( + children: [ + const Text('Quantità: '), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () { + final q = state.operation.quantity; + if (q > 1) { + context.read().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().updateFields(quantity: q + 1); + }, + ), + ], + ); + } + + Widget _buildAttachmentSection(OperationFormState state) { + return SharedAttachmentsSection( + parentType: AttachmentParentType.operation, + parentId: state.operation.id, + titleForUpload: state.operation.customerDisplayName ?? 'Nuova pratica', + onGenerateIdForQr: _generateIdForQr, + ); + } + + Widget _buildMainFormContent( + ThemeData theme, + OperationFormState state, + OperationStatus displayStatus, { + bool showFiles = true, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildStaffSection(state), + const Divider(height: 50), + _buildOperationStatusSection(state), + const Divider(height: 32), + _buildCustomerSection(state), + const SizedBox(height: 16), + _buildReferenceSection(state), + const Divider(height: 50), + _buildOperationTypeSection(state), + const SizedBox(height: 16), + _buildQuantitySection(state), + const Divider(height: 50), + _buildDetailsSection(state), + const Divider(height: 50), // QUANTITÀ - Row( - children: [ - const Text('Quantità: '), - IconButton( - icon: const Icon(Icons.remove), - onPressed: () { - final q = currentOp.quantity; - if (q > 1) { - context.read().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().updateFields( - quantity: q + 1, - ); - }, - ), - ], - ), const Divider(height: 32), - if (showFiles) ...[ - SharedAttachmentsSection( - parentType: AttachmentParentType.operation, - parentId: currentOp.id, - titleForUpload: - state.operation.customerDisplayName ?? 'Nuova pratica', - onGenerateIdForQr: _generateIdForQr, - ), - /* SharedFilesSection( - titleNameForUpload: - state.operation.customerDisplayName ?? 'Nuova pratica', - onGenerateIdForQr: _generateIdForQr, - ), */ - ], + if (showFiles) ...[_buildAttachmentSection(state)], ], ); } diff --git a/lib/features/settings/document_sequence/blocs/document_sequence_cubit.dart b/lib/features/settings/document_sequence/blocs/document_sequence_cubit.dart new file mode 100644 index 0000000..10d2719 --- /dev/null +++ b/lib/features/settings/document_sequence/blocs/document_sequence_cubit.dart @@ -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 sequences; + final bool isLoading; + final String? error; + + DocumentSequenceState({ + this.sequences = const [], + this.isLoading = false, + this.error, + }); +} + +class DocumentSequenceCubit extends Cubit { + final String companyId; + final _supabase = Supabase.instance.client; + + DocumentSequenceCubit(this.companyId) : super(DocumentSequenceState()); + + Future 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 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", + ), + ); + } + } +} diff --git a/lib/features/settings/document_sequence/data/document_sequence_repository.dart b/lib/features/settings/document_sequence/data/document_sequence_repository.dart new file mode 100644 index 0000000..7de38aa --- /dev/null +++ b/lib/features/settings/document_sequence/data/document_sequence_repository.dart @@ -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(); + + Future> 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 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, + }); + } +} diff --git a/lib/features/settings/document_sequence/models/document_sequence_model.dart b/lib/features/settings/document_sequence/models/document_sequence_model.dart new file mode 100644 index 0000000..c596674 --- /dev/null +++ b/lib/features/settings/document_sequence/models/document_sequence_model.dart @@ -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 map) { + return DocumentSequence( + docType: map['doc_type'], + nextValue: map['next_value'], + prefix: map['prefix'] ?? '', + ); + } +} diff --git a/lib/features/settings/document_sequence/ui/document_sequence_section.dart b/lib/features/settings/document_sequence/ui/document_sequence_section.dart new file mode 100644 index 0000000..407979c --- /dev/null +++ b/lib/features/settings/document_sequence/ui/document_sequence_section.dart @@ -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( + 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() + .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() + .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().saveSequences(), + icon: const Icon(Icons.save), + label: const Text("SALVA PROTOCOLLI"), + ), + ), + ], + ); + }, + ); + } +} diff --git a/lib/features/tickets/blocs/ticket_form_cubit.dart b/lib/features/tickets/blocs/ticket_form_cubit.dart index f4464c4..2c12ae3 100644 --- a/lib/features/tickets/blocs/ticket_form_cubit.dart +++ b/lib/features/tickets/blocs/ticket_form_cubit.dart @@ -153,8 +153,12 @@ class TicketFormCubit extends Cubit { if (ticketToSave.customerId == null || ticketToSave.customerId!.isEmpty) { throw Exception("Seleziona un cliente prima di salvare."); } - - final savedTicket = await _repository.saveTicket(ticketToSave); + TicketModel? savedTicket; + if (ticketToSave.id == null) { + savedTicket = await _repository.insertTicket(ticketToSave); + } else { + savedTicket = await _repository.updateTicket(ticketToSave); + } if (keepAdding) { emit( @@ -198,7 +202,7 @@ class TicketFormCubit extends Cubit { throw Exception("Seleziona un cliente prima di poter usare il QR."); } - final savedTicket = await _repository.saveTicket(ticketToSave); + final savedTicket = await _repository.insertTicket(ticketToSave); // Aggiorniamo silenziosamente lo stato con il ticket che ora ha un ID! emit(state.copyWith(ticket: savedTicket, status: TicketFormStatus.ready)); diff --git a/lib/features/tickets/data/ticket_repository.dart b/lib/features/tickets/data/ticket_repository.dart index ae01cb8..1029d61 100644 --- a/lib/features/tickets/data/ticket_repository.dart +++ b/lib/features/tickets/data/ticket_repository.dart @@ -192,12 +192,42 @@ class TicketRepository { } } - /// Salva il ticket con upsert - Future saveTicket(TicketModel ticket) async { + Future generateTicketReference(String companyId) async { + final response = await Supabase.instance.client.rpc( + 'get_next_document_number', + params: {'p_company_id': companyId, 'p_doc_type': 'ticket'}, + ); + + // Estraiamo i dati dal JSON + final int nextValue = response['next_value']; + final String prefix = response['prefix'] ?? ''; + + final year = DateTime.now().year; // 2026 + + // Formattazione con zeri iniziali (es. 000125) + final paddedNumber = nextValue.toString().padLeft(6, '0'); + + // Costruiamo la stringa. Se c'è un prefisso mette "TCK-2026-000125", + // altrimenti solo "2026-000125" + if (prefix.isNotEmpty) { + return '$prefix-$year-$paddedNumber'; + } else { + return '$year-$paddedNumber'; + } + } + + /// Salva il ticket + Future insertTicket(TicketModel ticket) async { + if (ticket.id != null) { + throw Exception('Impossibile creare un ticket esistente, id not null'); + } try { + final ticketToSave = ticket.copyWith( + referenceId: await generateTicketReference(ticket.companyId), + ); final response = await _supabase .from(_tableName) - .upsert(ticket.toMap()) + .insert(ticketToSave.toMap()) .select() .single(); diff --git a/lib/features/tickets/models/ticket_model.dart b/lib/features/tickets/models/ticket_model.dart index 6880772..3f307c8 100644 --- a/lib/features/tickets/models/ticket_model.dart +++ b/lib/features/tickets/models/ticket_model.dart @@ -98,7 +98,7 @@ class TicketModel extends Equatable { final WarrantyType? warrantyType; final String? publicNotes; final String? internalNotes; - final int? referenceNumber; + final String? referenceId; final String? alternativePhoneNumber; final bool hasCourtesyDevice; final TicketType ticketType; @@ -106,7 +106,6 @@ class TicketModel extends Equatable { final DateTime? estimatedDeliveryAt; final TicketResult? ticketResult; final String? resolutionNotes; - final String? legacyId; final String? customerName; final String? targetModelName; final String? sourceModelName; @@ -134,7 +133,7 @@ class TicketModel extends Equatable { this.warrantyType, this.publicNotes, this.internalNotes, - this.referenceNumber, + this.referenceId, this.alternativePhoneNumber, this.hasCourtesyDevice = false, required this.ticketType, @@ -142,7 +141,6 @@ class TicketModel extends Equatable { this.estimatedDeliveryAt, this.ticketResult, this.resolutionNotes, - this.legacyId, this.customerName, this.targetModelName, this.sourceModelName, @@ -185,7 +183,7 @@ class TicketModel extends Equatable { WarrantyType? warrantyType, String? publicNotes, String? internalNotes, - int? referenceNumber, + String? referenceId, String? alternativePhoneNumber, bool? hasCourtesyDevice, TicketType? ticketType, @@ -193,7 +191,6 @@ class TicketModel extends Equatable { DateTime? estimatedDeliveryAt, TicketResult? ticketResult, String? resolutionNotes, - String? legacyId, String? customerName, String? targetModelName, String? sourceModelName, @@ -221,7 +218,7 @@ class TicketModel extends Equatable { warrantyType: warrantyType ?? this.warrantyType, publicNotes: publicNotes ?? this.publicNotes, internalNotes: internalNotes ?? this.internalNotes, - referenceNumber: referenceNumber ?? this.referenceNumber, + referenceId: referenceId ?? this.referenceId, alternativePhoneNumber: alternativePhoneNumber ?? this.alternativePhoneNumber, hasCourtesyDevice: hasCourtesyDevice ?? this.hasCourtesyDevice, @@ -230,7 +227,6 @@ class TicketModel extends Equatable { estimatedDeliveryAt: estimatedDeliveryAt ?? this.estimatedDeliveryAt, ticketResult: ticketResult ?? this.ticketResult, resolutionNotes: resolutionNotes ?? this.resolutionNotes, - legacyId: legacyId ?? this.legacyId, customerName: customerName ?? this.customerName, targetModelName: targetModelName ?? this.targetModelName, sourceModelName: sourceModelName ?? this.sourceModelName, @@ -269,7 +265,7 @@ class TicketModel extends Equatable { warrantyType: WarrantyType.fromString(map['warranty_type'] as String?), publicNotes: map['public_notes'] as String?, internalNotes: map['internal_notes'] as String?, - referenceNumber: map['reference_number'] as int?, + referenceId: map['reference_id'] as String?, alternativePhoneNumber: map['alternative_phone_number'] as String?, hasCourtesyDevice: map['has_courtesy_device'] as bool? ?? false, ticketType: TicketType.fromString(map['ticket_type'] as String), @@ -279,7 +275,6 @@ class TicketModel extends Equatable { : null, ticketResult: TicketResult.fromString(map['ticket_result'] as String?), resolutionNotes: map['resolution_notes'] as String?, - legacyId: map['legacy_id'] as String?, customerName: (map['customer']?['name'] as String?).myFormat(), targetModelName: (map['target_model']?['name_with_brand'] as String?) ?.myFormat(), @@ -314,6 +309,7 @@ class TicketModel extends Equatable { 'warranty_type': warrantyType, 'public_notes': publicNotes, 'internal_notes': internalNotes, + 'reference_id': referenceId, 'alternative_phone_number': alternativePhoneNumber, 'has_courtesy_device': hasCourtesyDevice, 'ticket_type': ticketType.value, @@ -322,7 +318,6 @@ class TicketModel extends Equatable { 'estimated_delivery_at': estimatedDeliveryAt!.toUtc().toIso8601String(), if (ticketResult != null) 'ticket_result': ticketResult!.value, 'resolution_notes': resolutionNotes, - 'legacy_id': legacyId, 'included_accessories': includedAccessories, }; } @@ -346,7 +341,6 @@ class TicketModel extends Equatable { warrantyType, publicNotes, internalNotes, - referenceNumber, alternativePhoneNumber, hasCourtesyDevice, ticketType, @@ -354,7 +348,6 @@ class TicketModel extends Equatable { estimatedDeliveryAt, ticketResult, resolutionNotes, - legacyId, includedAccessories, customerName, targetModelName, diff --git a/lib/features/tickets/utils/ticket_pdf_service.dart b/lib/features/tickets/utils/ticket_pdf_service.dart new file mode 100644 index 0000000..9d96699 --- /dev/null +++ b/lib/features/tickets/utils/ticket_pdf_service.dart @@ -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 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 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(); + } +} diff --git a/lib/main.dart b/lib/main.dart index d5ff658..39fb34b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:flux/features/auth/bloc/auth_cubit.dart'; import 'package:flux/features/company/data/company_repository.dart'; import 'package:flux/features/operations/blocs/operation_list_cubit.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; +import 'package:flux/features/settings/document_sequence/data/document_sequence_repository.dart'; import 'package:flux/features/tickets/data/ticket_repository.dart'; import 'package:flux/l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; @@ -97,6 +98,9 @@ Future setupLocator() async { () => AttachmentsRepository(), ); getIt.registerLazySingleton(() => TicketRepository()); + getIt.registerLazySingleton( + () => DocumentSequenceRepository(), + ); // NOTA: CompanyRepository l'ho tolto perché la logica della Company // ora è gestita dal CoreRepository durante l'Onboarding. diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e12c657..f35d186 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include void fl_register_plugins(FlPluginRegistry* registry) { @@ -17,6 +18,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) gtk_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) printing_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin"); + printing_plugin_register_with_registrar(printing_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2127acd..cdb79f7 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_linux gtk + printing url_launcher_linux ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 425ee44..3b1fe6d 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import app_links import file_picker import file_selector_macos import pdfx +import printing import shared_preferences_foundation import url_launcher_macos @@ -17,6 +18,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) PdfxPlugin.register(with: registry.registrar(forPlugin: "PdfxPlugin")) + PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index e961585..35de733 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -677,6 +677,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.12.0" + pdf_widget_wrapper: + dependency: transitive + description: + name: pdf_widget_wrapper + sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5 + url: "https://pub.dev" + source: hosted + version: "1.0.4" pdfx: dependency: "direct main" description: @@ -789,6 +797,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.7.0" + printing: + dependency: "direct main" + description: + name: printing + sha256: "689170c9ddb1bda85826466ba80378aa8993486d3c959a71cd7d2d80cb606692" + url: "https://pub.dev" + source: hosted + version: "5.14.3" provider: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3983bed..1c40f76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,6 +33,7 @@ dependencies: uuid: ^4.5.3 pdf: ^3.12.0 universal_io: ^2.3.1 + printing: ^5.14.3 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index b2c13bf..9adb38d 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,6 +10,7 @@ #include #include #include +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { @@ -21,6 +22,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("PdfxPlugin")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + PrintingPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PrintingPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 668b8b9..0ca023d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,6 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_selector_windows pdfx permission_handler_windows + printing url_launcher_windows )