v
This commit is contained in:
@@ -33,6 +33,14 @@ jobs:
|
|||||||
files: "build/windows/installer/FluxInstaller.exe"
|
files: "build/windows/installer/FluxInstaller.exe"
|
||||||
api_key: ${{ secrets.MYRELEASE_TOKEN }}
|
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
|
- name: Pulisci Workspace Windows
|
||||||
if: always()
|
if: always()
|
||||||
run: Remove-Item -Recurse -Force ./*
|
run: Remove-Item -Recurse -Force ./*
|
||||||
@@ -63,6 +71,14 @@ jobs:
|
|||||||
files: "build/app/outputs/flutter-apk/app-release.apk"
|
files: "build/app/outputs/flutter-apk/app-release.apk"
|
||||||
api_key: ${{ secrets.MYRELEASE_TOKEN }}
|
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)
|
# JOB 3: WEB & CLOUDFLARE DEPLOY (Gira sul tuo MacBook)
|
||||||
# -----------------------------------------------------------------
|
# -----------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
|
|
||||||
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/features/tickets/models/ticket_model.dart';
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||||
import 'package:flux/features/tickets/data/ticket_repository.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 'package:get_it/get_it.dart';
|
||||||
import 'ticket_list_state.dart';
|
import 'ticket_list_state.dart';
|
||||||
|
|
||||||
class TicketListCubit extends Cubit<TicketListState> {
|
class TicketListCubit extends Cubit<TicketListState> {
|
||||||
final TicketRepository _repository = GetIt.I.get<TicketRepository>();
|
final TicketRepository _repository = GetIt.I.get<TicketRepository>();
|
||||||
|
final TrackingRepository _trackingRepository = GetIt.I
|
||||||
|
.get<TrackingRepository>();
|
||||||
static const int _limit = 20; // Paginazione a blocchi di 20
|
static const int _limit = 20; // Paginazione a blocchi di 20
|
||||||
|
|
||||||
TicketListCubit() : super(const TicketListState()) {
|
TicketListCubit() : super(const TicketListState()) {
|
||||||
@@ -95,4 +98,64 @@ class TicketListCubit extends Cubit<TicketListState> {
|
|||||||
void selectAll(List<TicketModel> tickets) {
|
void selectAll(List<TicketModel> tickets) {
|
||||||
emit(state.copyWith(selectedTickets: tickets.toSet()));
|
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
|
/// Elimina (o annulla) un ticket
|
||||||
Future<void> deleteTicket(String ticketId) async {
|
Future<void> deleteTicket(String ticketId) async {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ class _TicketListScreenState extends State<TicketListScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Assistenza & Riparazioni'),
|
title: const Text('Assistenza & Riparazioni'),
|
||||||
actions: [
|
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!
|
// Tasto per filtri avanzati (Data, Staff, Tipo) -> Da fare in un BottomSheet!
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.filter_list),
|
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/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.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_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/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_list_card.dart';
|
||||||
import 'package:flux/features/tickets/ui/widgets/ticket_shipping_modal.dart';
|
import 'package:flux/features/tickets/ui/widgets/ticket_shipping_modal.dart';
|
||||||
|
|
||||||
@@ -17,6 +17,67 @@ class TicketList extends StatelessWidget {
|
|||||||
required this.state,
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Stack(
|
return Stack(
|
||||||
@@ -87,45 +148,23 @@ class TicketList extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
Row(
|
||||||
// IL NOSTRO FAMOSO BOTTONE SPEDISCI
|
children: [
|
||||||
// IL BOTTONE SPEDISCI NELLA BARRA IN BASSO
|
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
onPressed: () async {
|
onPressed: () => _setStatusClosed(context),
|
||||||
// 1. Apriamo la modale e ASPETTIAMO il risultato (tipizzandolo come Record)
|
icon: const Icon(Icons.approval),
|
||||||
final bool? result = await showModalBottomSheet<bool?>(
|
label: const Text('Riconsegna'),
|
||||||
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(),
|
|
||||||
),
|
),
|
||||||
);
|
const SizedBox(width: 8),
|
||||||
},
|
FilledButton.icon(
|
||||||
);
|
onPressed: () => _showShippingModal(context),
|
||||||
|
|
||||||
// 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),
|
icon: const Icon(Icons.local_shipping),
|
||||||
label: const Text('Spedisci'),
|
label: const Text('Spedisci'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: flux
|
name: flux
|
||||||
description: "Gestione attività negozio di telefonia"
|
description: "Gestione attività negozio di telefonia"
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.0.12+12
|
version: 1.0.13+13
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.11.3
|
sdk: ^3.11.3
|
||||||
|
|||||||
Reference in New Issue
Block a user