Files
flux/lib/features/tickets/ui/widgets/ticket_list.dart
2026-06-02 11:52:31 +02:00

216 lines
8.4 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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_shipping_cubit.dart';
import 'package:flux/features/tickets/ui/widgets/loan_phone_return_dialog.dart';
import 'package:flux/features/tickets/ui/widgets/ticket_list_card.dart';
import 'package:flux/features/tickets/ui/widgets/ticket_shipping_modal.dart';
class TicketList extends StatelessWidget {
final ScrollController scrollController;
final TicketListState state;
const TicketList({
super.key,
required this.scrollController,
required this.state,
});
void _showShippingModal(BuildContext context) async {
// 1. Apriamo la modale e ASPETTIAMO il risultato (tipizzandolo come Record)
final bool? result = await showModalBottomSheet<bool?>(
context: context,
isScrollControlled: true,
builder: (context) {
return BlocProvider(
create: (context) =>
TicketShippingCubit(tickets: state.selectedTickets.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) {
// 5. Pulizia finale: Deselezioniamo tutti i ticket e ricarichiamo la lista
context.read<TicketListCubit>().clearSelection();
// (Se necessario, chiama il metodo per ricaricare la lista dei ticket dal DB)
context.read<TicketListCubit>().loadTickets(refresh: true);
}
}
void _setStatusClosed(BuildContext context) async {
// 1. Filtriamo solo i ticket che hanno un telefono in prestito
final ticketsWithLoans = state.selectedTickets
.where((t) => t.hasCourtesyDevice == true)
.toList();
// Prepariamo la variabile per contenere i telefoni restituiti (se ce ne sono)
Map<String, bool>? loanReturns;
// 2. Se ci sono telefoni in prestito, mostriamo il popup
if (ticketsWithLoans.isNotEmpty) {
loanReturns = await showDialog<Map<String, bool>>(
context: context,
builder: (context) =>
LoanPhoneReturnDialog(ticketsWithLoans: ticketsWithLoans),
);
// Se l'utente ha premuto fuori o ha fatto "Annulla", blocchiamo l'operazione bulk
if (loanReturns == null) return;
}
// 3. Se siamo qui, o non c'erano muletti, o l'utente ha confermato il popup.
// Lanciamo l'azione sul Cubit! (Dovrai creare/adattare questo metodo nel tuo Cubit)
if (context.mounted) {
final ticketIds = state.selectedTickets.map((t) => t.id!).toList();
// Passiamo gli ID dei ticket da chiudere e la mappa delle restituzioni
context.read<TicketListCubit>().closeTicketsBulk(
ticketIds: ticketIds,
loanReturns: loanReturns, // Può essere null se non c'erano muletti
);
}
}
void _deleteTickets(BuildContext context) {
context.read<TicketListCubit>().deleteTickets(
state.selectedTickets.toList(),
);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
ListView.builder(
controller: scrollController,
itemCount: state.hasReachedMax
? state.tickets.length
: state.tickets.length + 1,
itemBuilder: (context, index) {
// Se siamo all'ultimo elemento e non abbiamo raggiunto il max, mostriamo il loader
if (index >= state.tickets.length) {
return const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: CircularProgressIndicator(),
),
);
}
final ticket = state.tickets[index];
final isSelected = state.selectedTickets.contains(ticket);
final isSelectionMode = state.selectedTickets.isNotEmpty;
// Per Desktop mostriamo la checkbox vera e propria
final isDesktop = MediaQuery.of(context).size.width > 800;
return TicketListCard(
ticket: ticket,
isSelected: isSelected,
isSelectionMode: isSelectionMode,
isDesktop: isDesktop,
);
},
),
// 2. LA BARRA DELLE AZIONI BULK (Appare magicamente dal basso)
AnimatedPositioned(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
bottom: state.selectedTickets.isNotEmpty ? 90 : -100,
// Mettiamo left e right a 0 per far occupare tutta la larghezza invisibile
left: 0,
right: 0,
child: Align(
alignment: Alignment.bottomCenter,
// 1. IL LIMITE MASSIMO: Su desktop non supererà mai i 600px, su mobile si restringe da solo
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Card(
elevation: 8,
color: Theme.of(context).colorScheme.inverseSurface,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
16,
), // Qui possiamo giocare coi bordi
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 8.0,
),
// 2. LA ROW PRINCIPALE: Spinge tutto ai due estremi del nostro "dock"
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// BLOCCO SINISTRO: Chiusura e Contatore
Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.close),
color: Theme.of(
context,
).colorScheme.onInverseSurface,
onPressed: () => context
.read<TicketListCubit>()
.clearSelection(),
),
const SizedBox(width: 8),
Text(
'${state.selectedTickets.length} selezionati',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Theme.of(
context,
).colorScheme.onInverseSurface,
),
),
],
),
// BLOCCO DESTRO: Wrap confinato solo ai bottoni
Wrap(
spacing: 8.0,
runSpacing: 8.0,
alignment: WrapAlignment.end,
children: [
IconButton.filled(
tooltip: 'Elimina',
onPressed: () => _deleteTickets(context),
icon: const Icon(Icons.delete),
),
IconButton.filled(
tooltip: 'Riconsegna',
onPressed: () => _setStatusClosed(context),
icon: const Icon(Icons.approval),
),
IconButton.filled(
tooltip: 'Spedisci',
onPressed: () => _showShippingModal(context),
icon: const Icon(Icons.local_shipping),
),
],
),
],
),
),
),
),
),
),
),
],
);
}
}