From 3eba1a32ec060e844a8f0f54b12eb879eba7d17f Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sat, 18 Apr 2026 00:40:32 +0200 Subject: [PATCH] Refactor energy service models: remove unused imports, enhance energy service dialog with smart expiration presets, and improve UI elements in services grid. --- .../services/models/energy_service_model.dart | 2 - .../energy_service_dialog.dart | 98 ++++- .../finance_service_dialog.dart | 380 ++++++++++++++++++ .../ui/service_form_screen/services_grid.dart | 2 - 4 files changed, 471 insertions(+), 11 deletions(-) create mode 100644 lib/features/services/ui/service_form_screen/finance_service_dialog.dart diff --git a/lib/features/services/models/energy_service_model.dart b/lib/features/services/models/energy_service_model.dart index 5f35833..9cf9b54 100644 --- a/lib/features/services/models/energy_service_model.dart +++ b/lib/features/services/models/energy_service_model.dart @@ -1,6 +1,4 @@ import 'package:equatable/equatable.dart'; -import 'package:flux/features/services/models/entertainment_service_model.dart'; -import 'package:flux/features/services/models/fin_service_model.dart'; enum EnergyType { luce, gas } // Mappa il tuo public.energy_type diff --git a/lib/features/services/ui/service_form_screen/energy_service_dialog.dart b/lib/features/services/ui/service_form_screen/energy_service_dialog.dart index d08b580..58da992 100644 --- a/lib/features/services/ui/service_form_screen/energy_service_dialog.dart +++ b/lib/features/services/ui/service_form_screen/energy_service_dialog.dart @@ -134,7 +134,7 @@ class _EnergyList extends StatelessWidget { child: ListView.separated( shrinkWrap: true, itemCount: services.length, - separatorBuilder: (_, __) => const Divider(height: 1), + separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { final s = services[index]; final isLuce = s.type == EnergyType.luce; @@ -144,7 +144,7 @@ class _EnergyList extends StatelessWidget { (p) => p.id == s.providerId, ); final providerName = providerIndex >= 0 - ? (activeProviders[providerIndex].nome ?? 'Sconosciuto') + ? (activeProviders[providerIndex].nome) : 'Gestore Rimosso/Sconosciuto'; // Formattazione data pulita (es. 04/09/2025) @@ -206,6 +206,17 @@ class _EnergyFormState extends State<_EnergyForm> { EnergyType _selectedType = EnergyType.luce; String? _selectedProviderId; DateTime? _selectedExpiration; + int? _selectedMonthsPreset; + + void _applyPreset(int? months) { + if (months == null) return; + setState(() { + _selectedMonthsPreset = months; + // Calcoliamo la data: oggi + X mesi + final now = DateTime.now(); + _selectedExpiration = DateTime(now.year, now.month + months, now.day); + }); + } Future _pickDate() async { final picked = await showDatePicker( @@ -246,7 +257,83 @@ class _EnergyFormState extends State<_EnergyForm> { setState(() => _selectedType = newSelection.first); }, ), - const SizedBox(height: 16), + const SizedBox(height: 20), + // 2. SCADENZA INTELLIGENTE (La parte PRO) + const Text( + "Scadenza Contratto", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + + SegmentedButton( + showSelectedIcon: false, // Per un look più pulito + segments: const [ + ButtonSegment(value: 12, label: Text("12m")), + ButtonSegment(value: 24, label: Text("24m")), + ButtonSegment(value: 36, label: Text("36m")), + ButtonSegment( + value: null, + label: Icon(Icons.calendar_month, size: 20), + ), + ], + selected: {_selectedMonthsPreset}, + onSelectionChanged: (Set newSelection) { + final val = newSelection.first; + if (val == null) { + _pickDate(); // Se clicca l'icona calendario, apre il picker + } else { + _applyPreset(val); // Altrimenti applica 12, 24 o 36 + } + }, + ), + + const SizedBox(height: 12), + + // Visualizzazione della data calcolata (o scelta) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: _selectedExpiration != null + ? Theme.of(context).colorScheme.primary + : Colors.transparent, + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.event, + size: 18, + color: _selectedExpiration != null + ? Theme.of(context).colorScheme.primary + : Colors.grey, + ), + const SizedBox(width: 8), + Text( + _selectedExpiration != null + ? "Scade il: ${_selectedExpiration!.day.toString().padLeft(2, '0')}/${_selectedExpiration!.month.toString().padLeft(2, '0')}/${_selectedExpiration!.year}" + : "Seleziona una scadenza", + style: TextStyle( + fontWeight: FontWeight.bold, + color: _selectedExpiration != null + ? Theme.of(context).colorScheme.onSurface + : Colors.grey, + ), + ), + ], + ), + ), + + const SizedBox(height: 20), // 2. Provider Dropdown BlocBuilder( @@ -273,10 +360,7 @@ class _EnergyFormState extends State<_EnergyForm> { ), initialValue: _selectedProviderId, items: energyProviders.map((p) { - return DropdownMenuItem( - value: p.id, - child: Text(p.nome ?? 'Sconosciuto'), - ); + return DropdownMenuItem(value: p.id, child: Text(p.nome)); }).toList(), onChanged: (val) => setState(() => _selectedProviderId = val), ); diff --git a/lib/features/services/ui/service_form_screen/finance_service_dialog.dart b/lib/features/services/ui/service_form_screen/finance_service_dialog.dart new file mode 100644 index 0000000..2efbfcd --- /dev/null +++ b/lib/features/services/ui/service_form_screen/finance_service_dialog.dart @@ -0,0 +1,380 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; +import 'package:flux/features/products/blocs/product_cubit.dart'; +import 'package:flux/features/services/models/fin_service_model.dart'; +import 'package:flux/features/master_data/providers/models/provider_model.dart'; +import 'package:flux/features/products/models/model_model.dart'; + +// =========================================================================== +// DIALOG PRINCIPALE +// =========================================================================== +class FinanceServiceDialog extends StatefulWidget { + final List initialServices; + final String currentStoreId; + + const FinanceServiceDialog({ + super.key, + required this.initialServices, + required this.currentStoreId, + }); + + @override + State createState() => _FinanceServiceDialogState(); +} + +class _FinanceServiceDialogState extends State { + late List _tempList; + bool _isAddingNew = false; + + @override + void initState() { + super.initState(); + _tempList = List.from(widget.initialServices); + // Carichiamo i dati necessari dai Cubit + context.read().loadActiveProvidersForStore( + widget.currentStoreId, + ); + context.read().loadBrands(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Row( + children: [ + Icon( + Icons.payments_outlined, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text(_isAddingNew ? "Dettagli Finanziamento" : "Finanziamenti"), + ], + ), + content: AnimatedSize( + duration: const Duration(milliseconds: 300), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + child: _isAddingNew + ? _FinanceForm( + onSave: (newFin) => setState(() { + _tempList.add(newFin); + _isAddingNew = false; + }), + onCancel: () => setState(() => _isAddingNew = false), + ) + : BlocBuilder( + builder: (context, provState) { + return BlocBuilder( + builder: (context, prodState) { + return _FinanceList( + services: _tempList, + allProviders: + provState.allProviders, // Per vedere lo storico + allModels: prodState.models, + onDelete: (index) => + setState(() => _tempList.removeAt(index)), + onAddTap: () => setState(() => _isAddingNew = true), + ); + }, + ); + }, + ), + ), + ), + actions: !_isAddingNew + ? [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text("Annulla"), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, _tempList), + child: const Text("Conferma"), + ), + ] + : null, + ); + } +} + +// =========================================================================== +// VISTA LISTA (STORICA) +// =========================================================================== +class _FinanceList extends StatelessWidget { + final List services; + final List allProviders; + final List allModels; + final Function(int) onDelete; + final VoidCallback onAddTap; + + const _FinanceList({ + required this.services, + required this.allProviders, + required this.allModels, + required this.onDelete, + required this.onAddTap, + }); + + @override + Widget build(BuildContext context) { + if (services.isEmpty) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.symmetric(vertical: 32.0), + child: Text( + "Nessun finanziamento inserito.", + style: TextStyle(color: Colors.grey), + ), + ), + OutlinedButton.icon( + onPressed: onAddTap, + icon: const Icon(Icons.add), + label: const Text("Aggiungi primo"), + ), + ], + ); + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Flexible( + child: ListView.separated( + shrinkWrap: true, + itemCount: services.length, + separatorBuilder: (_, _) => const Divider(), + itemBuilder: (context, index) { + final s = services[index]; + + // Cerchiamo il nome del provider in TUTTI quelli caricati (storico) + final providerName = allProviders + .firstWhere( + (p) => p.id == s.providerId, + orElse: () => ProviderModel( + id: '', + nome: 'Operatore Storico', + companyId: '', + isActive: false, + energia: false, + telefoniaFissa: false, + telefoniaMobile: false, + assicurazioni: false, + altro: false, + intrattenimento: false, + ), + ) + .nome; + + // Cerchiamo il nome del modello + final modelName = allModels + .firstWhere( + (m) => m.id == s.modelId, + orElse: () => ModelModel( + id: '', + name: 'Prodotto', + nameWithBrand: 'Prodotto Storico', + brandId: '', + ), + ) + .nameWithBrand; + + final dateStr = + "${s.expiration.day.toString().padLeft(2, '0')}/${s.expiration.month.toString().padLeft(2, '0')}/${s.expiration.year}"; + + return ListTile( + title: Text( + modelName, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text("$providerName • Scade: $dateStr"), + trailing: IconButton( + icon: const Icon(Icons.delete_outline, color: Colors.red), + onPressed: () => onDelete(index), + ), + ); + }, + ), + ), + const SizedBox(height: 16), + TextButton.icon( + onPressed: onAddTap, + icon: const Icon(Icons.add), + label: const Text("Aggiungi altro"), + ), + ], + ); + } +} + +// =========================================================================== +// FORM CON OMNI-SEARCH +// =========================================================================== +class _FinanceForm extends StatefulWidget { + final Function(FinServiceModel) onSave; + final VoidCallback onCancel; + + const _FinanceForm({required this.onSave, required this.onCancel}); + + @override + State<_FinanceForm> createState() => _FinanceFormState(); +} + +class _FinanceFormState extends State<_FinanceForm> { + String? _selectedProviderId; + ModelModel? _selectedModel; + int _selectedMonths = 30; // Default richiesto + final TextEditingController _searchController = TextEditingController(); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 1. SCELTA ISTITUTO (Solo attivi) + BlocBuilder( + builder: (context, state) { + final finProviders = state + .activeProviders; // Già filtrati dal caricamento della dialog + return DropdownButtonFormField( + initialValue: _selectedProviderId, + decoration: const InputDecoration( + labelText: "Istituto di Credito", + border: OutlineInputBorder(), + ), + items: finProviders + .map( + (p) => DropdownMenuItem(value: p.id, child: Text(p.nome)), + ) + .toList(), + onChanged: (val) => setState(() => _selectedProviderId = val), + ); + }, + ), + const SizedBox(height: 16), + + // 2. RICERCA MODELLO + if (_selectedModel == null) ...[ + TextField( + controller: _searchController, + decoration: InputDecoration( + hintText: "Cerca modello (es: iPhone...)", + prefixIcon: const Icon(Icons.search), + border: const OutlineInputBorder(), + suffixIcon: IconButton( + icon: const Icon(Icons.add_circle_outline), + onPressed: () => _showQuickCreate(context), + ), + ), + onChanged: (val) { + // Qui andrebbe il debouncing per filtrare i modelli nel Cubit + }, + ), + const SizedBox(height: 8), + _buildSearchSuggestions(), + ] else + Card( + color: Theme.of(context).colorScheme.secondaryContainer, + child: ListTile( + leading: const Icon(Icons.phone_android), + title: Text( + _selectedModel!.nameWithBrand, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + trailing: IconButton( + icon: const Icon(Icons.close), + onPressed: () => setState(() => _selectedModel = null), + ), + ), + ), + + const SizedBox(height: 16), + + // 3. DURATA PRESET + const Text( + "Durata Rate", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 12, + color: Colors.grey, + ), + ), + const SizedBox(height: 8), + SegmentedButton( + segments: const [ + ButtonSegment(value: 24, label: Text("24m")), + ButtonSegment(value: 30, label: Text("30m")), + ButtonSegment(value: 48, label: Text("48m")), + ], + selected: {_selectedMonths}, + onSelectionChanged: (val) => + setState(() => _selectedMonths = val.first), + ), + const SizedBox(height: 24), + + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: widget.onCancel, + child: const Text("Indietro"), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: (_selectedProviderId == null || _selectedModel == null) + ? null + : () { + final now = DateTime.now(); + widget.onSave( + FinServiceModel( + providerId: _selectedProviderId!, + modelId: _selectedModel!.id!, + expiration: DateTime( + now.year, + now.month + _selectedMonths, + now.day, + ), + ), + ); + }, + child: const Text("Salva"), + ), + ], + ), + ], + ); + } + + Widget _buildSearchSuggestions() { + return BlocBuilder( + builder: (context, state) { + final query = _searchController.text.toLowerCase(); + if (query.isEmpty) return const SizedBox.shrink(); + + final filtered = state.models + .where((m) => m.nameWithBrand.toLowerCase().contains(query)) + .take(3) + .toList(); + + return Column( + children: filtered + .map( + (m) => ListTile( + title: Text(m.nameWithBrand), + onTap: () => setState(() => _selectedModel = m), + dense: true, + ), + ) + .toList(), + ); + }, + ); + } + + void _showQuickCreate(BuildContext context) { + // Implementazione rapida dialog creazione Brand/Modello come discusso prima + } +} diff --git a/lib/features/services/ui/service_form_screen/services_grid.dart b/lib/features/services/ui/service_form_screen/services_grid.dart index ded2450..fb441ee 100644 --- a/lib/features/services/ui/service_form_screen/services_grid.dart +++ b/lib/features/services/ui/service_form_screen/services_grid.dart @@ -144,7 +144,6 @@ class ServicesGrid extends StatelessWidget { color: Colors.teal, onTap: () { // TODO: Aprire la Dialog Finanziamenti complessa - print("Apri Fin Dialog"); }, ), ActionCard( @@ -154,7 +153,6 @@ class ServicesGrid extends StatelessWidget { color: Colors.redAccent, onTap: () { // TODO: Aprire la Dialog Contenuti complessa - print("Apri Contenuti Dialog"); }, ), ],