Files
flux/lib/features/tickets/ui/ticket_list_screen.dart
2026-05-09 19:32:40 +02:00

301 lines
10 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/routes/app_router.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';
import 'package:go_router/go_router.dart';
class TicketListScreen extends StatefulWidget {
const TicketListScreen({super.key});
@override
State<TicketListScreen> createState() => _TicketListScreenState();
}
class _TicketListScreenState extends State<TicketListScreen> {
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<TicketListCubit>().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<TicketListCubit>().updateFilters(
clearSearch: true,
);
},
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
),
onSubmitted: (value) {
context.read<TicketListCubit>().updateFilters(
searchTerm: value,
);
},
),
),
// 2. FILTRI RAPIDI PER STATO (CHIPS)
BlocBuilder<TicketListCubit, TicketListState>(
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<TicketListCubit, TicketListState>(
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: () {
context.pushNamed(ticketFormRoute, pathParameters: {'id': 'New'});
},
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<TicketListCubit>().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.ticketStatus.color;
final statusIcon = ticket.ticketStatus.icon;
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.ticketStatus.displayValue,
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: () {
context.pushNamed(
'ticket-form',
pathParameters: {'id': ticket.id!},
extra:
ticket, // <-- LA MAGIA È QUI: Passa l'oggetto intero!
// Teniamo anche il parametro URL per coerenza di routing
);
},
),
),
],
),
),
);
}
}