v
This commit is contained in:
@@ -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)
|
||||
# -----------------------------------------------------------------
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,45 +148,23 @@ class TicketList extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
|
||||
// IL NOSTRO FAMOSO BOTTONE SPEDISCI
|
||||
// IL BOTTONE SPEDISCI NELLA BARRA IN BASSO
|
||||
Row(
|
||||
children: [
|
||||
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(),
|
||||
onPressed: () => _setStatusClosed(context),
|
||||
icon: const Icon(Icons.approval),
|
||||
label: const Text('Riconsegna'),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
},
|
||||
const SizedBox(width: 8),
|
||||
FilledButton.icon(
|
||||
onPressed: () => _showShippingModal(context),
|
||||
icon: const Icon(Icons.local_shipping),
|
||||
label: const Text('Spedisci'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user