v
Some checks failed
Build and Release FLUX (Multi-Platform) / build-android (push) Successful in 1m52s
Build and Release FLUX (Multi-Platform) / build-web (push) Successful in 1m13s
Build and Release FLUX (Multi-Platform) / build-windows (push) Has been cancelled

This commit is contained in:
2026-05-24 12:42:11 +02:00
parent 123c006a1e
commit 879c848d77
7 changed files with 251 additions and 39 deletions

View File

@@ -1,13 +1,16 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/tickets/models/ticket_model.dart';
import 'package:flux/features/tickets/data/ticket_repository.dart';
import 'package:flux/features/tracking/data/tracking_repository.dart';
import 'package:flux/features/tracking/models/tracking_model.dart';
import 'package:get_it/get_it.dart';
import 'ticket_list_state.dart';
class TicketListCubit extends Cubit<TicketListState> {
final TicketRepository _repository = GetIt.I.get<TicketRepository>();
final TrackingRepository _trackingRepository = GetIt.I
.get<TrackingRepository>();
static const int _limit = 20; // Paginazione a blocchi di 20
TicketListCubit() : super(const TicketListState()) {
@@ -95,4 +98,64 @@ class TicketListCubit extends Cubit<TicketListState> {
void selectAll(List<TicketModel> tickets) {
emit(state.copyWith(selectedTickets: tickets.toSet()));
}
Future<void> closeTicketsBulk({
required List<String> ticketIds,
Map<String, bool>? loanReturns,
}) async {
// 1. Escludiamo i ticket per cui NON è stato restituito il muletto
if (loanReturns != null) {
for (final map in loanReturns.entries) {
if (!map.value) {
ticketIds.remove(map.key);
}
}
}
// Se non c'è più nulla da chiudere (es. ha rifiutato tutto), usciamo
if (ticketIds.isEmpty) {
clearSelection();
return;
}
// 2. Prepariamo i ticket per il DB
final List<TicketModel> ticketsToUpdate = [];
for (final ticketId in ticketIds) {
final ticket = state.tickets
.firstWhere((ticket) => ticket.id == ticketId)
.copyWith(ticketStatus: TicketStatus.closed);
ticketsToUpdate.add(ticket);
}
// 3. Salviamo su DB (in background)
for (final ticket in ticketsToUpdate) {
await _repository.updateTicket(ticket);
await _trackingRepository.logQuickEvent(
companyId: ticket.companyId,
message: 'Ticket chiuso - Riconsegnato',
type: TrackingType.statusChange,
parentId: ticket.id!,
parentType: TrackingParentType.ticket,
);
}
// 4. LA MAGIA: AGGIORNAMENTO LOCALE ISTANTANEO
final updatedTickets = state.tickets.map((t) {
if (ticketIds.contains(t.id)) {
return t.copyWith(ticketStatus: TicketStatus.closed);
}
return t;
}).toList();
// 5. Emettiamo il nuovo stato aggiornato e puliamo la selezione in un colpo solo
emit(
state.copyWith(
tickets: updatedTickets,
selectedTickets: {}, // Equivalente di clearSelection()
),
);
// Opzionale: Se vuoi comunque riallinearti al server in modo silenzioso dopo l'animazione
// loadTickets(refresh: true);
}
}

View File

@@ -259,6 +259,18 @@ class TicketRepository {
}
}
/// Chiude i ticket in bulk
Future<void> closeTickets(List<String> ticketIds) async {
try {
await _supabase
.from(_tableName)
.update({'ticket_status': TicketStatus.closed.value})
.inFilter('id', ticketIds);
} catch (e) {
throw Exception('Errore nella chiusura dei ticket: $e');
}
}
/// Elimina (o annulla) un ticket
Future<void> deleteTicket(String ticketId) async {
try {

View File

@@ -46,6 +46,11 @@ class _TicketListScreenState extends State<TicketListScreen> {
appBar: AppBar(
title: const Text('Assistenza & Riparazioni'),
actions: [
IconButton(
onPressed: () =>
context.read<TicketListCubit>().loadTickets(refresh: true),
icon: const Icon(Icons.refresh),
),
// Tasto per filtri avanzati (Data, Staff, Tipo) -> Da fare in un BottomSheet!
IconButton(
icon: const Icon(Icons.filter_list),

View File

@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flux/features/tickets/models/ticket_model.dart';
class LoanPhoneReturnDialog extends StatefulWidget {
final List<TicketModel> ticketsWithLoans;
const LoanPhoneReturnDialog({super.key, required this.ticketsWithLoans});
@override
State<LoanPhoneReturnDialog> createState() => _LoanPhoneReturnDialogState();
}
class _LoanPhoneReturnDialogState extends State<LoanPhoneReturnDialog> {
// Mappa per tenere traccia delle scelte: { ticketId: true/false }
final Map<String, bool> _returnStatuses = {};
@override
void initState() {
super.initState();
// Inizializziamo tutto a "true" (di default presumiamo che lo stia restituendo)
for (var ticket in widget.ticketsWithLoans) {
_returnStatuses[ticket.id!] = true;
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Row(
children: [
Icon(Icons.warning_amber_rounded, color: Colors.orange),
SizedBox(width: 8),
Text('Telefoni di cortesia'),
],
),
content: SizedBox(
width: double.maxFinite,
// Usiamo ListView.builder in caso ce ne siano tanti
child: ListView.separated(
shrinkWrap: true,
itemCount: widget.ticketsWithLoans.length,
separatorBuilder: (context, index) => const Divider(),
itemBuilder: (context, index) {
final ticket = widget.ticketsWithLoans[index];
final customerName = ticket.customer?.name ?? 'Cliente';
return SwitchListTile(
title: Text(
'$customerName ha un telefono di cortesia in prestito.',
),
subtitle: const Text('Confermi la riconsegna?'),
value: _returnStatuses[ticket.id!] ?? true,
activeThumbColor: Theme.of(context).colorScheme.primary,
onChanged: (bool value) {
setState(() {
_returnStatuses[ticket.id!] = value;
});
},
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(null), // Annulla tutto
child: const Text('Annulla'),
),
FilledButton(
onPressed: () =>
Navigator.of(context).pop(_returnStatuses), // Passa la mappa
child: const Text('Conferma'),
),
],
);
}
}

View File

@@ -1,9 +1,9 @@
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';
@@ -17,6 +17,67 @@ class TicketList extends StatelessWidget {
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
);
}
}
@override
Widget build(BuildContext context) {
return Stack(
@@ -87,42 +148,20 @@ class TicketList extends StatelessWidget {
),
),
const Spacer(),
// IL NOSTRO FAMOSO BOTTONE SPEDISCI
// IL BOTTONE SPEDISCI NELLA BARRA IN BASSO
FilledButton.icon(
onPressed: () 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,
);
}
},
icon: const Icon(Icons.local_shipping),
label: const Text('Spedisci'),
Row(
children: [
FilledButton.icon(
onPressed: () => _setStatusClosed(context),
icon: const Icon(Icons.approval),
label: const Text('Riconsegna'),
),
const SizedBox(width: 8),
FilledButton.icon(
onPressed: () => _showShippingModal(context),
icon: const Icon(Icons.local_shipping),
label: const Text('Spedisci'),
),
],
),
],
),