import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/routes/routes.dart'; import 'package:flux/core/widgets/staff_selector_modal.dart'; import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; import 'package:flux/features/operations/blocs/operation_list_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:go_router/go_router.dart'; class OperationListScreen extends StatefulWidget { const OperationListScreen({super.key}); @override State createState() => _OperationListScreenState(); } class _OperationListScreenState extends State { final ScrollController _scrollController = ScrollController(); final TextEditingController _searchController = TextEditingController(); // Set per gestire le Bulk Actions (Selezione multipla) final Set _selectedOperationIds = {}; bool get _isSelectionMode => _selectedOperationIds.isNotEmpty; // Flag per mostrare/nascondere la barra di ricerca integrata nell'AppBar bool _showSearchBar = false; @override void initState() { super.initState(); _scrollController.addListener(_onScroll); // Primo caricamento: partiamo da pagina 1 // (Il Cubit deciderà se fare il boot iniziale o se c'era già roba in cache) WidgetsBinding.instance.addPostFrameCallback((_) { final isDesktop = MediaQuery.sizeOf(context).width >= 900; if (isDesktop) { context.read().loadSpecificPageDesktop(1); } else { context.read().loadNextPageMobile(refresh: true); } }); } void _onScroll() { final isDesktop = MediaQuery.sizeOf(context).width >= 900; // 🥷 COMPORTAMENTO IBRIDO: Lo scroll infinito si attiva SOLO su mobile if (isDesktop) return; if (_isBottom) { context.read().loadNextPageMobile(); } } bool get _isBottom { if (!_scrollController.hasClients) return false; final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.offset; return currentScroll >= (maxScroll * 0.9); } @override void dispose() { _scrollController.dispose(); _searchController.dispose(); super.dispose(); } void _toggleSelection(String id) { setState(() { if (_selectedOperationIds.contains(id)) { _selectedOperationIds.remove(id); } else { _selectedOperationIds.add(id); } }); } void _clearSelection() { setState(() { _selectedOperationIds.clear(); }); } @override Widget build(BuildContext context) { final isDesktop = MediaQuery.sizeOf(context).width >= 900; return Scaffold( // --- APP BAR DINAMICA E INTEGRATA --- appBar: _isSelectionMode ? AppBar( backgroundColor: Theme.of(context).colorScheme.primaryContainer, leading: IconButton( icon: const Icon(Icons.close), onPressed: _clearSelection, ), title: Text("${_selectedOperationIds.length} selezionate"), actions: [ IconButton( icon: const Icon(Icons.edit_note), tooltip: 'Cambia Stato Massivo', onPressed: () { // TODO: Integrare bottom sheet per azioni massive }, ), ], ) : AppBar( title: _showSearchBar ? TextField( controller: _searchController, autofocus: true, decoration: const InputDecoration( hintText: 'Cerca per cliente, nota o riferimento...', border: InputBorder.none, ), style: const TextStyle(fontSize: 16), onChanged: (text) { context.read().updateFilters( text: text, ); }, ) : const Text("Gestione Servizi"), elevation: 0, actions: [ IconButton( icon: Icon(_showSearchBar ? Icons.close : Icons.search), onPressed: () { setState(() { _showSearchBar = !_showSearchBar; if (!_showSearchBar) { _searchController.clear(); context.read().clearFilters(); } }); }, ), if (!isDesktop) // Il pull-to-refresh c'è già su mobile, su desktop mettiamo un tasto manuale IconButton( icon: const Icon(Icons.filter_list), onPressed: () { // TODO: Bottone Filtri Avanzati (es. DateRange Picker) }, ), ], ), // --- CORPO RESPONSIVO --- body: BlocBuilder( builder: (context, state) { if (state.status == OperationListStatus.loading && state.operations.isEmpty) { return const Center(child: CircularProgressIndicator()); } if (state.operations.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text("Nessuna pratica trovata."), const SizedBox(height: 12), ElevatedButton( onPressed: () => isDesktop ? context .read() .loadSpecificPageDesktop(1) : context.read().loadNextPageMobile( refresh: true, ), child: const Text("Ricarica"), ), ], ), ); } // 🥷 SCENARIO DESKTOP: Griglia + Barra di Paginazione Gmail-Style if (isDesktop) { return Column( children: [ Expanded( child: GridView.builder( controller: _scrollController, padding: const EdgeInsets.all(16), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 420, // Larghezza bilanciata per le card su desktop mainAxisExtent: 175, // Altezza controllata per evitare buchi bianchi crossAxisSpacing: 16, mainAxisSpacing: 16, ), itemCount: state.operations.length, itemBuilder: (context, index) { final operation = state.operations[index]; return _buildResponsiveCard(operation); }, ), ), _buildDesktopPaginationFooter( state, ), // La barra in fondo stile Gmail ], ); } // 🥷 SCENARIO MOBILE: ListView con Infinite Scroll e Pull-to-Refresh return RefreshIndicator( onRefresh: () => context .read() .loadNextPageMobile(refresh: true), child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.only( bottom: 80, left: 8, right: 8, top: 8, ), itemCount: state.hasReachedMax ? state.operations.length : state.operations.length + 1, itemBuilder: (context, index) { if (index >= state.operations.length) { return const Center( child: Padding( padding: EdgeInsets.all(16.0), child: CircularProgressIndicator(strokeWidth: 2), ), ); } final operation = state.operations[index]; return Padding( padding: const EdgeInsets.symmetric(vertical: 4.0), child: _buildResponsiveCard(operation), ); }, ), ); }, ), floatingActionButton: _isSelectionMode ? null : FloatingActionButton( onPressed: () async { StaffMemberModel? createdBy = await getStaffMember(context); if (createdBy == null || !context.mounted) return; context.pushNamed( Routes.operationForm, pathParameters: {'id': 'new'}, extra: (createdBy: createdBy, operation: null), ); }, child: const Icon(Icons.add), ), ); } // --- COSTRUZIONE DELLA COMPONENTISTICA DETTAGLIATA --- Widget _buildResponsiveCard(OperationModel operation) { final isSelected = _selectedOperationIds.contains(operation.id); return _RichOperationCard( operation: operation, isSelected: isSelected, isSelectionMode: _isSelectionMode, onTap: () { if (_isSelectionMode) { _toggleSelection(operation.id!); } else { context.pushNamed( Routes.operationForm, extra: (createdBy: null, operation: operation), pathParameters: {'id': operation.id!}, ); } }, onLongPress: () => _toggleSelection(operation.id!), ); } // 🥷 LA BARRA DI PAGINAZIONE DESKTOP (Stile Gmail / Typesense) Widget _buildDesktopPaginationFooter(OperationListState state) { final theme = Theme.of(context); final cubit = context.read(); // Calcolo intervallo visualizzato (es. 1-25 di 140) final fromItem = ((state.currentPage - 1) * state.itemsPerPage) + 1; final toItem = DateUtils.isSameDay(DateTime.now(), DateTime.now()) // segnaposto logico ? (fromItem + state.operations.length - 1) : fromItem; return Container( height: 56, padding: const EdgeInsets.symmetric(horizontal: 24), decoration: BoxDecoration( color: theme.colorScheme.surface, border: Border(top: BorderSide(color: theme.dividerColor, width: 0.5)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Info totali a sinistra Text( "$fromItem-$toItem di ${state.totalItems} pratiche totali", style: theme.textTheme.bodyMedium?.copyWith( color: Colors.grey[700], ), ), // Controlli di navigazione a destra Row( children: [ // Prima Pagina IconButton( icon: const Icon(Icons.first_page), onPressed: state.currentPage > 1 ? () => cubit.loadSpecificPageDesktop(1) : null, ), // Pagina Precedente IconButton( icon: const Icon(Icons.chevron_left), onPressed: state.currentPage > 1 ? () => cubit.loadSpecificPageDesktop(state.currentPage - 1) : null, ), // Indicatore numerico centrale impacchettato Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: Text( "Pagina ${state.currentPage} di ${state.totalPages}", style: const TextStyle(fontWeight: FontWeight.bold), ), ), // Pagina Successiva IconButton( icon: const Icon(Icons.chevron_right), onPressed: state.currentPage < state.totalPages ? () => cubit.loadSpecificPageDesktop(state.currentPage + 1) : null, ), // Ultima Pagina IconButton( icon: const Icon(Icons.last_page), onPressed: state.currentPage < state.totalPages ? () => cubit.loadSpecificPageDesktop(state.totalPages) : null, ), ], ), ], ), ); } } // ========================================================================= // 🥷 3. LA CARD RICCA, REATTIVA E DEFINITIVA (Quella revisionata insieme) // ========================================================================= class _RichOperationCard extends StatelessWidget { final OperationModel operation; final bool isSelected; final bool isSelectionMode; final VoidCallback onTap; final VoidCallback onLongPress; const _RichOperationCard({ required this.operation, required this.isSelected, required this.isSelectionMode, required this.onTap, required this.onLongPress, }); Color _getStatusColor(OperationStatus status) { switch (status) { case OperationStatus.success: return Colors.green; case OperationStatus.waitingForAction: case OperationStatus.draft: return Colors.orange; case OperationStatus.waitingForSupport: return Colors.blue; case OperationStatus.failure: return Colors.grey.shade800; } } Color _getTypeColor(String type) { switch (type) { case 'FIN': return Colors.deepPurple; case 'TELEPASS': return Colors.yellow.shade700; case 'ENERGY': return Colors.amber.shade700; case 'ENTERTAINMENT': return Colors.pinkAccent; case 'AL': case 'MNP': return Colors.indigo; case 'NIP': case 'FWA': return Colors.cyan; default: return Colors.blueGrey; } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final statusColor = _getStatusColor(operation.status); final typeColor = _getTypeColor(operation.type); return Card( margin: EdgeInsets.zero, // Gestito dai margini dei padri (griglia/lista) elevation: isSelected ? 4 : 1, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide( color: isSelected ? theme.colorScheme.primary : Colors.transparent, width: 2, ), ), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: onTap, onLongPress: onLongPress, child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Container( decoration: BoxDecoration( color: isSelected ? theme.colorScheme.primaryContainer.withValues(alpha: 0.15) : null, // 🥷 COERENZA 100%: Banda laterale legata allo status per eliminare i malintesi cromatici border: Border(left: BorderSide(color: statusColor, width: 6)), ), padding: const EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // --- LINEA HEADER --- Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ if (isSelectionMode) Padding( padding: const EdgeInsets.only(right: 8.0), child: SizedBox( height: 18, width: 18, child: Checkbox( value: isSelected, onChanged: (_) => onTap(), ), ), ), Expanded( child: Text( (operation.reference.isEmpty) ? 'Senza Riferimento' : operation.reference, style: theme.textTheme.labelSmall?.copyWith( color: Colors.grey[600], ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), Text( operation.createdAt != null ? "${operation.createdAt!.day.toString().padLeft(2, '0')}/${operation.createdAt!.month.toString().padLeft(2, '0')}/${operation.createdAt!.year}" : '', style: theme.textTheme.labelSmall?.copyWith( color: Colors.grey[600], ), ), ], ), const SizedBox(height: 8), // --- LINEA CENTRALE: CLIENTE + INSERTO OPERATIVO --- Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( operation.customer?.name ?? "Cliente sconosciuto", style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 15, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), const SizedBox(width: 8), // 🥷 IL RE DEL SERVICE: Il tipo operazione svetta con box e contrasto ad hoc Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: typeColor.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(6), border: Border.all( color: typeColor.withValues(alpha: 0.25), ), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (_getIconForType( operation.type, operation.subType, ) != null) ...[ Icon( _getIconForType( operation.type, operation.subType, ), size: 13, color: typeColor, ), const SizedBox(width: 4), ], Text( (operation.subType != null && operation.subType!.isNotEmpty) ? operation.subType! : operation.type, style: TextStyle( color: typeColor, fontWeight: FontWeight.bold, fontSize: 11, ), ), ], ), ), ], ), const SizedBox(height: 10), // --- LINEA DEI TAG TECNICI --- Wrap( spacing: 6, runSpacing: 4, children: [ // Tag Target Espanso (Privato / Business) _MiniChip( label: operation.isBusiness ? 'Business' : 'Privato', icon: operation.isBusiness ? Icons.business : Icons.person, color: operation.isBusiness ? Colors.indigo : Colors.teal, ), // Tag Gestore (Agganciato dinamicamente al displayColor generato dall'esadecimale del DB!) if (operation.provider != null) _MiniChip( label: operation.provider?.name ?? 'Gestore', color: operation.provider?.displayColor ?? Colors.blueGrey, ), // Specifiche addizionali del Finanziamento if (operation.type == 'Fin' && operation.modelId != null) _MiniChip( label: operation.modelDisplayName ?? 'Prodotto', icon: Icons.devices, color: Colors.deepPurple, ), ], ), const Spacer(), // --- FOOTER CARD: AGENTE + CHIP STATO --- Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ const Icon( Icons.support_agent, size: 14, color: Colors.grey, ), const SizedBox(width: 4), Text( operation.staffDisplayName ?? 'Assegnato', style: theme.textTheme.labelSmall?.copyWith( color: Colors.grey[700], ), ), ], ), Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: statusColor, borderRadius: BorderRadius.circular(6), ), child: Text( operation.status.displayName, style: const TextStyle( fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold, ), ), ), ], ), ], ), ), ), ), ); } IconData? _getIconForType(String type, String? subtype) { if (type == 'Energy') { if (subtype?.toLowerCase() == 'luce') return Icons.bolt; if (subtype?.toLowerCase() == 'gas') return Icons.local_fire_department; } return null; } } // Micro Widget di supporto per i tag interni class _MiniChip extends StatelessWidget { final String label; final IconData? icon; final Color color; const _MiniChip({required this.label, this.icon, required this.color}); @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), decoration: BoxDecoration( color: color.withValues(alpha: 0.08), border: Border.all(color: color.withValues(alpha: 0.25)), borderRadius: BorderRadius.circular(4), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ if (icon != null) ...[ Icon(icon, size: 11, color: color), const SizedBox(width: 4), ], Text( label, style: TextStyle( fontSize: 10, color: color, fontWeight: FontWeight.bold, ), ), ], ), ); } }