From 879c848d771f56e1db73838071042a63d10d2d93 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sun, 24 May 2026 12:42:11 +0200 Subject: [PATCH] v --- .gitea/workflows/release.yaml | 16 +++ .../tickets/blocs/ticket_list_cubit.dart | 65 +++++++++- .../tickets/data/ticket_repository.dart | 12 ++ .../tickets/ui/ticket_list_screen.dart | 5 + .../ui/widgets/loan_phone_return_dialog.dart | 77 ++++++++++++ .../tickets/ui/widgets/ticket_list.dart | 113 ++++++++++++------ pubspec.yaml | 2 +- 7 files changed, 251 insertions(+), 39 deletions(-) create mode 100644 lib/features/tickets/ui/widgets/loan_phone_return_dialog.dart diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index 6e568dc..8aed393 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -33,6 +33,14 @@ jobs: files: "build/windows/installer/FluxInstaller.exe" api_key: ${{ secrets.MYRELEASE_TOKEN }} + - name: Aggiorna Link su Supabase + run: | + curl.exe -X PATCH "https://pvqpjloswwvtfoxbkfbh.supabase.co/rest/v1/app_config?id=eq.1" \ + -H "apikey: ${{ secrets.SUPABASE_SERVICE_KEY }}" \ + -H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_KEY }}" \ + -H "Content-Type: application/json" \ + -d "{\"download_url_apk\": \"https://gitea.catelli.it/brontomark/flux/releases/download/${{ github.ref_name }}/FluxInstaller.exe\"}" + - name: Pulisci Workspace Windows if: always() run: Remove-Item -Recurse -Force ./* @@ -63,6 +71,14 @@ jobs: files: "build/app/outputs/flutter-apk/app-release.apk" api_key: ${{ secrets.MYRELEASE_TOKEN }} + - name: Aggiorna Link su Supabase + run: | + curl -X PATCH "https://pvqpjloswwvtfoxbkfbh.supabase.co/rest/v1/app_config?id=eq.1" \ + -H "apikey: ${{ secrets.SUPABASE_SERVICE_KEY }}" \ + -H "Authorization: Bearer ${{ secrets.SUPABASE_SERVICE_KEY }}" \ + -H "Content-Type: application/json" \ + -d "{\"download_url_apk\": \"https://gitea.catelli.it/brontomark/flux/releases/download/${{ github.ref_name }}/app-release.apk\"}" + # ----------------------------------------------------------------- # JOB 3: WEB & CLOUDFLARE DEPLOY (Gira sul tuo MacBook) # ----------------------------------------------------------------- diff --git a/lib/features/tickets/blocs/ticket_list_cubit.dart b/lib/features/tickets/blocs/ticket_list_cubit.dart index ff5a900..d97c3f8 100644 --- a/lib/features/tickets/blocs/ticket_list_cubit.dart +++ b/lib/features/tickets/blocs/ticket_list_cubit.dart @@ -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 { final TicketRepository _repository = GetIt.I.get(); + final TrackingRepository _trackingRepository = GetIt.I + .get(); static const int _limit = 20; // Paginazione a blocchi di 20 TicketListCubit() : super(const TicketListState()) { @@ -95,4 +98,64 @@ class TicketListCubit extends Cubit { void selectAll(List tickets) { emit(state.copyWith(selectedTickets: tickets.toSet())); } + + Future closeTicketsBulk({ + required List ticketIds, + Map? 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 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); + } } diff --git a/lib/features/tickets/data/ticket_repository.dart b/lib/features/tickets/data/ticket_repository.dart index 1a99707..4af2000 100644 --- a/lib/features/tickets/data/ticket_repository.dart +++ b/lib/features/tickets/data/ticket_repository.dart @@ -259,6 +259,18 @@ class TicketRepository { } } + /// Chiude i ticket in bulk + Future closeTickets(List 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 deleteTicket(String ticketId) async { try { diff --git a/lib/features/tickets/ui/ticket_list_screen.dart b/lib/features/tickets/ui/ticket_list_screen.dart index 2c3160c..0e8c16e 100644 --- a/lib/features/tickets/ui/ticket_list_screen.dart +++ b/lib/features/tickets/ui/ticket_list_screen.dart @@ -46,6 +46,11 @@ class _TicketListScreenState extends State { appBar: AppBar( title: const Text('Assistenza & Riparazioni'), actions: [ + IconButton( + onPressed: () => + context.read().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), diff --git a/lib/features/tickets/ui/widgets/loan_phone_return_dialog.dart b/lib/features/tickets/ui/widgets/loan_phone_return_dialog.dart new file mode 100644 index 0000000..069ee9b --- /dev/null +++ b/lib/features/tickets/ui/widgets/loan_phone_return_dialog.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flux/features/tickets/models/ticket_model.dart'; + +class LoanPhoneReturnDialog extends StatefulWidget { + final List ticketsWithLoans; + + const LoanPhoneReturnDialog({super.key, required this.ticketsWithLoans}); + + @override + State createState() => _LoanPhoneReturnDialogState(); +} + +class _LoanPhoneReturnDialogState extends State { + // Mappa per tenere traccia delle scelte: { ticketId: true/false } + final Map _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'), + ), + ], + ); + } +} diff --git a/lib/features/tickets/ui/widgets/ticket_list.dart b/lib/features/tickets/ui/widgets/ticket_list.dart index eb06b77..abc5504 100644 --- a/lib/features/tickets/ui/widgets/ticket_list.dart +++ b/lib/features/tickets/ui/widgets/ticket_list.dart @@ -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( + 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().clearSelection(); + // (Se necessario, chiama il metodo per ricaricare la lista dei ticket dal DB) + context.read().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? loanReturns; + + // 2. Se ci sono telefoni in prestito, mostriamo il popup + if (ticketsWithLoans.isNotEmpty) { + loanReturns = await showDialog>( + 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().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( - 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().clearSelection(); - // (Se necessario, chiama il metodo per ricaricare la lista dei ticket dal DB) - context.read().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'), + ), + ], ), ], ), diff --git a/pubspec.yaml b/pubspec.yaml index a1c467d..f725f26 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: flux description: "Gestione attività negozio di telefonia" publish_to: 'none' -version: 1.0.12+12 +version: 1.0.13+13 environment: sdk: ^3.11.3