stampa ddt
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flux/features/documents/models/shipment_document_model.dart';
|
|||||||
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
||||||
import 'package:flux/features/master_data/providers/models/provider_role.dart';
|
import 'package:flux/features/master_data/providers/models/provider_role.dart';
|
||||||
import 'package:flux/features/settings/document_sequence/models/document_sequence_model.dart';
|
import 'package:flux/features/settings/document_sequence/models/document_sequence_model.dart';
|
||||||
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ class TicketsShipmentRepository {
|
|||||||
// NUOVO METODO: Salva il DDT e aggiorna i Ticket
|
// NUOVO METODO: Salva il DDT e aggiorna i Ticket
|
||||||
Future<void> createShipmentDocument({
|
Future<void> createShipmentDocument({
|
||||||
required ShipmentDocumentModel document,
|
required ShipmentDocumentModel document,
|
||||||
required String newTicketStatus, // es: 'shipped' o 'inExternalLab'
|
required TicketStatus newTicketStatus, // es: 'shipped' o 'inExternalLab'
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
// 1. Inseriamo il singolo Documento di Trasporto
|
// 1. Inseriamo il singolo Documento di Trasporto
|
||||||
@@ -42,8 +43,8 @@ class TicketsShipmentRepository {
|
|||||||
|
|
||||||
// 2. Aggiorniamo lo stato di TUTTI i ticket inclusi nel DDT
|
// 2. Aggiorniamo lo stato di TUTTI i ticket inclusi nel DDT
|
||||||
await _supabase
|
await _supabase
|
||||||
.from('tickets')
|
.from('ticket')
|
||||||
.update({'ticket_status': newTicketStatus})
|
.update({'ticket_status': newTicketStatus.value})
|
||||||
.inFilter('id', document.ticketIds);
|
.inFilter('id', document.ticketIds);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw ('Errore durante la creazione della spedizione: $e');
|
throw ('Errore durante la creazione della spedizione: $e');
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart'; // Per estrarre gli store
|
import 'package:supabase_flutter/supabase_flutter.dart'; // Per estrarre gli store
|
||||||
import '../models/provider_model.dart';
|
import '../models/provider_model.dart';
|
||||||
@@ -13,7 +14,13 @@ class ProviderFormCubit extends Cubit<ProviderFormState> {
|
|||||||
final _client = Supabase.instance.client; // Lo usiamo al volo per gli store
|
final _client = Supabase.instance.client; // Lo usiamo al volo per gli store
|
||||||
|
|
||||||
ProviderFormCubit()
|
ProviderFormCubit()
|
||||||
: super(ProviderFormState(provider: ProviderModel.empty(companyId: '')));
|
: super(
|
||||||
|
ProviderFormState(
|
||||||
|
provider: ProviderModel.empty(
|
||||||
|
companyId: GetIt.I.get<SessionCubit>().state.company!.id!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// --- INIZIALIZZAZIONE ---
|
// --- INIZIALIZZAZIONE ---
|
||||||
Future<void> initForm({
|
Future<void> initForm({
|
||||||
@@ -51,7 +58,8 @@ class ProviderFormCubit extends Cubit<ProviderFormState> {
|
|||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: ProviderFormStatus.initial,
|
status: ProviderFormStatus.initial,
|
||||||
provider: existingProvider ?? ProviderModel.empty(companyId: ''),
|
provider:
|
||||||
|
existingProvider ?? ProviderModel.empty(companyId: companyId),
|
||||||
availableStores: storesResponse as List<dynamic>,
|
availableStores: storesResponse as List<dynamic>,
|
||||||
selectedStoreIds: linkedStoreIds,
|
selectedStoreIds: linkedStoreIds,
|
||||||
localLocations: existingProvider?.locations ?? [],
|
localLocations: existingProvider?.locations ?? [],
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import '../models/provider_model.dart';
|
import '../models/provider_model.dart';
|
||||||
@@ -5,6 +6,7 @@ import '../models/provider_location_model.dart';
|
|||||||
|
|
||||||
class ProviderRepository {
|
class ProviderRepository {
|
||||||
final _supabase = GetIt.I.get<SupabaseClient>();
|
final _supabase = GetIt.I.get<SupabaseClient>();
|
||||||
|
final _companyId = GetIt.I.get<SessionCubit>().state.company!.id!;
|
||||||
|
|
||||||
// 1. Carica i provider abilitati per uno specifico Store
|
// 1. Carica i provider abilitati per uno specifico Store
|
||||||
Future<List<ProviderModel>> getProvidersByStore(String storeId) async {
|
Future<List<ProviderModel>> getProvidersByStore(String storeId) async {
|
||||||
@@ -44,9 +46,10 @@ class ProviderRepository {
|
|||||||
List<String> enabledStoreIds,
|
List<String> enabledStoreIds,
|
||||||
) async {
|
) async {
|
||||||
// A. Salva/Aggiorna il Provider principale
|
// A. Salva/Aggiorna il Provider principale
|
||||||
|
final providerWithCompany = provider.copyWith(companyId: _companyId);
|
||||||
final savedRow = await _supabase
|
final savedRow = await _supabase
|
||||||
.from('provider')
|
.from('provider')
|
||||||
.upsert(provider.toMap())
|
.upsert(providerWithCompany.toMap())
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
|
|||||||
@@ -129,8 +129,8 @@ class ProviderModel extends Equatable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
return {
|
Map<String, dynamic> baseMap = {
|
||||||
if (id != null) 'id': id,
|
if (id != null && id!.trim().isNotEmpty) 'id': id,
|
||||||
'company_id': companyId,
|
'company_id': companyId,
|
||||||
'name': name,
|
'name': name,
|
||||||
'is_active': isActive,
|
'is_active': isActive,
|
||||||
@@ -146,6 +146,7 @@ class ProviderModel extends Equatable {
|
|||||||
// Trasformiamo gli Enum di nuovo in stringhe per Supabase
|
// Trasformiamo gli Enum di nuovo in stringhe per Supabase
|
||||||
'roles': roles.map((e) => e.name).toList(),
|
'roles': roles.map((e) => e.name).toList(),
|
||||||
};
|
};
|
||||||
|
return baseMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -60,7 +60,13 @@ class _ProviderListScreenState extends State<ProviderListScreen> {
|
|||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Gestione Fornitori')),
|
appBar: AppBar(title: const Text('Gestione Fornitori')),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: () => context.pushNamed(Routes.providerForm),
|
onPressed: () async {
|
||||||
|
final providerListCubit = context.read<ProviderListCubit>();
|
||||||
|
final storeId = context.read<SessionCubit>().state.currentStore?.id;
|
||||||
|
await context.pushNamed(Routes.providerForm);
|
||||||
|
if (!mounted || storeId == null) return;
|
||||||
|
providerListCubit.loadProviders(storeId);
|
||||||
|
},
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
label: const Text('Nuovo Fornitore'),
|
label: const Text('Nuovo Fornitore'),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -77,21 +77,21 @@ class TicketListCubit extends Cubit<TicketListState> {
|
|||||||
loadTickets(refresh: true); // Applica i filtri e ricarica
|
loadTickets(refresh: true); // Applica i filtri e ricarica
|
||||||
}
|
}
|
||||||
|
|
||||||
void toggleTicketSelection(String ticketId) {
|
void toggleTicketSelection(TicketModel ticket) {
|
||||||
final currentSelection = Set<String>.from(state.selectedTicketIds);
|
final currentSelection = Set<TicketModel>.from(state.selectedTickets);
|
||||||
if (currentSelection.contains(ticketId)) {
|
if (currentSelection.contains(ticket)) {
|
||||||
currentSelection.remove(ticketId);
|
currentSelection.remove(ticket);
|
||||||
} else {
|
} else {
|
||||||
currentSelection.add(ticketId);
|
currentSelection.add(ticket);
|
||||||
}
|
}
|
||||||
emit(state.copyWith(selectedTicketIds: currentSelection));
|
emit(state.copyWith(selectedTickets: currentSelection));
|
||||||
}
|
}
|
||||||
|
|
||||||
void clearSelection() {
|
void clearSelection() {
|
||||||
emit(state.copyWith(selectedTicketIds: {}));
|
emit(state.copyWith(selectedTickets: {}));
|
||||||
}
|
}
|
||||||
|
|
||||||
void selectAll(List<String> ticketIds) {
|
void selectAll(List<TicketModel> tickets) {
|
||||||
emit(state.copyWith(selectedTicketIds: ticketIds.toSet()));
|
emit(state.copyWith(selectedTickets: tickets.toSet()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class TicketListState extends Equatable {
|
|||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final bool hasReachedMax;
|
final bool hasReachedMax;
|
||||||
final String errorMessage;
|
final String errorMessage;
|
||||||
final Set<String> selectedTicketIds;
|
final Set<TicketModel> selectedTickets;
|
||||||
|
|
||||||
// Filtri attivi
|
// Filtri attivi
|
||||||
final String? searchTerm;
|
final String? searchTerm;
|
||||||
@@ -21,7 +21,7 @@ class TicketListState extends Equatable {
|
|||||||
this.isLoading = false,
|
this.isLoading = false,
|
||||||
this.hasReachedMax = false,
|
this.hasReachedMax = false,
|
||||||
this.errorMessage = '',
|
this.errorMessage = '',
|
||||||
this.selectedTicketIds = const {},
|
this.selectedTickets = const {},
|
||||||
this.searchTerm,
|
this.searchTerm,
|
||||||
this.dateRange,
|
this.dateRange,
|
||||||
this.statusFilter,
|
this.statusFilter,
|
||||||
@@ -34,7 +34,7 @@ class TicketListState extends Equatable {
|
|||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
bool? hasReachedMax,
|
bool? hasReachedMax,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
Set<String>? selectedTicketIds,
|
Set<TicketModel>? selectedTickets,
|
||||||
String? searchTerm,
|
String? searchTerm,
|
||||||
DateTimeRange? dateRange,
|
DateTimeRange? dateRange,
|
||||||
TicketStatus? statusFilter,
|
TicketStatus? statusFilter,
|
||||||
@@ -54,7 +54,7 @@ class TicketListState extends Equatable {
|
|||||||
statusFilter: clearStatus ? null : (statusFilter ?? this.statusFilter),
|
statusFilter: clearStatus ? null : (statusFilter ?? this.statusFilter),
|
||||||
ticketTypeFilter: ticketTypeFilter ?? this.ticketTypeFilter,
|
ticketTypeFilter: ticketTypeFilter ?? this.ticketTypeFilter,
|
||||||
staffIdFilter: staffIdFilter ?? this.staffIdFilter,
|
staffIdFilter: staffIdFilter ?? this.staffIdFilter,
|
||||||
selectedTicketIds: selectedTicketIds ?? this.selectedTicketIds,
|
selectedTickets: selectedTickets ?? this.selectedTickets,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ class TicketListState extends Equatable {
|
|||||||
isLoading,
|
isLoading,
|
||||||
hasReachedMax,
|
hasReachedMax,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
selectedTicketIds,
|
selectedTickets,
|
||||||
searchTerm,
|
searchTerm,
|
||||||
dateRange,
|
dateRange,
|
||||||
statusFilter,
|
statusFilter,
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import 'package:flux/features/documents/models/shipment_document_model.dart';
|
|||||||
import 'package:flux/features/master_data/providers/models/provider_location_model.dart';
|
import 'package:flux/features/master_data/providers/models/provider_location_model.dart';
|
||||||
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
||||||
import 'package:flux/features/settings/document_sequence/data/document_sequence_repository.dart';
|
import 'package:flux/features/settings/document_sequence/data/document_sequence_repository.dart';
|
||||||
|
import 'package:flux/features/settings/document_sequence/models/document_sequence_model.dart';
|
||||||
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
part 'ticket_shipping_state.dart';
|
part 'ticket_shipping_state.dart';
|
||||||
@@ -85,27 +87,6 @@ class TicketShippingCubit extends Cubit<TicketShippingState> {
|
|||||||
Future<void> toggleAutoNumber(bool value) async {
|
Future<void> toggleAutoNumber(bool value) async {
|
||||||
// Aggiorniamo subito l'UI per mostrare che lo switch si è acceso
|
// Aggiorniamo subito l'UI per mostrare che lo switch si è acceso
|
||||||
emit(state.copyWith(isAutoNumber: value));
|
emit(state.copyWith(isAutoNumber: value));
|
||||||
|
|
||||||
if (value) {
|
|
||||||
// Se lo switch è acceso, chiediamo il numero al DB
|
|
||||||
try {
|
|
||||||
final nextNumber = await _sequenceRepository.getNextDocumentNumber(
|
|
||||||
'ddt',
|
|
||||||
);
|
|
||||||
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
document: state.document.copyWith(docNumber: nextNumber),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// Se qualcosa va storto, spegniamo lo switch e mostriamo l'errore
|
|
||||||
emit(state.copyWith(isAutoNumber: false, errorMessage: e.toString()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Se lo spegne, svuotiamo semplicemente il campo
|
|
||||||
emit(state.copyWith(document: state.document.copyWith(docNumber: '')));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Metodo unico e pulito per aggiornare i campi testuali/numerici del documento
|
// Metodo unico e pulito per aggiornare i campi testuali/numerici del documento
|
||||||
@@ -131,7 +112,7 @@ class TicketShippingCubit extends Cubit<TicketShippingState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> confirmShipment({required String newTicketStatus}) async {
|
Future<void> confirmShipment({required TicketStatus newTicketStatus}) async {
|
||||||
if (state.document.providerId.isEmpty ||
|
if (state.document.providerId.isEmpty ||
|
||||||
state.document.destinationLocationId.isEmpty) {
|
state.document.destinationLocationId.isEmpty) {
|
||||||
emit(
|
emit(
|
||||||
@@ -142,7 +123,7 @@ class TicketShippingCubit extends Cubit<TicketShippingState> {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (state.document.docNumber.trim().isEmpty) {
|
if (!state.isAutoNumber && state.document.docNumber.trim().isEmpty) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: TicketShippingStatus.failure,
|
status: TicketShippingStatus.failure,
|
||||||
@@ -153,6 +134,17 @@ class TicketShippingCubit extends Cubit<TicketShippingState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emit(state.copyWith(status: TicketShippingStatus.loading));
|
emit(state.copyWith(status: TicketShippingStatus.loading));
|
||||||
|
if (state.isAutoNumber) {
|
||||||
|
try {
|
||||||
|
final nextNumber = await _sequenceRepository.getNextDocumentNumber(
|
||||||
|
DocumentType.shipment.name,
|
||||||
|
);
|
||||||
|
updateDocument(docNumber: nextNumber);
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(isAutoNumber: false, errorMessage: e.toString()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await _repository.createShipmentDocument(
|
await _repository.createShipmentDocument(
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class TicketShippingState extends Equatable {
|
|||||||
required this.document,
|
required this.document,
|
||||||
this.availableProviders = const [],
|
this.availableProviders = const [],
|
||||||
this.availableLocations = const [],
|
this.availableLocations = const [],
|
||||||
this.isAutoNumber = false,
|
this.isAutoNumber = true,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
|
import 'package:flutter/foundation.dart' show kIsWeb, defaultTargetPlatform;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
|
import 'package:flux/features/documents/models/shipment_document_model.dart';
|
||||||
|
import 'package:flux/features/master_data/providers/models/provider_location_model.dart';
|
||||||
|
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
||||||
import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart';
|
import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart';
|
||||||
import 'package:flux/features/tickets/blocs/ticket_list_state.dart';
|
import 'package:flux/features/tickets/blocs/ticket_list_state.dart';
|
||||||
|
import 'package:flux/features/tickets/blocs/ticket_shipping_cubit.dart';
|
||||||
import 'package:flux/features/tickets/ui/widgets/ticket_list_card.dart';
|
import 'package:flux/features/tickets/ui/widgets/ticket_list_card.dart';
|
||||||
|
import 'package:flux/features/tickets/ui/widgets/ticket_shipping_modal.dart';
|
||||||
|
import 'package:flux/features/tickets/utils/ticket_shipping_pdf_service.dart';
|
||||||
|
import 'package:pdf/pdf.dart';
|
||||||
|
import 'package:printing/printing.dart';
|
||||||
|
|
||||||
class TicketList extends StatelessWidget {
|
class TicketList extends StatelessWidget {
|
||||||
final ScrollController scrollController;
|
final ScrollController scrollController;
|
||||||
@@ -35,8 +45,8 @@ class TicketList extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final ticket = state.tickets[index];
|
final ticket = state.tickets[index];
|
||||||
final isSelected = state.selectedTicketIds.contains(ticket.id);
|
final isSelected = state.selectedTickets.contains(ticket);
|
||||||
final isSelectionMode = state.selectedTicketIds.isNotEmpty;
|
final isSelectionMode = state.selectedTickets.isNotEmpty;
|
||||||
|
|
||||||
// Per Desktop mostriamo la checkbox vera e propria
|
// Per Desktop mostriamo la checkbox vera e propria
|
||||||
final isDesktop = MediaQuery.of(context).size.width > 800;
|
final isDesktop = MediaQuery.of(context).size.width > 800;
|
||||||
@@ -53,7 +63,7 @@ class TicketList extends StatelessWidget {
|
|||||||
AnimatedPositioned(
|
AnimatedPositioned(
|
||||||
duration: const Duration(milliseconds: 300),
|
duration: const Duration(milliseconds: 300),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
bottom: state.selectedTicketIds.isNotEmpty
|
bottom: state.selectedTickets.isNotEmpty
|
||||||
? 90
|
? 90
|
||||||
: -100, // Nasconde o mostra
|
: -100, // Nasconde o mostra
|
||||||
left: 16,
|
left: 16,
|
||||||
@@ -77,7 +87,7 @@ class TicketList extends StatelessWidget {
|
|||||||
context.read<TicketListCubit>().clearSelection(),
|
context.read<TicketListCubit>().clearSelection(),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${state.selectedTicketIds.length} selezionati',
|
'${state.selectedTickets.length} selezionati',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -86,11 +96,89 @@ class TicketList extends StatelessWidget {
|
|||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|
||||||
// IL NOSTRO FAMOSO BOTTONE SPEDISCI
|
// IL NOSTRO FAMOSO BOTTONE SPEDISCI
|
||||||
|
// IL BOTTONE SPEDISCI NELLA BARRA IN BASSO
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
// Qui lanceremo la modale per creare il DDT
|
// 1. Apriamo la modale e ASPETTIAMO il risultato (tipizzandolo come Record)
|
||||||
// passando la lista: state.selectedTicketIds.toList()
|
final result =
|
||||||
print("Spedisco i ticket: ${state.selectedTicketIds}");
|
await showModalBottomSheet<
|
||||||
|
({
|
||||||
|
ShipmentDocumentModel document,
|
||||||
|
ProviderModel provider,
|
||||||
|
ProviderLocationModel location,
|
||||||
|
})
|
||||||
|
>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (context) {
|
||||||
|
return BlocProvider(
|
||||||
|
create: (context) => TicketShippingCubit(
|
||||||
|
ticketIds: state.selectedTickets
|
||||||
|
.map((t) => t.id!)
|
||||||
|
.toList(),
|
||||||
|
)..loadRepairCenters(),
|
||||||
|
child: TicketShippingModal(
|
||||||
|
ticketIds: state.selectedTickets
|
||||||
|
.map((t) => t.id!)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Se l'utente ha chiuso trascinando giù, result è null.
|
||||||
|
// Se ha salvato con successo, result contiene il nostro Record!
|
||||||
|
if (result != null && context.mounted) {
|
||||||
|
try {
|
||||||
|
// Recuperiamo la Company (dal tuo SessionCubit o AuthCubit)
|
||||||
|
final company = context
|
||||||
|
.read<SessionCubit>()
|
||||||
|
.state
|
||||||
|
.company!;
|
||||||
|
|
||||||
|
final ticketListCubit = context
|
||||||
|
.read<TicketListCubit>();
|
||||||
|
|
||||||
|
// 3. GENERIAMO I BYTES DEL PDF
|
||||||
|
final pdfBytes =
|
||||||
|
await TicketShippingPdfService.generateDdt(
|
||||||
|
company: company,
|
||||||
|
provider: result.provider,
|
||||||
|
location: result.location,
|
||||||
|
document: result.document,
|
||||||
|
tickets: state.selectedTickets.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. LANCIAMO LA STAMPA (Anteprima nativa / Browser)
|
||||||
|
// Check sicuro: se NON siamo sul web E la piattaforma nativa è macOS
|
||||||
|
if (!kIsWeb &&
|
||||||
|
defaultTargetPlatform == TargetPlatform.macOS) {
|
||||||
|
// Scialuppa di salvataggio per il Mac
|
||||||
|
await Printing.sharePdf(
|
||||||
|
bytes: pdfBytes,
|
||||||
|
filename: 'ddt_${result.document.docNumber}.pdf',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Per Web, Windows, Linux, Android e iOS... diamo spettacolo!
|
||||||
|
await Printing.layoutPdf(
|
||||||
|
onLayout: (PdfPageFormat format) async =>
|
||||||
|
pdfBytes,
|
||||||
|
name: 'ddt_${result.document.docNumber}.pdf',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Pulizia finale: Deselezioniamo tutti i ticket e ricarichiamo la lista
|
||||||
|
ticketListCubit.clearSelection();
|
||||||
|
// (Se necessario, chiama il metodo per ricaricare la lista dei ticket dal DB)
|
||||||
|
ticketListCubit.loadTickets(refresh: true);
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Errore stampa PDF: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.local_shipping),
|
icon: const Icon(Icons.local_shipping),
|
||||||
label: const Text('Spedisci'),
|
label: const Text('Spedisci'),
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class TicketListCard extends StatelessWidget {
|
|||||||
if (ticket.id != null) {
|
if (ticket.id != null) {
|
||||||
context
|
context
|
||||||
.read<TicketListCubit>()
|
.read<TicketListCubit>()
|
||||||
.toggleTicketSelection(ticket.id!);
|
.toggleTicketSelection(ticket);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -61,7 +61,7 @@ class TicketListCard extends StatelessWidget {
|
|||||||
if (ticket.id != null) {
|
if (ticket.id != null) {
|
||||||
context
|
context
|
||||||
.read<TicketListCubit>()
|
.read<TicketListCubit>()
|
||||||
.toggleTicketSelection(ticket.id!);
|
.toggleTicketSelection(ticket);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: AnimatedSwitcher(
|
child: AnimatedSwitcher(
|
||||||
@@ -168,7 +168,7 @@ class TicketListCard extends StatelessWidget {
|
|||||||
// Modalità selezione attiva: un tap normale seleziona/deseleziona
|
// Modalità selezione attiva: un tap normale seleziona/deseleziona
|
||||||
if (ticket.id != null) {
|
if (ticket.id != null) {
|
||||||
context.read<TicketListCubit>().toggleTicketSelection(
|
context.read<TicketListCubit>().toggleTicketSelection(
|
||||||
ticket.id!,
|
ticket,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -184,7 +184,7 @@ class TicketListCard extends StatelessWidget {
|
|||||||
// Pressione lunga: forza la selezione (utile per iniziare il workflow)
|
// Pressione lunga: forza la selezione (utile per iniziare il workflow)
|
||||||
if (!isSelectionMode && ticket.id != null) {
|
if (!isSelectionMode && ticket.id != null) {
|
||||||
context.read<TicketListCubit>().toggleTicketSelection(
|
context.read<TicketListCubit>().toggleTicketSelection(
|
||||||
ticket.id!,
|
ticket,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
373
lib/features/tickets/ui/widgets/ticket_shipping_modal.dart
Normal file
373
lib/features/tickets/ui/widgets/ticket_shipping_modal.dart
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/features/tickets/blocs/ticket_shipping_cubit.dart';
|
||||||
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||||
|
|
||||||
|
class TicketShippingModal extends StatefulWidget {
|
||||||
|
final List<String> ticketIds;
|
||||||
|
|
||||||
|
const TicketShippingModal({super.key, required this.ticketIds});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TicketShippingModal> createState() => _TicketShippingModalState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TicketShippingModalState extends State<TicketShippingModal> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Appena si apre la modale, carichiamo la lista dei laboratori
|
||||||
|
context.read<TicketShippingCubit>().loadRepairCenters();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).viewInsets.bottom + 24,
|
||||||
|
top: 24,
|
||||||
|
left: 24,
|
||||||
|
right: 24,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
borderRadius: const BorderRadius.vertical(top: Radius.circular(28)),
|
||||||
|
),
|
||||||
|
child: BlocConsumer<TicketShippingCubit, TicketShippingState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.status == TicketShippingStatus.success) {
|
||||||
|
final provider = state.availableProviders.firstWhere(
|
||||||
|
(p) => p.id == state.document.providerId,
|
||||||
|
);
|
||||||
|
final location = state.availableLocations.firstWhere(
|
||||||
|
(l) => l.id == state.document.destinationLocationId,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Creiamo un Dart Record elegante e lo "spariamo" fuori
|
||||||
|
final ddtData = (
|
||||||
|
document: state.document,
|
||||||
|
provider: provider,
|
||||||
|
location: location,
|
||||||
|
);
|
||||||
|
|
||||||
|
Navigator.pop(context, ddtData);
|
||||||
|
}
|
||||||
|
if (state.status == TicketShippingStatus.failure &&
|
||||||
|
state.errorMessage != null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(state.errorMessage!),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.status == TicketShippingStatus.loading &&
|
||||||
|
state.availableProviders.isEmpty) {
|
||||||
|
return const SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final doc =
|
||||||
|
state.document; // Scorciatoia comoda per il nostro modello
|
||||||
|
|
||||||
|
return Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
_buildHeader(context, state),
|
||||||
|
const Divider(height: 32),
|
||||||
|
|
||||||
|
// 1. DESTINAZIONE
|
||||||
|
_buildSectionTitle(Icons.business, "Destinatario"),
|
||||||
|
_buildProviderDropdown(context, state),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
if (doc.providerId.isNotEmpty)
|
||||||
|
_buildLocationDropdown(context, state),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 2. DATI DOCUMENTO
|
||||||
|
_buildSectionTitle(Icons.description, "Dati Documento"),
|
||||||
|
_buildNumberingSection(context, state),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildDatePicker(context, state),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 3. DETTAGLI MERCE E TRASPORTO
|
||||||
|
_buildSectionTitle(Icons.inventory_2, "Dettagli Trasporto"),
|
||||||
|
_buildShippingDetails(context, state),
|
||||||
|
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// BOTTONE CONFERMA
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 54,
|
||||||
|
child: FilledButton.icon(
|
||||||
|
onPressed: state.status == TicketShippingStatus.loading
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
// Assicurati che lo stato qui coincida con l'Enum del tuo TicketStatus
|
||||||
|
context
|
||||||
|
.read<TicketShippingCubit>()
|
||||||
|
.confirmShipment(
|
||||||
|
newTicketStatus:
|
||||||
|
TicketStatus.waitingForReturn,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: state.status == TicketShippingStatus.loading
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.local_shipping),
|
||||||
|
label: const Text(
|
||||||
|
"GENERA DDT E SPEDISCI",
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildHeader(BuildContext context, TicketShippingState state) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
CircleAvatar(
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
child: Icon(
|
||||||
|
Icons.share_location,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Spedizione Multipla",
|
||||||
|
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"${widget.ticketIds.length} ticket pronti per il laboratorio",
|
||||||
|
style: const TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProviderDropdown(
|
||||||
|
BuildContext context,
|
||||||
|
TicketShippingState state,
|
||||||
|
) {
|
||||||
|
return DropdownButtonFormField<String>(
|
||||||
|
value: state.document.providerId.isEmpty
|
||||||
|
? null
|
||||||
|
: state.document.providerId,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Seleziona Centro Riparazioni",
|
||||||
|
prefixIcon: Icon(Icons.store),
|
||||||
|
),
|
||||||
|
items: state.availableProviders
|
||||||
|
.map((p) => DropdownMenuItem(value: p.id, child: Text(p.name)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (val) => val != null
|
||||||
|
? context.read<TicketShippingCubit>().selectProvider(val)
|
||||||
|
: null,
|
||||||
|
validator: (v) => v == null || v.isEmpty ? 'Campo obbligatorio' : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildLocationDropdown(
|
||||||
|
BuildContext context,
|
||||||
|
TicketShippingState state,
|
||||||
|
) {
|
||||||
|
return DropdownButtonFormField<String>(
|
||||||
|
value: state.document.destinationLocationId.isEmpty
|
||||||
|
? null
|
||||||
|
: state.document.destinationLocationId,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Sede di destinazione",
|
||||||
|
prefixIcon: Icon(Icons.location_on),
|
||||||
|
),
|
||||||
|
items: state.availableLocations
|
||||||
|
.map((l) => DropdownMenuItem(value: l.id, child: Text(l.name)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (val) => val != null
|
||||||
|
? context.read<TicketShippingCubit>().selectLocation(val)
|
||||||
|
: null,
|
||||||
|
validator: (v) => v == null || v.isEmpty ? 'Campo obbligatorio' : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNumberingSection(
|
||||||
|
BuildContext context,
|
||||||
|
TicketShippingState state,
|
||||||
|
) {
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: state.isAutoNumber
|
||||||
|
? const Text('Numero auto-generato alla conferma')
|
||||||
|
: TextFormField(
|
||||||
|
// Key è fondamentale per far aggiornare il campo quando cambia da auto a manuale
|
||||||
|
key: ValueKey('docNum_${state.isAutoNumber}'),
|
||||||
|
initialValue: state.document.docNumber,
|
||||||
|
readOnly: state.isAutoNumber, // Bloccato se automatico
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Numero DDT",
|
||||||
|
helperText: state.isAutoNumber
|
||||||
|
? "Generato automaticamente"
|
||||||
|
: "Inserimento manuale",
|
||||||
|
),
|
||||||
|
onChanged: (val) => context
|
||||||
|
.read<TicketShippingCubit>()
|
||||||
|
.updateDocument(docNumber: val),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
// Switch per modalità automatica
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Auto",
|
||||||
|
style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: state.isAutoNumber,
|
||||||
|
onChanged: (val) =>
|
||||||
|
context.read<TicketShippingCubit>().toggleAutoNumber(val),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDatePicker(BuildContext context, TicketShippingState state) {
|
||||||
|
final date = state.document.docDate;
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text("Data Documento"),
|
||||||
|
subtitle: Text(
|
||||||
|
"${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}",
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.calendar_month),
|
||||||
|
onTap: () async {
|
||||||
|
final newDate = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: date,
|
||||||
|
firstDate: DateTime.now().subtract(const Duration(days: 365)),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
);
|
||||||
|
if (newDate != null) {
|
||||||
|
context.read<TicketShippingCubit>().updateDocument(docDate: newDate);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildShippingDetails(
|
||||||
|
BuildContext context,
|
||||||
|
TicketShippingState state,
|
||||||
|
) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
initialValue: state.document.packageCount.toString(),
|
||||||
|
decoration: const InputDecoration(labelText: "N. Colli"),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
onChanged: (val) => context
|
||||||
|
.read<TicketShippingCubit>()
|
||||||
|
.updateDocument(packageCount: int.tryParse(val) ?? 1),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
initialValue: state.document.weight?.toString() ?? '',
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Peso",
|
||||||
|
suffixText: "Kg",
|
||||||
|
),
|
||||||
|
keyboardType: const TextInputType.numberWithOptions(
|
||||||
|
decimal: true,
|
||||||
|
),
|
||||||
|
onChanged: (val) => context
|
||||||
|
.read<TicketShippingCubit>()
|
||||||
|
.updateDocument(weight: double.tryParse(val)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: state.document.shippingReason,
|
||||||
|
decoration: const InputDecoration(labelText: "Causale Trasporto"),
|
||||||
|
onChanged: (val) => context
|
||||||
|
.read<TicketShippingCubit>()
|
||||||
|
.updateDocument(shippingReason: val),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: state.document.notes ?? '',
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Aspetto Beni (es. Scatola, Busta)",
|
||||||
|
),
|
||||||
|
onChanged: (val) =>
|
||||||
|
context.read<TicketShippingCubit>().updateDocument(notes: val),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionTitle(IconData icon, String title) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 18, color: Colors.grey),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
title.toUpperCase(),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey,
|
||||||
|
letterSpacing: 1.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
205
lib/features/tickets/utils/ticket_shipping_pdf_service.dart
Normal file
205
lib/features/tickets/utils/ticket_shipping_pdf_service.dart
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
import 'package:flux/features/company/models/company_model.dart';
|
||||||
|
import 'package:flux/features/documents/models/shipment_document_model.dart';
|
||||||
|
import 'package:flux/features/master_data/providers/models/provider_location_model.dart';
|
||||||
|
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
||||||
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||||
|
import 'package:pdf/pdf.dart';
|
||||||
|
import 'package:pdf/widgets.dart' as pw;
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
|
class TicketShippingPdfService {
|
||||||
|
static Future<Uint8List> generateDdt({
|
||||||
|
required CompanyModel company,
|
||||||
|
required ProviderModel provider,
|
||||||
|
required ProviderLocationModel location,
|
||||||
|
required ShipmentDocumentModel document,
|
||||||
|
required List<TicketModel> tickets,
|
||||||
|
}) async {
|
||||||
|
final pdf = pw.Document();
|
||||||
|
|
||||||
|
// Formattatore per le date
|
||||||
|
final dateFormat = DateFormat('dd/MM/yyyy');
|
||||||
|
|
||||||
|
pdf.addPage(
|
||||||
|
pw.MultiPage(
|
||||||
|
pageFormat: PdfPageFormat.a4,
|
||||||
|
margin: const pw.EdgeInsets.all(32),
|
||||||
|
// --- INTESTAZIONE (Ripetuta su ogni pagina) ---
|
||||||
|
header: (pw.Context context) {
|
||||||
|
return pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Dati Mittente (La tua Company)
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Text(
|
||||||
|
company.name,
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.Text(company.address),
|
||||||
|
pw.Text(
|
||||||
|
'${company.city} (${company.province}) - ${company.zipCode}',
|
||||||
|
),
|
||||||
|
pw.Text('P.IVA: ${company.vatId}'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Dati Destinatario (Il Laboratorio e la Sede)
|
||||||
|
pw.Expanded(
|
||||||
|
child: pw.Container(
|
||||||
|
padding: const pw.EdgeInsets.all(8),
|
||||||
|
decoration: pw.BoxDecoration(
|
||||||
|
border: pw.Border.all(color: PdfColors.grey),
|
||||||
|
borderRadius: const pw.BorderRadius.all(
|
||||||
|
pw.Radius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: pw.Column(
|
||||||
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
pw.Text(
|
||||||
|
'DESTINATARIO:',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: PdfColors.grey700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.Text(
|
||||||
|
provider.name,
|
||||||
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 4),
|
||||||
|
pw.Text('Destinazione merce:'),
|
||||||
|
pw.Text(location.address),
|
||||||
|
pw.Text(
|
||||||
|
'${location.zipCode} ${location.city} (${location.province})',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
// Titolo Documento
|
||||||
|
pw.Center(
|
||||||
|
child: pw.Text(
|
||||||
|
'DOCUMENTO DI TRASPORTO (D.D.T.)',
|
||||||
|
style: pw.TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 10),
|
||||||
|
// Dati Documento
|
||||||
|
pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
pw.Text(
|
||||||
|
'Numero: ${document.docNumber}',
|
||||||
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
pw.Text(
|
||||||
|
'Data: ${dateFormat.format(document.docDate)}',
|
||||||
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold),
|
||||||
|
),
|
||||||
|
pw.Text('Causale: ${document.shippingReason}'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- IL CORPO (La tabella dei ticket che scorre) ---
|
||||||
|
build: (pw.Context context) {
|
||||||
|
return [
|
||||||
|
pw.TableHelper.fromTextArray(
|
||||||
|
headers: ['Rif. Ticket', 'Modello', 'Difetto / Note', 'Q.tà'],
|
||||||
|
headerStyle: pw.TextStyle(
|
||||||
|
fontWeight: pw.FontWeight.bold,
|
||||||
|
color: PdfColors.white,
|
||||||
|
),
|
||||||
|
headerDecoration: const pw.BoxDecoration(
|
||||||
|
color: PdfColors.blueGrey800,
|
||||||
|
),
|
||||||
|
cellAlignment: pw.Alignment.centerLeft,
|
||||||
|
data: tickets.map((t) {
|
||||||
|
return [
|
||||||
|
t.id?.substring(0, 8).toUpperCase() ??
|
||||||
|
'-', // Magari hai un ID progressivo migliore
|
||||||
|
t.targetModelName ?? 'Sconosciuto',
|
||||||
|
t.request,
|
||||||
|
'1', // Tipicamente 1 per ogni ticket
|
||||||
|
];
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
|
||||||
|
// --- PIÈ DI PAGINA (Ripetuto su ogni pagina, ma con le firme) ---
|
||||||
|
footer: (pw.Context context) {
|
||||||
|
return pw.Column(
|
||||||
|
children: [
|
||||||
|
pw.Divider(),
|
||||||
|
pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.spaceAround,
|
||||||
|
children: [
|
||||||
|
pw.Text('Colli: ${document.packageCount}'),
|
||||||
|
pw.Text('Peso: ${document.weight ?? '-'} Kg'),
|
||||||
|
pw.Text(
|
||||||
|
'Aspetto: ${document.notes ?? 'Scatola'}',
|
||||||
|
), // Adatta se hai un campo specifico
|
||||||
|
],
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 20),
|
||||||
|
// Spazio Firme
|
||||||
|
pw.Row(
|
||||||
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_buildSignatureBox('Firma Mittente'),
|
||||||
|
_buildSignatureBox('Firma Vettore (Corriere)'),
|
||||||
|
_buildSignatureBox('Firma Destinatario'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
pw.SizedBox(height: 10),
|
||||||
|
pw.Align(
|
||||||
|
alignment: pw.Alignment.centerRight,
|
||||||
|
child: pw.Text(
|
||||||
|
'Pagina ${context.pageNumber} di ${context.pagesCount}',
|
||||||
|
style: const pw.TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: PdfColors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return pdf.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
static pw.Widget _buildSignatureBox(String title) {
|
||||||
|
return pw.Column(
|
||||||
|
children: [
|
||||||
|
pw.Text(title, style: const pw.TextStyle(fontSize: 10)),
|
||||||
|
pw.SizedBox(height: 40),
|
||||||
|
pw.Container(width: 120, height: 1, color: PdfColors.black),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|||||||
import 'package:flux/features/attachments/data/attachments_repository.dart';
|
import 'package:flux/features/attachments/data/attachments_repository.dart';
|
||||||
import 'package:flux/features/auth/bloc/auth_cubit.dart';
|
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/documents/data/tickets_shipment_repository.dart';
|
||||||
import 'package:flux/features/master_data/providers/blocs/provider_list_cubit.dart';
|
import 'package:flux/features/master_data/providers/blocs/provider_list_cubit.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';
|
||||||
@@ -126,6 +127,9 @@ Future<void> setupLocator() async {
|
|||||||
);
|
);
|
||||||
getIt.registerLazySingleton<CompanyRepository>(() => CompanyRepository());
|
getIt.registerLazySingleton<CompanyRepository>(() => CompanyRepository());
|
||||||
getIt.registerLazySingleton<TrackingRepository>(() => TrackingRepository());
|
getIt.registerLazySingleton<TrackingRepository>(() => TrackingRepository());
|
||||||
|
getIt.registerLazySingleton<TicketsShipmentRepository>(
|
||||||
|
() => TicketsShipmentRepository(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class FluxApp extends StatefulWidget {
|
class FluxApp extends StatefulWidget {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
<key>com.apple.security.device.audio-input</key>
|
<key>com.apple.security.device.audio-input</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.print</key>
|
<key>com.apple.security.print</key>
|
||||||
|
<key>com.apple.security.files.downloads.read-write</key>
|
||||||
|
<true/>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.cs.allow-jit</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
<key>com.apple.security.files.downloads.read-write</key>
|
<key>com.apple.security.files.downloads.read-write</key>
|
||||||
<true/>
|
<true/>
|
||||||
@@ -19,6 +20,8 @@
|
|||||||
<key>com.apple.security.files.user-selected.read-write</key>
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.print</key>
|
<key>com.apple.security.print</key>
|
||||||
|
<key>com.apple.security.files.downloads.read-write</key>
|
||||||
|
<true/>
|
||||||
<true/>
|
<true/>
|
||||||
|
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
Reference in New Issue
Block a user