Refactor energy service models: remove unused imports, enhance energy service dialog with smart expiration presets, and improve UI elements in services grid.

This commit is contained in:
2026-04-18 00:40:32 +02:00
parent 60a8b00bcd
commit 3eba1a32ec
4 changed files with 471 additions and 11 deletions

View File

@@ -1,6 +1,4 @@
import 'package:equatable/equatable.dart'; 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 enum EnergyType { luce, gas } // Mappa il tuo public.energy_type

View File

@@ -134,7 +134,7 @@ class _EnergyList extends StatelessWidget {
child: ListView.separated( child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
itemCount: services.length, itemCount: services.length,
separatorBuilder: (_, __) => const Divider(height: 1), separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final s = services[index]; final s = services[index];
final isLuce = s.type == EnergyType.luce; final isLuce = s.type == EnergyType.luce;
@@ -144,7 +144,7 @@ class _EnergyList extends StatelessWidget {
(p) => p.id == s.providerId, (p) => p.id == s.providerId,
); );
final providerName = providerIndex >= 0 final providerName = providerIndex >= 0
? (activeProviders[providerIndex].nome ?? 'Sconosciuto') ? (activeProviders[providerIndex].nome)
: 'Gestore Rimosso/Sconosciuto'; : 'Gestore Rimosso/Sconosciuto';
// Formattazione data pulita (es. 04/09/2025) // Formattazione data pulita (es. 04/09/2025)
@@ -206,6 +206,17 @@ class _EnergyFormState extends State<_EnergyForm> {
EnergyType _selectedType = EnergyType.luce; EnergyType _selectedType = EnergyType.luce;
String? _selectedProviderId; String? _selectedProviderId;
DateTime? _selectedExpiration; 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<void> _pickDate() async { Future<void> _pickDate() async {
final picked = await showDatePicker( final picked = await showDatePicker(
@@ -246,7 +257,83 @@ class _EnergyFormState extends State<_EnergyForm> {
setState(() => _selectedType = newSelection.first); 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<int?>(
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<int?> 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 // 2. Provider Dropdown
BlocBuilder<ProvidersCubit, ProvidersState>( BlocBuilder<ProvidersCubit, ProvidersState>(
@@ -273,10 +360,7 @@ class _EnergyFormState extends State<_EnergyForm> {
), ),
initialValue: _selectedProviderId, initialValue: _selectedProviderId,
items: energyProviders.map((p) { items: energyProviders.map((p) {
return DropdownMenuItem( return DropdownMenuItem(value: p.id, child: Text(p.nome));
value: p.id,
child: Text(p.nome ?? 'Sconosciuto'),
);
}).toList(), }).toList(),
onChanged: (val) => setState(() => _selectedProviderId = val), onChanged: (val) => setState(() => _selectedProviderId = val),
); );

View File

@@ -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<FinServiceModel> initialServices;
final String currentStoreId;
const FinanceServiceDialog({
super.key,
required this.initialServices,
required this.currentStoreId,
});
@override
State<FinanceServiceDialog> createState() => _FinanceServiceDialogState();
}
class _FinanceServiceDialogState extends State<FinanceServiceDialog> {
late List<FinServiceModel> _tempList;
bool _isAddingNew = false;
@override
void initState() {
super.initState();
_tempList = List.from(widget.initialServices);
// Carichiamo i dati necessari dai Cubit
context.read<ProvidersCubit>().loadActiveProvidersForStore(
widget.currentStoreId,
);
context.read<ProductCubit>().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<ProvidersCubit, ProvidersState>(
builder: (context, provState) {
return BlocBuilder<ProductCubit, ProductState>(
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<FinServiceModel> services;
final List<ProviderModel> allProviders;
final List<ModelModel> 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<ProvidersCubit, ProvidersState>(
builder: (context, state) {
final finProviders = state
.activeProviders; // Già filtrati dal caricamento della dialog
return DropdownButtonFormField<String>(
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<int>(
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<ProductCubit, ProductState>(
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
}
}

View File

@@ -144,7 +144,6 @@ class ServicesGrid extends StatelessWidget {
color: Colors.teal, color: Colors.teal,
onTap: () { onTap: () {
// TODO: Aprire la Dialog Finanziamenti complessa // TODO: Aprire la Dialog Finanziamenti complessa
print("Apri Fin Dialog");
}, },
), ),
ActionCard( ActionCard(
@@ -154,7 +153,6 @@ class ServicesGrid extends StatelessWidget {
color: Colors.redAccent, color: Colors.redAccent,
onTap: () { onTap: () {
// TODO: Aprire la Dialog Contenuti complessa // TODO: Aprire la Dialog Contenuti complessa
print("Apri Contenuti Dialog");
}, },
), ),
], ],