From 3ecf617998ace18421df16a23f2c81314650e80f Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Tue, 19 May 2026 12:46:13 +0200 Subject: [PATCH] j --- .../bloc/latest_store_operations_bloc.dart | 4 +- .../bloc/latest_store_operations_events.dart | 4 +- .../ui/latest_store_operations_card.dart | 4 +- .../blocs/latest_store_tickets_bloc.dart | 58 ++++++ .../blocs/latest_store_tickets_events.dart | 17 ++ .../blocs/latest_store_tickets_state.dart | 29 +++ .../ui/latest_store_tickets_card.dart | 184 ++++++++++++++++++ lib/features/home/ui/home_screen.dart | 11 +- .../data/operations_repository.dart | 2 +- .../tickets/data/ticket_repository.dart | 16 ++ 10 files changed, 313 insertions(+), 16 deletions(-) create mode 100644 lib/features/home/latest_store_tickets/blocs/latest_store_tickets_bloc.dart create mode 100644 lib/features/home/latest_store_tickets/blocs/latest_store_tickets_events.dart create mode 100644 lib/features/home/latest_store_tickets/blocs/latest_store_tickets_state.dart create mode 100644 lib/features/home/latest_store_tickets/ui/latest_store_tickets_card.dart diff --git a/lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart b/lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart index 5d52d3d..55f088a 100644 --- a/lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart +++ b/lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart @@ -17,12 +17,12 @@ class LatestStoreOperationsBloc status: LatestStoreOperationsStatus.initial, ), ) { - on((event, emit) async { + on((event, emit) async { emit(state.copyWith(status: LatestStoreOperationsStatus.loading)); try { // 1. Creiamo uno stream "intermedio" che idrata i dati final hydratedStream = _repository - .getLastStoreOperationsStream(storeId: event.storeId, limit: 5) + .getLatestStoreOperationsStream(storeId: event.storeId, limit: 10) .asyncMap((List rawOperations) async { // Questo gira ad ogni "scatto" dello stream di Supabase List fullyHydratedOperations = []; diff --git a/lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart b/lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart index c15c0f8..e7b4061 100644 --- a/lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart +++ b/lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart @@ -7,10 +7,10 @@ sealed class LatestStoreOperationsEvent extends Equatable { List get props => []; } -class InitLastStoreOperationsEvent extends LatestStoreOperationsEvent { +class InitLatestStoreOperationsEvent extends LatestStoreOperationsEvent { final String storeId; - const InitLastStoreOperationsEvent(this.storeId); + const InitLatestStoreOperationsEvent(this.storeId); @override List get props => [storeId]; diff --git a/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart b/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart index 6513b65..d0976ab 100644 --- a/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart +++ b/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart @@ -18,7 +18,7 @@ class LatestStoreOperationsCard extends StatelessWidget { // 1. Creiamo il Bloc e facciamo partire subito la query create: (context) => LatestStoreOperationsBloc() - ..add(InitLastStoreOperationsEvent(currentStoreId ?? '')), + ..add(InitLatestStoreOperationsEvent(currentStoreId ?? '')), child: BlocListener( // 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream! listenWhen: (previous, current) => @@ -26,7 +26,7 @@ class LatestStoreOperationsCard extends StatelessWidget { listener: (context, state) { if (state.currentStore?.id != null) { context.read().add( - InitLastStoreOperationsEvent(state.currentStore!.id!), + InitLatestStoreOperationsEvent(state.currentStore!.id!), ); } }, diff --git a/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_bloc.dart b/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_bloc.dart new file mode 100644 index 0000000..87ee969 --- /dev/null +++ b/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_bloc.dart @@ -0,0 +1,58 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flux/features/tickets/data/ticket_repository.dart'; +import 'package:flux/features/tickets/models/ticket_model.dart'; +import 'package:get_it/get_it.dart'; + +part 'latest_store_tickets_events.dart'; +part 'latest_store_tickets_state.dart'; + +class LatestStoreTicketsBloc + extends Bloc { + final _repository = GetIt.I.get(); + LatestStoreTicketsBloc() + : super( + const LatestStoreTicketsState(status: LatestStoreTicketsStatus.initial), + ) { + on((event, emit) async { + emit(state.copyWith(status: LatestStoreTicketsStatus.loading)); + try { + final hydratedStream = _repository + .getLatestStoreTicketsStream(storeId: event.storeId, limit: 10) + .asyncMap((List rawTickets) async { + List fullyHydratedTickets = []; + + for (TicketModel ticket in rawTickets) { + TicketModel fullTicket = await _repository.getTicketById( + ticket.id!, + ); + fullyHydratedTickets.add(fullTicket); + } + + return fullyHydratedTickets; + }); + await emit.forEach( + hydratedStream, + onData: (List fullyHydratedTickets) { + return state.copyWith( + tickets: fullyHydratedTickets, + status: LatestStoreTicketsStatus.success, + ); + }, + onError: (error, stackTrace) => state.copyWith( + status: LatestStoreTicketsStatus.failure, + error: error.toString(), + ), + ); + } catch (e) { + emit( + state.copyWith( + status: LatestStoreTicketsStatus.failure, + error: e.toString(), + ), + ); + } + }); + // TODO: implement event handlers + } +} diff --git a/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_events.dart b/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_events.dart new file mode 100644 index 0000000..48499ef --- /dev/null +++ b/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_events.dart @@ -0,0 +1,17 @@ +part of 'latest_store_tickets_bloc.dart'; + +abstract class LatestStoreTicketsEvent extends Equatable { + const LatestStoreTicketsEvent(); + + @override + List get props => []; +} + +class InitLatestStoreTicketsEvent extends LatestStoreTicketsEvent { + final String storeId; + + const InitLatestStoreTicketsEvent(this.storeId); + + @override + List get props => [storeId]; +} diff --git a/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_state.dart b/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_state.dart new file mode 100644 index 0000000..c3b67a2 --- /dev/null +++ b/lib/features/home/latest_store_tickets/blocs/latest_store_tickets_state.dart @@ -0,0 +1,29 @@ +part of 'latest_store_tickets_bloc.dart'; + +enum LatestStoreTicketsStatus { initial, loading, success, failure } + +class LatestStoreTicketsState extends Equatable { + final LatestStoreTicketsStatus status; + final String? error; + final List tickets; + const LatestStoreTicketsState({ + required this.status, + this.error, + this.tickets = const [], + }); + + @override + List get props => [status, error, tickets]; + + LatestStoreTicketsState copyWith({ + LatestStoreTicketsStatus? status, + String? error, + List? tickets, + }) { + return LatestStoreTicketsState( + status: status ?? this.status, + error: error, + tickets: tickets ?? this.tickets, + ); + } +} diff --git a/lib/features/home/latest_store_tickets/ui/latest_store_tickets_card.dart b/lib/features/home/latest_store_tickets/ui/latest_store_tickets_card.dart new file mode 100644 index 0000000..8861c42 --- /dev/null +++ b/lib/features/home/latest_store_tickets/ui/latest_store_tickets_card.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/routes/routes.dart'; +import 'package:flux/core/theme/theme.dart'; +import 'package:flux/features/home/latest_store_tickets/blocs/latest_store_tickets_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class LatestStoreTicketsCard extends StatelessWidget { + const LatestStoreTicketsCard({super.key}); + + @override + Widget build(BuildContext context) { + final currentStoreId = context.read().state.currentStore?.id; + + return BlocProvider( + // 1. Creiamo il Bloc e facciamo partire subito la query + create: (context) => + LatestStoreTicketsBloc() + ..add(InitLatestStoreTicketsEvent(currentStoreId ?? '')), + child: BlocListener( + // 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream! + listenWhen: (previous, current) => + previous.currentStore?.id != current.currentStore?.id, + listener: (context, state) { + if (state.currentStore?.id != null) { + context.read().add( + InitLatestStoreTicketsEvent(state.currentStore!.id!), + ); + } + }, + child: _LatestStoreTicketsCardContent(), + ), + ); + } +} + +class _LatestStoreTicketsCardContent extends StatelessWidget { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + const color = Colors.blue; + + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)), + ), + child: InkWell( + onTap: () => context.pushNamed(Routes.tickets), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // --- HEADER DELLA CARD --- + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.design_services_outlined, + color: color, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + "Ticket recenti", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: context.primaryText, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 12), + + // --- CORPO DELLA CARD (LA LISTA REAL-TIME) --- + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state.status == LatestStoreTicketsStatus.loading || + state.status == LatestStoreTicketsStatus.initial) { + return const Center(child: CircularProgressIndicator()); + } + + if (state.status == LatestStoreTicketsStatus.failure) { + return Center( + child: Text( + "Errore di caricamento", + style: TextStyle(color: theme.colorScheme.error), + ), + ); + } + + if (state.tickets.isEmpty) { + return Center( + child: Text( + "Nessun ticket recente.", + style: TextStyle( + color: context.secondaryText, + fontStyle: FontStyle.italic, + ), + ), + ); + } + + return ListView.separated( + itemCount: state.tickets.length, + separatorBuilder: (context, index) => Divider( + height: 1, + color: theme.dividerColor.withValues(alpha: 0.3), + ), + itemBuilder: (context, index) { + final ticket = state.tickets[index]; + return InkWell( + onTap: () => context.pushNamed( + Routes.ticketForm, + extra: (createdBy: null, ticket: ticket), + pathParameters: {'id': ticket.id!}, + ), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 5, + child: Text( + ticket.customer?.name ?? + 'Cliente sconosciuto', + style: TextStyle( + fontWeight: FontWeight.w700, + color: context.primaryText, + ), + ), + ), + Expanded( + flex: 5, + child: Text( + ticket.targetModelName ?? + 'Modello sconosciuto', + style: TextStyle( + fontWeight: FontWeight.w600, + color: context.primaryText, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + "${ticket.createdAt?.day}/${ticket.createdAt?.month}", + style: TextStyle( + color: context.secondaryText, + fontSize: 12, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 887fea4..1c3e277 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -6,6 +6,7 @@ import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/widgets/staff_selector_modal.dart'; import 'package:flux/features/home/latest_store_operations/ui/latest_store_operations_card.dart'; +import 'package:flux/features/home/latest_store_tickets/ui/latest_store_tickets_card.dart'; import 'package:flux/features/home/ui/quick_actions_widget.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; @@ -80,15 +81,7 @@ class HomeScreen extends StatelessWidget { context: context, ), LatestStoreOperationsCard(), - _buildDashboardWidget( - title: context.l10n.homeLatestOperationTickets, - icon: Icons.support_agent_outlined, - color: Colors.purple, - context: context, - onTap: () => context.pushNamed( - Routes.tickets, - ), // <-- Aggiunto! - ), + LatestStoreTicketsCard(), ]), ), ), diff --git a/lib/features/operations/data/operations_repository.dart b/lib/features/operations/data/operations_repository.dart index 9e380a7..1abc4f5 100644 --- a/lib/features/operations/data/operations_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -82,7 +82,7 @@ class OperationsRepository { } } - Stream> getLastStoreOperationsStream({ + Stream> getLatestStoreOperationsStream({ required String storeId, required int limit, }) { diff --git a/lib/features/tickets/data/ticket_repository.dart b/lib/features/tickets/data/ticket_repository.dart index 3c87622..21e98f3 100644 --- a/lib/features/tickets/data/ticket_repository.dart +++ b/lib/features/tickets/data/ticket_repository.dart @@ -174,6 +174,22 @@ class TicketRepository { }); } + Stream> getLatestStoreTicketsStream({ + required String storeId, + required int limit, + }) { + return _supabase + .from(_tableName) + .stream(primaryKey: ['id']) + .eq('store_id', storeId) + .order('created_at', ascending: false) + .limit(limit) + .map( + (listOfMaps) => + listOfMaps.map((map) => TicketModel.fromMap(map)).toList(), + ); + } + /// Recupera un ticket specifico CON TUTTE LE RELAZIONI espanse (Cliente e Modelli) /// Questa รจ la vera magia di Supabase! Future getTicketById(String ticketId) async {