diff --git a/lib/core/layout/app_shell.dart b/lib/core/layout/app_shell.dart index f6dd2cb..9cc0c61 100644 --- a/lib/core/layout/app_shell.dart +++ b/lib/core/layout/app_shell.dart @@ -89,10 +89,16 @@ class _AppMenuState extends State { Icon(Icons.bolt, color: theme.colorScheme.primary, size: 32), if (!effectivelyCollapsed) ...[ const SizedBox(width: 12), - Text( - "FLUX", - style: theme.textTheme.titleLarge?.copyWith( - fontWeight: FontWeight.bold, + TextButton( + onPressed: () { + if (widget.isDrawer) Navigator.pop(context); + context.goNamed(Routes.home); + }, + child: Text( + "FLUX", + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), ), ), ], @@ -111,13 +117,36 @@ class _AppMenuState extends State { padding: const EdgeInsets.symmetric(horizontal: 8.0), children: [ _buildRouteItem( - title: context.l10n.commonDashboard, + title: 'Dashboard', icon: Icons.dashboard_outlined, - routeName: Routes.home, // <--- Usiamo la tua costante! + routeName: Routes.home, pathToCheck: '/', // Il path da controllare per colorarlo isCollapsed: effectivelyCollapsed, ), + const SizedBox(height: 8), + // --- SEZIONE OPERATIVA --- + _buildHierarchicalItem( + title: 'Operatività', + icon: Icons.work_outline, + basePathToCheck: '/', + isCollapsed: effectivelyCollapsed, + subItems: [ + _SubMenuItem( + 'Operazioni', + Routes.operations, + '/operations', + ), + _SubMenuItem( + 'Assistenza', + Routes.tickets, + '/tickets', + ), + _SubMenuItem('Tasks', Routes.tasks, '/tasks'), + _SubMenuItem('Sticky Notes', Routes.notes, '/notes'), + ], + ), + const SizedBox(height: 8), // --- IL MENU GERARCHICO (ANAGRAFICHE) --- @@ -256,7 +285,9 @@ class _AppMenuState extends State { required bool isCollapsed, required List<_SubMenuItem> subItems, }) { - final isSelected = widget.currentPath.startsWith(basePathToCheck); + final isSelected = subItems.any( + (item) => widget.currentPath.startsWith(item.pathToCheck), + ); final theme = Theme.of(context); if (isCollapsed) { diff --git a/lib/features/master_data/providers/blocs/provider_form_cubit.dart b/lib/features/master_data/providers/blocs/provider_form_cubit.dart index 55f86e1..a5e612d 100644 --- a/lib/features/master_data/providers/blocs/provider_form_cubit.dart +++ b/lib/features/master_data/providers/blocs/provider_form_cubit.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/enums_and_consts/consts.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; // Per estrarre gli store import '../models/provider_model.dart'; @@ -32,7 +33,7 @@ class ProviderFormCubit extends Cubit { try { // 1. Scarichiamo tutti i negozi dell'azienda final storesResponse = await _client - .from('store') + .from(Tables.stores) .select('id, name') .eq('company_id', companyId); @@ -41,7 +42,7 @@ class ProviderFormCubit extends Cubit { if (existingProvider != null && existingProvider.id != null) { // ... (Vecchio codice di recupero) final links = await _client - .from('providers_in_stores') + .from(Tables.providersInStores) .select('store_id') .eq('provider_id', existingProvider.id!); linkedStoreIds = (links as List) @@ -83,6 +84,7 @@ class ProviderFormCubit extends Cubit { String? fiscalCode, String? sdiCode, String? emailPec, + String? Function()? colorHex, }) { emit( state.copyWith( @@ -93,6 +95,7 @@ class ProviderFormCubit extends Cubit { fiscalCode: fiscalCode, sdiCode: sdiCode, emailPec: emailPec, + colorHex: colorHex, ), ), ); diff --git a/lib/features/master_data/providers/models/provider_model.dart b/lib/features/master_data/providers/models/provider_model.dart index 1a65965..e5dfe26 100644 --- a/lib/features/master_data/providers/models/provider_model.dart +++ b/lib/features/master_data/providers/models/provider_model.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; import 'provider_location_model.dart'; import 'provider_role.dart'; @@ -8,6 +9,7 @@ class ProviderModel extends Equatable { final String companyId; final String name; // Nome "commerciale" per riconoscerlo velocemente final bool isActive; + final String? colorHex; // Dati fiscali e legali final String? businessName; // Ragione Sociale @@ -29,6 +31,7 @@ class ProviderModel extends Equatable { required this.companyId, required this.name, this.isActive = true, + this.colorHex, this.businessName, this.vatNumber, this.fiscalCode, @@ -42,6 +45,17 @@ class ProviderModel extends Equatable { this.locations, }); + // 🥷 IL GETTER MAGICO: Converte l'esadecimale in un Color di Flutter + Color get displayColor { + if (colorHex == null || colorHex!.isEmpty) { + return Colors.blueGrey; // Colore di default + } + + // Rimuove l'eventuale '#' e aggiunge 'FF' per l'opacità (Alpha) + final hex = colorHex!.replaceAll('#', ''); + return Color(int.parse('FF$hex', radix: 16)); + } + factory ProviderModel.empty({required String companyId}) { return ProviderModel( companyId: companyId, @@ -56,6 +70,7 @@ class ProviderModel extends Equatable { String? companyId, String? name, bool? isActive, + String? Function()? colorHex, String? businessName, String? vatNumber, String? fiscalCode, @@ -73,6 +88,7 @@ class ProviderModel extends Equatable { companyId: companyId ?? this.companyId, name: name ?? this.name, isActive: isActive ?? this.isActive, + colorHex: colorHex != null ? colorHex() : this.colorHex, businessName: businessName ?? this.businessName, vatNumber: vatNumber ?? this.vatNumber, fiscalCode: fiscalCode ?? this.fiscalCode, @@ -114,6 +130,7 @@ class ProviderModel extends Equatable { companyId: map['company_id'] as String, name: map['name'] as String, isActive: map['is_active'] as bool? ?? true, + colorHex: map['color_hex'] as String?, businessName: map['business_name'] as String?, vatNumber: map['vat_number'] as String?, fiscalCode: map['fiscal_code'] as String?, @@ -134,6 +151,7 @@ class ProviderModel extends Equatable { 'company_id': companyId, 'name': name, 'is_active': isActive, + 'color_hex': colorHex, 'business_name': businessName, 'vat_number': vatNumber, 'fiscal_code': fiscalCode, @@ -155,6 +173,7 @@ class ProviderModel extends Equatable { companyId, name, isActive, + colorHex, businessName, vatNumber, fiscalCode, diff --git a/lib/features/master_data/providers/ui/provider_form_screen.dart b/lib/features/master_data/providers/ui/provider_form_screen.dart index 39171c4..a4e50c5 100644 --- a/lib/features/master_data/providers/ui/provider_form_screen.dart +++ b/lib/features/master_data/providers/ui/provider_form_screen.dart @@ -66,6 +66,17 @@ class _ProviderFormScreenState extends State { super.dispose(); } + final List _brandColors = [ + '#E60000', // Vodafone/Iliad (Rosso scuro) + '#0047BB', // TIM (Blu) + '#F4811F', // WINDTRE (Arancione) + '#FFCC00', // Fastweb (Giallo) + '#00A859', // Verde generico + '#8E44AD', // Viola + '#2C3E50', // Blu scuro/Nero + '#607D8B', // BlueGrey (Default) + ]; + void _flushControllers() { context.read().updateFields( name: _nameCtrl.text.trim(), @@ -132,6 +143,8 @@ class _ProviderFormScreenState extends State { children: [ _buildGeneralCard(context, state), const SizedBox(height: 24), + _buildColorPicker(), + const SizedBox(height: 24), _buildRolesCard(context, state), const SizedBox(height: 24), _buildFiscalCard(context), @@ -392,4 +405,70 @@ class _ProviderFormScreenState extends State { ), ); } + + Widget _buildColorPicker() { + return Column( + children: [ + const Text( + 'Colore Riconoscitivo', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16), + ), + const SizedBox(height: 12), + BlocBuilder( + builder: (context, state) { + // Se non ha un colore, usiamo il BlueGrey di default + final currentColorHex = state.provider?.colorHex ?? '#607D8B'; + + return Wrap( + spacing: 12, + runSpacing: 12, + children: _brandColors.map((hexCode) { + final isSelected = + currentColorHex.toUpperCase() == hexCode.toUpperCase(); + + // Conversione rapida per disegnare il cerchio + final colorValue = Color( + int.parse('FF${hexCode.replaceAll('#', '')}', radix: 16), + ); + + return InkWell( + borderRadius: BorderRadius.circular(24), + onTap: () { + // Aggiorniamo il Cubit con il nuovo colore + context.read().updateFields( + colorHex: () => hexCode, + ); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 42, + height: 42, + decoration: BoxDecoration( + color: colorValue, + shape: BoxShape.circle, + border: Border.all( + color: isSelected ? Colors.black : Colors.transparent, + width: isSelected ? 3 : 0, + ), + boxShadow: [ + if (isSelected) + BoxShadow( + color: colorValue.withValues(alpha: 0.4), + blurRadius: 8, + spreadRadius: 2, + ), + ], + ), + child: isSelected + ? const Icon(Icons.check, color: Colors.white, size: 24) + : null, + ), + ); + }).toList(), + ); + }, + ), + ], + ); + } } diff --git a/lib/features/operations/blocs/operation_form_cubit.dart b/lib/features/operations/blocs/operation_form_cubit.dart index 886ef31..fd7bbab 100644 --- a/lib/features/operations/blocs/operation_form_cubit.dart +++ b/lib/features/operations/blocs/operation_form_cubit.dart @@ -98,17 +98,23 @@ class OperationFormCubit extends Cubit { emit( state.copyWith( - status: OperationFormStatus.ready, // Torna ready per il nuovo form + status: OperationFormStatus.ready, operation: OperationModel( companyId: current.companyId, storeId: current.storeId, storeDisplayName: current.storeDisplayName, - batchUuid: current.batchUuid, // MANTIENE IL COLLEGAMENTO - customerId: current.customerId, // MANTIENE IL CLIENTE + // 🥷 REINSERIAMO LO STAFF (Il "colpevole" era qui) + staffId: current.staffId, + staffDisplayName: current.staffDisplayName, + + batchUuid: current.batchUuid, + customerId: current.customerId, customer: current.customer, reference: current.reference, status: OperationStatus.draft, createdAt: DateTime.now(), + // Mantieni isBusiness se vuoi che rimanga coerente col cliente + isBusiness: current.isBusiness, ), ), ); @@ -213,7 +219,7 @@ class OperationFormCubit extends Cubit { String? type, String? providerId, String? providerDisplayName, - String? subtype, + String? subType, String? description, DateTime? expirationDate, int? quantity, @@ -226,7 +232,7 @@ class OperationFormCubit extends Cubit { bool clearProvider = false, bool clearType = false, - bool clearSubtype = false, + bool clearSubType = false, bool clearDescription = false, bool clearExpiration = false, bool clearQuantity = false, @@ -251,7 +257,7 @@ class OperationFormCubit extends Cubit { description: clearDescription ? null : (description ?? current.description), - subtype: clearSubtype ? null : (subtype ?? current.subtype), + subType: clearSubType ? null : (subType ?? current.subType), expirationDate: clearExpiration ? null : (expirationDate ?? current.expirationDate), @@ -287,7 +293,7 @@ class OperationFormCubit extends Cubit { }) { // 1. Aggiorniamo il tipo nel modello in canna // (Presumo tu abbia un metodo copyWith o simile) - final updatedOp = state.operation.copyWith(type: newType, subtype: ''); + final updatedOp = state.operation.copyWith(type: newType, subType: ''); // 2. Prepariamoci ad auto-selezionare il provider String? newProviderId = updatedOp.providerId; @@ -389,7 +395,7 @@ class OperationFormCubit extends Cubit { state.copyWith( operation: currentOp.copyWith( type: newType, - subtype: + subType: '', // Resettiamo il sottotipo per evitare incongruenze (es. passo da Luce a DAZN) expirationDate: defaultDate, // Impostiamo la scadenza di default se calcolata diff --git a/lib/features/operations/models/operation_model.dart b/lib/features/operations/models/operation_model.dart index 57639f6..16c4f26 100644 --- a/lib/features/operations/models/operation_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -28,7 +28,7 @@ class OperationModel extends Equatable { final String? id; final DateTime? createdAt; final String type; - final String? subtype; + final String? subType; final String? providerId; final String? providerDisplayName; final String? modelId; @@ -58,7 +58,7 @@ class OperationModel extends Equatable { this.id, this.createdAt, this.type = '', - this.subtype, + this.subType, this.providerId, this.providerDisplayName, this.modelId, @@ -87,7 +87,7 @@ class OperationModel extends Equatable { String? id, DateTime? createdAt, String? type, - String? subtype, + String? subType, String? providerId, String? providerDisplayName, String? modelId, @@ -114,7 +114,7 @@ class OperationModel extends Equatable { id: id ?? this.id, createdAt: createdAt ?? this.createdAt, type: type ?? this.type, - subtype: subtype ?? this.subtype, + subType: subType ?? this.subType, providerId: providerId ?? this.providerId, providerDisplayName: providerDisplayName ?? this.providerDisplayName, modelId: modelId ?? this.modelId, @@ -144,7 +144,7 @@ class OperationModel extends Equatable { id, createdAt, type, - subtype, + subType, providerId, providerDisplayName, modelId, @@ -180,7 +180,7 @@ class OperationModel extends Equatable { ? DateTime.parse(map['created_at']) : null, type: map['type'] as String? ?? '', - subtype: map['sub_type'] as String?, + subType: map['sub_type'] as String?, // I campi relazionali nullabili restano rigorosamente null! providerId: map['provider_id'] as String?, @@ -237,7 +237,7 @@ class OperationModel extends Equatable { return { if (id != null) 'id': id, 'type': type, - 'sub_type': subtype, + 'sub_type': subType, 'provider_id': providerId, 'model_id': modelId, 'description': description, diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index 25a71b5..b82fce4 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -79,7 +79,7 @@ class _OperationFormScreenState extends State { _noteController.text = model.note; } if (_freeTextSubtypeController.text.isEmpty) { - _freeTextSubtypeController.text = model.subtype ?? ''; + _freeTextSubtypeController.text = model.subType ?? ''; } if (_freeTextDescriptionController.text.isEmpty) { _freeTextDescriptionController.text = model.description ?? ''; @@ -91,7 +91,7 @@ class _OperationFormScreenState extends State { context.read().updateFields( reference: _referenceController.text, note: _noteController.text, - subtype: _freeTextSubtypeController.text, + subType: _freeTextSubtypeController.text, description: _freeTextDescriptionController.text, ); } diff --git a/lib/features/operations/ui/operation_list_screen.dart b/lib/features/operations/ui/operation_list_screen.dart index 5b0f405..420563f 100644 --- a/lib/features/operations/ui/operation_list_screen.dart +++ b/lib/features/operations/ui/operation_list_screen.dart @@ -1,12 +1,9 @@ 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'; -// Importa i tuoi modelli e cubit class OperationListScreen extends StatefulWidget { const OperationListScreen({super.key}); @@ -18,10 +15,13 @@ class OperationListScreen extends StatefulWidget { class _OperationListScreenState extends State { final ScrollController _scrollController = ScrollController(); + // 🥷 1. LO STATO PER LE BULK ACTIONS + final Set _selectedOperationIds = {}; + bool get _isSelectionMode => _selectedOperationIds.isNotEmpty; + @override void initState() { super.initState(); - // Agganciamo il listener per la paginazione (Scroll Infinito) _scrollController.addListener(_onScroll); } @@ -35,7 +35,6 @@ class _OperationListScreenState extends State { if (!_scrollController.hasClients) return false; final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.offset; - // Carica quando mancano 200px alla fine return currentScroll >= (maxScroll * 0.9); } @@ -45,162 +44,448 @@ class _OperationListScreenState extends State { 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) { return Scaffold( - appBar: AppBar( - title: const Text("Gestione Servizi"), - elevation: 0, - actions: [ - IconButton( - icon: const Icon(Icons.search), - onPressed: () { - // Qui potrai implementare una barra di ricerca - }, - ), - ], - ), + // 🥷 2. APPBAR DINAMICA (Standard o Modalità Selezione) + 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: Apri BottomSheet per cambiare stato a tutte le selezionate + }, + ), + ], + ) + : AppBar( + title: const Text("Gestione Servizi"), + elevation: 0, + actions: [ + IconButton( + icon: const Icon(Icons.filter_list), + onPressed: () { + // TODO: Apri drawer laterale o modal per i filtri avanzati + }, + ), + IconButton(icon: const Icon(Icons.search), onPressed: () {}), + ], + ), body: BlocBuilder( builder: (context, state) { - // 1. Stato di caricamento iniziale if (state.status == OperationListStatus.loading && state.operations.isEmpty) { return const Center(child: CircularProgressIndicator()); } - // 2. Lista vuota if (state.operations.isEmpty) { - return Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Text("Nessuna pratica trovata."), - const SizedBox(height: 10), - ElevatedButton( - onPressed: () => context - .read() - .loadOperations(refresh: true), - child: const Text("Riprova"), - ), - ], - ), - ); + return const Center(child: Text("Nessuna pratica trovata.")); } - // 3. La Lista (con Pull-to-refresh) + // 🥷 3. IL MOTORE RESPONSIVO return RefreshIndicator( onRefresh: () => context.read().loadOperations( refresh: true, ), - child: ListView.builder( - controller: _scrollController, - padding: const EdgeInsets.only(bottom: 80), // Spazio per il FAB - 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), - ), - ); - } + child: LayoutBuilder( + builder: (context, constraints) { + // Se lo schermo è largo (Desktop/Tablet), usiamo la griglia + final isDesktop = constraints.maxWidth > 700; - final operation = state.operations[index]; - return _buildOperationCard(context, operation); + return GridView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(12).copyWith(bottom: 80), + // Magia della griglia: si adatta! + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: + 450, // Larghezza massima della singola card + mainAxisExtent: + 180, // Altezza fissa della card (da aggiustare in base ai tuoi font) + crossAxisSpacing: 12, + mainAxisSpacing: 12, + ), + itemCount: state.hasReachedMax + ? state.operations.length + : state.operations.length + 1, + itemBuilder: (context, index) { + if (index >= state.operations.length) { + return const Center( + child: CircularProgressIndicator(strokeWidth: 2), + ); + } + + final operation = state.operations[index]; + 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!), + ); + }, + ); }, ), ); }, ), - floatingActionButton: 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), - ), - ); - } - - Widget _buildOperationCard(BuildContext context, OperationModel operation) { - return Card( - margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: ListTile( - contentPadding: const EdgeInsets.all(12), - title: Row( - children: [ - Expanded( - child: Text( - operation.customer?.name ?? "Cliente sconosciuto", - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), + floatingActionButton: _isSelectionMode + ? null // Nascondi il FAB se stai selezionando + : FloatingActionButton( + onPressed: () { + /* Tuo codice per nuova operazione */ + }, + child: const Icon(Icons.add), ), - ], - ), - subtitle: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 4), - Text( - "Pratica: ${operation.reference} • ${operation.createdAt?.day}/${operation.createdAt?.month}/${operation.createdAt?.year}", - ), - const SizedBox(height: 8), - Row( - children: [ - Text(operation.type), - const SizedBox(width: 8), - _buildOperationStatus(operation.status), - ], - ), - ], - ), - trailing: const Icon(Icons.chevron_right), - onTap: () => context.pushNamed( - Routes.operationForm, - extra: (createdBy: null, operation: operation), - pathParameters: {'id': operation.id!}, - ), - ), ); } - - Widget _buildOperationStatus(OperationStatus status) { - Color color; - switch (status) { - case OperationStatus.failure: - color = Colors.grey.shade800; - break; - case OperationStatus.waitingForAction || OperationStatus.draft: - color = Colors.orange; - break; - case OperationStatus.success: - color = Colors.green; - break; - case OperationStatus.waitingForSupport: - color = Colors.blue; - break; - } - return Chip( - label: Text("BOZZA", style: TextStyle(fontSize: 10, color: Colors.white)), - backgroundColor: color, - visualDensity: VisualDensity.compact, - ); - } - - void startNewOperation(BuildContext context) { - context.pushNamed('operation-form', pathParameters: {'id': 'new'}); - } +} + +// 🥷 4. LA SUPER CARD ESTRATTA +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, + }); + + // 🥷 1. IL COLORE DELLO STATO: Centralizzato per usarlo ovunque + 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; // O Colors.red se preferisci + } + } + + // 🥷 2. IL COLORE DEL TIPO: Per farlo risaltare + 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( + 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.2) + : null, + // BANDA LATERALE LEGATA ALLO STATO (Stilosissima) + border: Border(left: BorderSide(color: statusColor, width: 6)), + ), + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // --- HEADER --- + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + if (isSelectionMode) + SizedBox( + height: 24, + width: 24, + child: Checkbox( + value: isSelected, + onChanged: (_) => onTap(), + ), + ), + Expanded( + child: Text( + operation.reference ?? 'Senza Riferimento', + style: theme.textTheme.labelSmall?.copyWith( + color: Colors.grey[600], + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + "${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), + + // --- CLIENTE E TIPO OPERAZIONE --- + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + operation.customer?.name ?? "Cliente sconosciuto", + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + // IL TIPO DI OPERAZIONE CHE SPICCA + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), + decoration: BoxDecoration( + color: typeColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: typeColor.withValues(alpha: 0.3), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_getIconForType( + operation.type, + operation.subType, + ) != + null) ...[ + Icon( + _getIconForType( + operation.type, + operation.subType, + ), + size: 14, + color: typeColor, + ), + const SizedBox(width: 4), + ], + Text( + operation.subType?.isNotEmpty == true + ? operation.subType! + : operation.type, + style: TextStyle( + color: typeColor, + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 12), + + // --- I TAG COMPATTI (Business/Privato, Provider, Device) --- + Wrap( + spacing: 6, + runSpacing: 6, + children: [ + // Espanso in "Business" e "Privato" + _MiniChip( + label: operation.isBusiness ? 'Business' : 'Privato', + icon: operation.isBusiness + ? Icons.business + : Icons.person, + color: operation.isBusiness ? Colors.indigo : Colors.teal, + ), + + // Tag Provider con il suo colore personalizzato dal DB + if (operation.providerId != null) + _MiniChip( + label: operation.providerDisplayName ?? 'Gestore', + // Se hai popolato il campo colorHex, qui puoi usare: operation.provider?.displayColor ?? Colors.grey + color: Colors.redAccent, + ), + + if (operation.type == 'Fin' && operation.modelId != null) + _MiniChip( + label: operation.modelDisplayName ?? 'Modello', + icon: Icons.devices, + color: Colors.deepPurple, + ), + ], + ), + + const Spacer(), + + // --- FOOTER: Staff e 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 ?? 'Staff', + style: theme.textTheme.labelSmall?.copyWith( + color: Colors.grey[700], + ), + ), + ], + ), + _buildOperationStatus(operation.status, statusColor), + ], + ), + ], + ), + ), + ), + ), + ); + } + + 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; + } + + Widget _buildOperationStatus(OperationStatus status, Color statusColor) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + color: statusColor, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + status.displayName, + style: const TextStyle( + fontSize: 10, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ); + } +} + +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: 4), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + border: Border.all(color: color.withValues(alpha: 0.3)), + borderRadius: BorderRadius.circular(6), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) ...[ + Icon(icon, size: 12, color: color), + const SizedBox(width: 4), + ], + Text( + label, + style: TextStyle( + fontSize: 11, + color: color, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } } diff --git a/lib/features/operations/ui/widgets/details_section.dart b/lib/features/operations/ui/widgets/details_section.dart index cce022c..e343567 100644 --- a/lib/features/operations/ui/widgets/details_section.dart +++ b/lib/features/operations/ui/widgets/details_section.dart @@ -164,8 +164,8 @@ class OperationDetailsSection extends StatelessWidget { if (currentType == 'Energy') ...[ DropdownButtonFormField( initialValue: - (currentOp?.subtype != null && currentOp!.subtype!.isNotEmpty) - ? currentOp!.subtype + (currentOp?.subType != null && currentOp!.subType!.isNotEmpty) + ? currentOp!.subType : null, decoration: const InputDecoration(labelText: 'Dettaglio Fornitura'), items: [ @@ -174,7 +174,7 @@ class OperationDetailsSection extends StatelessWidget { ].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), onChanged: (val) { if (val != null) { - context.read().updateFields(subtype: val); + context.read().updateFields(subType: val); } }, ),