ticket refinements

This commit is contained in:
2026-06-02 11:52:31 +02:00
parent 7fad6ee02b
commit a51ac8fe7f
10 changed files with 155 additions and 66 deletions

View File

@@ -47,7 +47,7 @@ class DashboardNoteListCubit extends Cubit<DashboardNoteListState> {
Future<void> _loadNotesSilently() async { Future<void> _loadNotesSilently() async {
try { try {
final notes = await _repository.getNotes(); final notes = await _repository.getNotes();
if (isClosed) return;
emit( emit(
state.copyWith( state.copyWith(
status: DashboardNoteListStatus.success, status: DashboardNoteListStatus.success,
@@ -56,6 +56,7 @@ class DashboardNoteListCubit extends Cubit<DashboardNoteListState> {
), ),
); );
} catch (e) { } catch (e) {
if (isClosed) return;
emit( emit(
state.copyWith( state.copyWith(
status: DashboardNoteListStatus.failure, status: DashboardNoteListStatus.failure,

View File

@@ -57,6 +57,7 @@ class DashboardStoreOperationListCubit
limit: 10, limit: 10,
offset: 0, offset: 0,
); );
if (isClosed) return;
emit( emit(
state.copyWith( state.copyWith(
status: DashboardStoreOperationListStatus.success, status: DashboardStoreOperationListStatus.success,
@@ -65,6 +66,7 @@ class DashboardStoreOperationListCubit
), ),
); );
} catch (e) { } catch (e) {
if (isClosed) return;
emit( emit(
state.copyWith( state.copyWith(
status: DashboardStoreOperationListStatus.failure, status: DashboardStoreOperationListStatus.failure,

View File

@@ -59,7 +59,7 @@ class DashboardStoreTicketListCubit
limit: 10, limit: 10,
offset: 0, offset: 0,
); );
if (isClosed) return;
emit( emit(
state.copyWith( state.copyWith(
status: DashboardStoreTicketListStatus.success, status: DashboardStoreTicketListStatus.success,
@@ -68,6 +68,7 @@ class DashboardStoreTicketListCubit
), ),
); );
} catch (e) { } catch (e) {
if (isClosed) return;
emit( emit(
state.copyWith( state.copyWith(
status: DashboardStoreTicketListStatus.failure, status: DashboardStoreTicketListStatus.failure,
@@ -76,4 +77,10 @@ class DashboardStoreTicketListCubit
); );
} }
} }
@override
Future<void> close() {
stopListening();
return super.close();
}
} }

View File

@@ -53,7 +53,7 @@ class DashboardTaskListCubit extends Cubit<DashboardTaskListState> {
statuses: [TaskStatus.open, TaskStatus.inProgress], statuses: [TaskStatus.open, TaskStatus.inProgress],
limit: 10, limit: 10,
); );
if (isClosed) return;
emit( emit(
state.copyWith( state.copyWith(
status: DashboardTaskListStatus.success, status: DashboardTaskListStatus.success,
@@ -62,6 +62,7 @@ class DashboardTaskListCubit extends Cubit<DashboardTaskListState> {
), ),
); );
} catch (e) { } catch (e) {
if (isClosed) return;
emit( emit(
state.copyWith( state.copyWith(
status: DashboardTaskListStatus.failure, status: DashboardTaskListStatus.failure,

View File

@@ -153,10 +153,10 @@ class StaffRepository {
// Assegna un membro a un negozio // Assegna un membro a un negozio
Future<void> assignStaffToStore(String staffId, String storeId) async { Future<void> assignStaffToStore(String staffId, String storeId) async {
await _supabase.from(Tables.staffInStores).insert({ await _supabase.from(Tables.staffInStores).upsert({
'staff_member_id': staffId, 'staff_member_id': staffId,
'store_id': storeId, 'store_id': storeId,
}); }, onConflict: 'staff_member_id,store_id'); // Evita duplicati
} }
// Rimuove l'assegnazione // Rimuove l'assegnazione

View File

@@ -367,4 +367,22 @@ class TicketFormCubit extends Cubit<TicketFormState> {
); );
} }
} }
Future<void> deleteTicket() async {
final currentTicket = state.ticket;
if (currentTicket.id == null || currentTicket.id!.isEmpty) return;
try {
await _repository.deleteTicket(currentTicket.id!);
emit(state.copyWith(status: TicketFormStatus.deleted));
} catch (e) {
emit(
state.copyWith(
status: TicketFormStatus.failure,
errorMessage: 'Errore durante l\'eliminazione: $e',
),
);
}
}
} }

View File

@@ -2,7 +2,16 @@ import 'package:equatable/equatable.dart';
import 'package:flux/features/tickets/models/ticket_model.dart'; import 'package:flux/features/tickets/models/ticket_model.dart';
// Adatta gli import al tuo progetto! // Adatta gli import al tuo progetto!
enum TicketFormStatus { initial, ready, loading, saving, success, pop, failure } enum TicketFormStatus {
initial,
ready,
loading,
saving,
success,
pop,
failure,
deleted,
}
class TicketFormState extends Equatable { class TicketFormState extends Equatable {
final TicketModel ticket; final TicketModel ticket;

View File

@@ -158,4 +158,19 @@ class TicketListCubit extends Cubit<TicketListState> {
// Opzionale: Se vuoi comunque riallinearti al server in modo silenzioso dopo l'animazione // Opzionale: Se vuoi comunque riallinearti al server in modo silenzioso dopo l'animazione
// loadTickets(refresh: true); // loadTickets(refresh: true);
} }
Future<void> deleteTickets(List<TicketModel> tickets) async {
try {
for (final ticket in tickets) {
await _repository.deleteTicket(ticket.id!);
}
// Rimuoviamo i ticket localmente senza ricaricare tutto
final remainingTickets = state.tickets
.where((t) => !tickets.any((toDelete) => toDelete.id == t.id))
.toList();
emit(state.copyWith(tickets: remainingTickets, selectedTickets: {}));
} catch (e) {
emit(state.copyWith(errorMessage: e.toString()));
}
}
} }

View File

@@ -348,6 +348,32 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
trackingCubit.loadTrackings(ticketId, TrackingParentType.ticket); trackingCubit.loadTrackings(ticketId, TrackingParentType.ticket);
} }
void _deleteTicket(TicketModel ticket, {Color color = Colors.red}) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Conferma Cancellazione'),
content: Text(
'Sei sicuro di voler cancellare il ticket "${ticket.referenceId}"? Questa azione è irreversibile.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Annulla'),
),
ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: color),
onPressed: () {
context.read<TicketFormCubit>().deleteTicket();
Navigator.of(context).pop(); // Chiude il dialog
},
child: const Text('Cancella Ticket'),
),
],
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -359,6 +385,10 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
_syncTextControllers(state.ticket); _syncTextControllers(state.ticket);
} }
if (state.status == TicketFormStatus.deleted) {
Navigator.of(context).pop();
}
if (state.status == TicketFormStatus.success) { if (state.status == TicketFormStatus.success) {
context.read<TicketListCubit>().loadTickets(refresh: true); context.read<TicketListCubit>().loadTickets(refresh: true);
_showSuccessActions( _showSuccessActions(
@@ -388,66 +418,61 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
: 'Modifica Ticket - Operatore: ${state.ticket.createdByName}', : 'Modifica Ticket - Operatore: ${state.ticket.createdByName}',
), ),
actions: [ actions: [
BlocBuilder<TicketFormCubit, TicketFormState>( if (ticket.id != null) ...[
builder: (context, state) { Padding(
final ticket = state.ticket; padding: const EdgeInsets.symmetric(
horizontal: 16.0,
// Se il ticket non è ancora salvato, niente azioni rapide vertical: 8.0,
if (ticket.id == null || ticket.id!.isEmpty) { ),
return const SizedBox.shrink(); child: FilledButton.icon(
} onPressed: () => _deleteTicket(ticket, color: Colors.red),
icon: const Icon(Icons.delete),
// CONDIZIONE A: Da iniziare label: const Text('Cancella Ticket'),
if (ticket.ticketStatus == TicketStatus.open || ),
ticket.ticketStatus == TicketStatus.waitingForParts) { ),
return Padding( ],
padding: const EdgeInsets.symmetric( if (ticket.ticketStatus == TicketStatus.open ||
horizontal: 16.0, ticket.ticketStatus == TicketStatus.waitingForParts) ...[
vertical: 8.0, // CONDIZIONE A: Da iniziare
), Padding(
child: FilledButton.icon( padding: const EdgeInsets.symmetric(
style: FilledButton.styleFrom( horizontal: 16.0,
backgroundColor: vertical: 8.0,
Colors.amber.shade700, // Colore Action ),
), child: FilledButton.icon(
onPressed: () async { style: FilledButton.styleFrom(
StaffMemberModel? takenBy = await getStaffMember( backgroundColor: Colors.amber.shade700, // Colore Action
context, ),
); onPressed: () async {
if (takenBy == null || !context.mounted) return; StaffMemberModel? takenBy = await getStaffMember(context);
context.read<TicketFormCubit>().takeInCharge( if (takenBy == null || !context.mounted) return;
staffId: takenBy.id!, context.read<TicketFormCubit>().takeInCharge(
staffName: takenBy.name, staffId: takenBy.id!,
); staffName: takenBy.name,
_navigateToWorkspace(ticket.id!); );
}, _navigateToWorkspace(ticket.id!);
icon: const Icon(Icons.play_arrow, color: Colors.white), },
label: const Text( icon: const Icon(Icons.play_arrow, color: Colors.white),
'Prendi in Carico', label: const Text(
style: TextStyle(color: Colors.white), 'Prendi in Carico',
), style: TextStyle(color: Colors.white),
), ),
); ),
} ),
// CONDIZIONE B: Già in lavorazione ],
else if (ticket.ticketStatus == TicketStatus.inProgress) { if (ticket.ticketStatus == TicketStatus.inProgress) ...[
return Padding( Padding(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16.0,
vertical: 8.0, vertical: 8.0,
), ),
child: FilledButton.icon( child: FilledButton.icon(
onPressed: () => _navigateToWorkspace(ticket.id!), onPressed: () => _navigateToWorkspace(ticket.id!),
icon: const Icon(Icons.handyman), icon: const Icon(Icons.handyman),
label: const Text('Vai a Lavorazione'), label: const Text('Vai a Lavorazione'),
), ),
); ),
} ],
// Se è chiuso o in altri stati strani, nascondiamo il bottone
return const SizedBox.shrink();
},
),
Padding( Padding(
padding: const EdgeInsets.only(right: 16.0), padding: const EdgeInsets.only(right: 16.0),
child: Chip( child: Chip(

View File

@@ -78,6 +78,12 @@ class TicketList extends StatelessWidget {
} }
} }
void _deleteTickets(BuildContext context) {
context.read<TicketListCubit>().deleteTickets(
state.selectedTickets.toList(),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
@@ -178,6 +184,11 @@ class TicketList extends StatelessWidget {
runSpacing: 8.0, runSpacing: 8.0,
alignment: WrapAlignment.end, alignment: WrapAlignment.end,
children: [ children: [
IconButton.filled(
tooltip: 'Elimina',
onPressed: () => _deleteTickets(context),
icon: const Icon(Icons.delete),
),
IconButton.filled( IconButton.filled(
tooltip: 'Riconsegna', tooltip: 'Riconsegna',
onPressed: () => _setStatusClosed(context), onPressed: () => _setStatusClosed(context),