From 0c8b9ae3ec1ffebe4b83d6ca8cee82e6d96d3d5a Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Tue, 5 May 2026 19:29:20 +0200 Subject: [PATCH] 2 --- .../tickets/blocs/ticket_list_cubit.dart | 6 + .../tickets/blocs/ticket_list_state.dart | 10 + .../tickets/ui/ticket_list_screen.dart | 291 ++++++++++++++++++ lib/main.dart | 2 + 4 files changed, 309 insertions(+) create mode 100644 lib/features/tickets/ui/ticket_list_screen.dart diff --git a/lib/features/tickets/blocs/ticket_list_cubit.dart b/lib/features/tickets/blocs/ticket_list_cubit.dart index 5ad1616..20aa312 100644 --- a/lib/features/tickets/blocs/ticket_list_cubit.dart +++ b/lib/features/tickets/blocs/ticket_list_cubit.dart @@ -35,6 +35,8 @@ class TicketListCubit extends Cubit { searchTerm: state.searchTerm, dateRange: state.dateRange, statusFilter: state.statusFilter, + ticketTypeFilter: state.ticketTypeFilter, + staffIdFilter: state.staffIdFilter, ); emit( @@ -54,6 +56,8 @@ class TicketListCubit extends Cubit { String? searchTerm, DateTimeRange? dateRange, TicketStatus? statusFilter, + TicketType? ticketTypeFilter, + String? staffIdFilter, bool clearSearch = false, bool clearDate = false, bool clearStatus = false, @@ -63,6 +67,8 @@ class TicketListCubit extends Cubit { searchTerm: searchTerm, dateRange: dateRange, statusFilter: statusFilter, + ticketTypeFilter: ticketTypeFilter, + staffIdFilter: staffIdFilter, clearSearch: clearSearch, clearDate: clearDate, clearStatus: clearStatus, diff --git a/lib/features/tickets/blocs/ticket_list_state.dart b/lib/features/tickets/blocs/ticket_list_state.dart index d36caa5..a2b9636 100644 --- a/lib/features/tickets/blocs/ticket_list_state.dart +++ b/lib/features/tickets/blocs/ticket_list_state.dart @@ -12,6 +12,8 @@ class TicketListState extends Equatable { final String? searchTerm; final DateTimeRange? dateRange; final TicketStatus? statusFilter; + final TicketType? ticketTypeFilter; + final String? staffIdFilter; const TicketListState({ this.tickets = const [], @@ -21,6 +23,8 @@ class TicketListState extends Equatable { this.searchTerm, this.dateRange, this.statusFilter, + this.ticketTypeFilter, + this.staffIdFilter, }); TicketListState copyWith({ @@ -31,6 +35,8 @@ class TicketListState extends Equatable { String? searchTerm, DateTimeRange? dateRange, TicketStatus? statusFilter, + TicketType? ticketTypeFilter, + String? staffIdFilter, bool clearSearch = false, bool clearDate = false, bool clearStatus = false, @@ -43,6 +49,8 @@ class TicketListState extends Equatable { searchTerm: clearSearch ? null : (searchTerm ?? this.searchTerm), dateRange: clearDate ? null : (dateRange ?? this.dateRange), statusFilter: clearStatus ? null : (statusFilter ?? this.statusFilter), + ticketTypeFilter: ticketTypeFilter ?? this.ticketTypeFilter, + staffIdFilter: staffIdFilter ?? this.staffIdFilter, ); } @@ -55,5 +63,7 @@ class TicketListState extends Equatable { searchTerm, dateRange, statusFilter, + ticketTypeFilter, + staffIdFilter, ]; } diff --git a/lib/features/tickets/ui/ticket_list_screen.dart b/lib/features/tickets/ui/ticket_list_screen.dart new file mode 100644 index 0000000..43a0503 --- /dev/null +++ b/lib/features/tickets/ui/ticket_list_screen.dart @@ -0,0 +1,291 @@ +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/models/ticket_model.dart'; +import 'package:flux/features/tickets/models/ticket_status_extension.dart'; + +class TicketListScreen extends StatefulWidget { + const TicketListScreen({super.key}); + + @override + State createState() => _TicketListScreenState(); +} + +class _TicketListScreenState extends State { + final ScrollController _scrollController = ScrollController(); + final TextEditingController _searchController = TextEditingController(); + + @override + void initState() { + super.initState(); + // INFINITY SCROLL: Quando arriviamo quasi in fondo, chiediamo altri ticket + _scrollController.addListener(() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 200) { + context.read().fetchTickets(); + } + }); + } + + @override + void dispose() { + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Assistenza & Riparazioni'), + actions: [ + // Tasto per filtri avanzati (Data, Staff, Tipo) -> Da fare in un BottomSheet! + IconButton( + icon: const Icon(Icons.filter_list), + onPressed: () { + // TODO: Aprire BottomSheet filtri avanzati + }, + ), + ], + ), + body: Column( + children: [ + // 1. BARRA DI RICERCA + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: 'Cerca per nome cliente...', + prefixIcon: const Icon(Icons.search), + suffixIcon: IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + _searchController.clear(); + context.read().updateFilters( + clearSearch: true, + ); + }, + ), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + ), + ), + onSubmitted: (value) { + context.read().updateFilters( + searchTerm: value, + ); + }, + ), + ), + + // 2. FILTRI RAPIDI PER STATO (CHIPS) + BlocBuilder( + buildWhen: (previous, current) => + previous.statusFilter != current.statusFilter, + builder: (context, state) { + return SizedBox( + height: 50, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: 8.0), + children: [ + _buildStatusChip(context, state, null, 'Tutti'), + ...TicketStatus.values.map( + (status) => _buildStatusChip( + context, + state, + status, + status.displayValue, + ), + ), + ], + ), + ); + }, + ), + const Divider(), + + // 3. LA LISTA DEI TICKET + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading && state.tickets.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + if (state.tickets.isEmpty) { + return const Center(child: Text('Nessun ticket trovato.')); + } + + return ListView.builder( + controller: _scrollController, + itemCount: state.hasReachedMax + ? state.tickets.length + : state.tickets.length + 1, + itemBuilder: (context, index) { + // Se siamo all'ultimo elemento e non abbiamo raggiunto il max, mostriamo il loader + if (index >= state.tickets.length) { + return const Center( + child: Padding( + padding: EdgeInsets.all(16.0), + child: CircularProgressIndicator(), + ), + ); + } + + final ticket = state.tickets[index]; + return _TicketCard(ticket: ticket); + }, + ); + }, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () { + // TODO: Navigare alla creazione di un nuovo ticket + }, + icon: const Icon(Icons.add), + label: const Text('Nuovo Ticket'), + ), + ); + } + + // Widget di supporto per creare le Chip di filtro + Widget _buildStatusChip( + BuildContext context, + TicketListState state, + TicketStatus? status, + String label, + ) { + final isSelected = state.statusFilter == status; + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: ChoiceChip( + label: Text(label), + selected: isSelected, + selectedColor: + status?.color.withValues(alpha: 0.2) ?? + Colors.blue.withValues(alpha: 0.2), + onSelected: (selected) { + context.read().updateFilters( + statusFilter: selected ? status : null, + clearStatus: !selected && status != null, + ); + }, + ), + ); + } +} + +// --------------------------------------------------------- +// LA CARD DEL TICKET (Il "Colpo d'Occhio") +// --------------------------------------------------------- +class _TicketCard extends StatelessWidget { + final TicketModel ticket; + + const _TicketCard({required this.ticket}); + + @override + Widget build(BuildContext context) { + final statusColor = ticket.status?.color ?? Colors.grey; + final statusIcon = ticket.status?.icon ?? Icons.help_outline; + + return Card( + margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), + clipBehavior: Clip + .antiAlias, // Serve per tagliare il container laterale con gli angoli della card + child: IntrinsicHeight( + // Serve per far sì che il container laterale prenda tutta l'altezza + child: Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // LA STRISCIA COLORATA LATERALE + Container(width: 6, color: statusColor), + Expanded( + child: ListTile( + contentPadding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 8.0, + ), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + ticket.customerName ?? 'Cliente Sconosciuto', + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + overflow: TextOverflow.ellipsis, + ), + ), + // IL BADGE DELLO STATO + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: statusColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: statusColor.withValues(alpha: 0.5), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(statusIcon, size: 14, color: statusColor), + const SizedBox(width: 4), + Text( + ticket.status?.displayValue ?? 'N/D', + style: TextStyle( + fontSize: 12, + color: statusColor, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + // MODELLO O TIPO DI INTERVENTO + Text( + ticket.targetModelName ?? ticket.ticketType.displayValue, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + const SizedBox(height: 4), + // DATA CREAZIONE (Es: 04/05/2026) + Text( + ticket.createdAt != null + ? 'Creato il: ${ticket.createdAt!.day}/${ticket.createdAt!.month}/${ticket.createdAt!.year}' + : '', + style: TextStyle( + color: Colors.grey.shade600, + fontSize: 12, + ), + ), + ], + ), + onTap: () { + // TODO: Aprire il dettaglio del ticket! + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 8f62822..4d3873b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,7 @@ import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flux/features/attachments/data/attachments_repository.dart'; import 'package:flux/features/auth/bloc/auth_cubit.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; +import 'package:flux/features/tickets/data/ticket_repository.dart'; import 'package:flux/l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; @@ -92,6 +93,7 @@ Future setupLocator() async { getIt.registerLazySingleton( () => AttachmentsRepository(), ); + getIt.registerLazySingleton(() => TicketRepository()); // NOTA: CompanyRepository l'ho tolto perché la logica della Company // ora è gestita dal CoreRepository durante l'Onboarding.