feat-insert-service #5
@@ -7,15 +7,17 @@ import 'package:get_it/get_it.dart';
|
|||||||
import '../models/provider_model.dart';
|
import '../models/provider_model.dart';
|
||||||
|
|
||||||
class ProvidersState extends Equatable {
|
class ProvidersState extends Equatable {
|
||||||
final List<ProviderModel> allProviders; // Tutti i provider della company
|
final List<ProviderModel> allProviders;
|
||||||
final List<String>
|
final List<String> associatedIds;
|
||||||
associatedIds; // ID dei provider attivi nello store selezionato
|
// NUOVO CAMPO: Lista dei provider pronti per essere usati nel form pratiche
|
||||||
|
final List<ProviderModel> activeProviders;
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
const ProvidersState({
|
const ProvidersState({
|
||||||
this.allProviders = const [],
|
this.allProviders = const [],
|
||||||
this.associatedIds = const [],
|
this.associatedIds = const [],
|
||||||
|
this.activeProviders = const [], // Inizializza
|
||||||
this.isLoading = false,
|
this.isLoading = false,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
});
|
});
|
||||||
@@ -23,14 +25,18 @@ class ProvidersState extends Equatable {
|
|||||||
ProvidersState copyWith({
|
ProvidersState copyWith({
|
||||||
List<ProviderModel>? allProviders,
|
List<ProviderModel>? allProviders,
|
||||||
List<String>? associatedIds,
|
List<String>? associatedIds,
|
||||||
|
List<ProviderModel>? activeProviders, // Aggiungi qui
|
||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
}) {
|
}) {
|
||||||
return ProvidersState(
|
return ProvidersState(
|
||||||
allProviders: allProviders ?? this.allProviders,
|
allProviders: allProviders ?? this.allProviders,
|
||||||
associatedIds: associatedIds ?? this.associatedIds,
|
associatedIds: associatedIds ?? this.associatedIds,
|
||||||
|
activeProviders: activeProviders ?? this.activeProviders, // Aggiungi qui
|
||||||
isLoading: isLoading ?? this.isLoading,
|
isLoading: isLoading ?? this.isLoading,
|
||||||
errorMessage: errorMessage,
|
errorMessage:
|
||||||
|
errorMessage ??
|
||||||
|
this.errorMessage, // Correzione bug: mancava "?? this.errorMessage" nel tuo originale
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +44,7 @@ class ProvidersState extends Equatable {
|
|||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
allProviders,
|
allProviders,
|
||||||
associatedIds,
|
associatedIds,
|
||||||
|
activeProviders, // Aggiungi qui
|
||||||
isLoading,
|
isLoading,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
];
|
];
|
||||||
@@ -74,6 +81,23 @@ class ProvidersCubit extends Cubit<ProvidersState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadActiveProvidersForStore(String storeId) async {
|
||||||
|
emit(state.copyWith(isLoading: true));
|
||||||
|
try {
|
||||||
|
final activeList = await _repository.fetchActiveProvidersForStore(
|
||||||
|
storeId,
|
||||||
|
);
|
||||||
|
emit(state.copyWith(activeProviders: activeList, isLoading: false));
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: "Errore caricamento gestori: $e",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Aggiunge o rimuove l'associazione con lo store
|
// Aggiunge o rimuove l'associazione con lo store
|
||||||
Future<void> toggleProviderAssociation({
|
Future<void> toggleProviderAssociation({
|
||||||
required String providerId,
|
required String providerId,
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ActionCard extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final int count;
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const ActionCard({
|
||||||
|
super.key,
|
||||||
|
required this.label,
|
||||||
|
required this.count,
|
||||||
|
required this.icon,
|
||||||
|
required this.color,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isActive = count > 0;
|
||||||
|
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
width: 110, // Larghezza fissa per avere una griglia ordinata
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isActive
|
||||||
|
? color.withValues(alpha: 0.15)
|
||||||
|
: Theme.of(context).cardColor,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: isActive ? color : Colors.grey.withValues(alpha: 0.3),
|
||||||
|
width: isActive ? 2 : 1,
|
||||||
|
),
|
||||||
|
boxShadow: isActive
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: color.withValues(alpha: 0.2),
|
||||||
|
blurRadius: 8,
|
||||||
|
spreadRadius: 1,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: isActive ? color : Colors.grey, size: 28),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
|
||||||
|
color: isActive ? color : Colors.grey.shade700,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
if (isActive) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
count.toString(),
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:flux/features/customers/ui/customer_search_sheet.dart';
|
import 'package:flux/features/customers/ui/customer_search_sheet.dart';
|
||||||
import 'package:flux/features/services/models/service_model.dart';
|
import 'package:flux/features/services/models/service_model.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,333 @@
|
|||||||
|
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/master_data/providers/models/provider_model.dart';
|
||||||
|
import 'package:flux/features/services/models/energy_service_model.dart'; // Assicurati degli import
|
||||||
|
|
||||||
|
class EnergyServiceDialog extends StatefulWidget {
|
||||||
|
final List<EnergyServiceModel> initialServices;
|
||||||
|
final String
|
||||||
|
currentStoreId; // Ci serve per sapere per quale negozio caricare i gestori
|
||||||
|
|
||||||
|
const EnergyServiceDialog({
|
||||||
|
super.key,
|
||||||
|
required this.initialServices,
|
||||||
|
required this.currentStoreId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EnergyServiceDialog> createState() => _EnergyServiceDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EnergyServiceDialogState extends State<EnergyServiceDialog> {
|
||||||
|
// Lista temporanea per non "sporcare" il cubit finché non si preme Conferma
|
||||||
|
late List<EnergyServiceModel> _tempList;
|
||||||
|
bool _isAddingNew = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_tempList = List.from(widget.initialServices);
|
||||||
|
// Al caricamento della modale, chiediamo al Cubit di recuperare i gestori veri!
|
||||||
|
context.read<ProvidersCubit>().loadActiveProvidersForStore(
|
||||||
|
widget.currentStoreId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.bolt, color: Theme.of(context).colorScheme.primary),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(_isAddingNew ? "Nuovo Contratto" : "Servizi Energia"),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
content: AnimatedSize(
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
// Cambia vista in base al flag
|
||||||
|
child: _isAddingNew
|
||||||
|
? _EnergyForm(
|
||||||
|
onSave: (newService) {
|
||||||
|
setState(() {
|
||||||
|
_tempList.add(newService);
|
||||||
|
_isAddingNew = false; // Torna alla lista
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onCancel: () {
|
||||||
|
setState(() => _isAddingNew = false);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: _EnergyList(
|
||||||
|
services: _tempList,
|
||||||
|
onDelete: (index) {
|
||||||
|
setState(() => _tempList.removeAt(index));
|
||||||
|
},
|
||||||
|
onAddTap: () {
|
||||||
|
setState(() => _isAddingNew = true); // Passa al form
|
||||||
|
},
|
||||||
|
activeProviders: [
|
||||||
|
// Passiamo i provider attivi filtrati per tipo Energia
|
||||||
|
...context
|
||||||
|
.read<ProvidersCubit>()
|
||||||
|
.state
|
||||||
|
.activeProviders
|
||||||
|
.where((p) => p.energia == true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
if (!_isAddingNew) ...[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text("Annulla"),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context, _tempList),
|
||||||
|
child: const Text("Conferma Tutti"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// VISTA 1: LA LISTA DEI CONTRATTI
|
||||||
|
// ==========================================
|
||||||
|
class _EnergyList extends StatelessWidget {
|
||||||
|
final List<EnergyServiceModel> services;
|
||||||
|
final List<ProviderModel>
|
||||||
|
activeProviders; // <--- NUOVO: La lista vera dal Cubit
|
||||||
|
final Function(int) onDelete;
|
||||||
|
final VoidCallback onAddTap;
|
||||||
|
|
||||||
|
const _EnergyList({
|
||||||
|
required this.services,
|
||||||
|
required this.activeProviders, // <--- Richiesto
|
||||||
|
required this.onDelete,
|
||||||
|
required this.onAddTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
if (services.isEmpty)
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 32.0),
|
||||||
|
child: Text(
|
||||||
|
"Nessun contratto energia inserito.",
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Flexible(
|
||||||
|
child: ListView.separated(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: services.length,
|
||||||
|
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final s = services[index];
|
||||||
|
final isLuce = s.type == EnergyType.luce;
|
||||||
|
|
||||||
|
// LA MAGIA: Troviamo il nome partendo dall'ID salvato nel servizio
|
||||||
|
final providerIndex = activeProviders.indexWhere(
|
||||||
|
(p) => p.id == s.providerId,
|
||||||
|
);
|
||||||
|
final providerName = providerIndex >= 0
|
||||||
|
? (activeProviders[providerIndex].nome ?? 'Sconosciuto')
|
||||||
|
: 'Gestore Rimosso/Sconosciuto';
|
||||||
|
|
||||||
|
// Formattazione data pulita (es. 04/09/2025)
|
||||||
|
final day = s.expiration.day.toString().padLeft(2, '0');
|
||||||
|
final month = s.expiration.month.toString().padLeft(2, '0');
|
||||||
|
final formattedDate = "$day/$month/${s.expiration.year}";
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
leading: CircleAvatar(
|
||||||
|
backgroundColor: isLuce
|
||||||
|
? Colors.orange.shade100
|
||||||
|
: Colors.blue.shade100,
|
||||||
|
child: Icon(
|
||||||
|
isLuce
|
||||||
|
? Icons.lightbulb_outline
|
||||||
|
: Icons.local_fire_department,
|
||||||
|
color: isLuce ? Colors.orange : Colors.blue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
title: Text(
|
||||||
|
providerName,
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
subtitle: Text("Scadenza: $formattedDate"),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.delete_outline, color: Colors.red),
|
||||||
|
onPressed: () => onDelete(index),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
OutlinedButton.icon(
|
||||||
|
onPressed: onAddTap,
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text("Aggiungi Contratto"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// VISTA 2: IL FORM DI INSERIMENTO
|
||||||
|
// ==========================================
|
||||||
|
class _EnergyForm extends StatefulWidget {
|
||||||
|
final Function(EnergyServiceModel) onSave;
|
||||||
|
final VoidCallback onCancel;
|
||||||
|
|
||||||
|
const _EnergyForm({required this.onSave, required this.onCancel});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_EnergyForm> createState() => _EnergyFormState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EnergyFormState extends State<_EnergyForm> {
|
||||||
|
EnergyType _selectedType = EnergyType.luce;
|
||||||
|
String? _selectedProviderId;
|
||||||
|
DateTime? _selectedExpiration;
|
||||||
|
|
||||||
|
Future<void> _pickDate() async {
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: DateTime.now().add(
|
||||||
|
const Duration(days: 365),
|
||||||
|
), // Default 1 anno
|
||||||
|
firstDate: DateTime.now(),
|
||||||
|
lastDate: DateTime.now().add(const Duration(days: 365 * 10)),
|
||||||
|
);
|
||||||
|
if (picked != null) {
|
||||||
|
setState(() => _selectedExpiration = picked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// 1. Tipo (Luce o Gas) - Segmented Button stile M3
|
||||||
|
SegmentedButton<EnergyType>(
|
||||||
|
segments: const [
|
||||||
|
ButtonSegment(
|
||||||
|
value: EnergyType.luce,
|
||||||
|
label: Text("Luce"),
|
||||||
|
icon: Icon(Icons.lightbulb_outline),
|
||||||
|
),
|
||||||
|
ButtonSegment(
|
||||||
|
value: EnergyType.gas,
|
||||||
|
label: Text("Gas"),
|
||||||
|
icon: Icon(Icons.local_fire_department),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
selected: {_selectedType},
|
||||||
|
onSelectionChanged: (Set<EnergyType> newSelection) {
|
||||||
|
setState(() => _selectedType = newSelection.first);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 2. Provider Dropdown
|
||||||
|
BlocBuilder<ProvidersCubit, ProvidersState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
return const Center(
|
||||||
|
child: LinearProgressIndicator(),
|
||||||
|
); // Mostra una barretta di caricamento
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.activeProviders.isEmpty) {
|
||||||
|
return const Text(
|
||||||
|
"Nessun gestore associato a questo negozio.",
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Filtra solo i provider di tipo Energia (Se hai una categoria nel modello)
|
||||||
|
// Se non hai una categoria nel ProviderModel, puoi rimuovere il .where
|
||||||
|
final energyProviders = state.activeProviders;
|
||||||
|
return DropdownButtonFormField<String>(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: "Gestore / Provider",
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
initialValue: _selectedProviderId,
|
||||||
|
items: energyProviders.map((p) {
|
||||||
|
return DropdownMenuItem(
|
||||||
|
value: p.id,
|
||||||
|
child: Text(p.nome ?? 'Sconosciuto'),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (val) => setState(() => _selectedProviderId = val),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 3. Scadenza (DatePicker integrato in un TextField)
|
||||||
|
TextFormField(
|
||||||
|
readOnly: true,
|
||||||
|
onTap: _pickDate,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: "Data Scadenza",
|
||||||
|
border: const OutlineInputBorder(),
|
||||||
|
suffixIcon: const Icon(Icons.calendar_month),
|
||||||
|
),
|
||||||
|
// Mostra la data se selezionata, altrimenti vuoto
|
||||||
|
controller: TextEditingController(
|
||||||
|
text: _selectedExpiration != null
|
||||||
|
? "${_selectedExpiration!.day}/${_selectedExpiration!.month}/${_selectedExpiration!.year}"
|
||||||
|
: "",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 4. Pulsanti Interni al Form
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: widget.onCancel,
|
||||||
|
child: const Text("Indietro"),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed:
|
||||||
|
(_selectedProviderId == null || _selectedExpiration == null)
|
||||||
|
? null // Disabilitato se mancano dati obbligatori
|
||||||
|
: () {
|
||||||
|
final newService = EnergyServiceModel(
|
||||||
|
type: _selectedType,
|
||||||
|
expiration: _selectedExpiration!,
|
||||||
|
providerId: _selectedProviderId!,
|
||||||
|
);
|
||||||
|
widget.onSave(newService);
|
||||||
|
},
|
||||||
|
child: const Text("Salva Contratto"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
158
lib/features/services/ui/service_form_screen/int_dialogs.dart
Normal file
158
lib/features/services/ui/service_form_screen/int_dialogs.dart
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
import 'dart:async'; // Necessario per il Timer
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Future<void> updateCountDialog(
|
||||||
|
BuildContext context,
|
||||||
|
String title,
|
||||||
|
int currentValue,
|
||||||
|
Function(int) onSave,
|
||||||
|
) async {
|
||||||
|
int tempValue =
|
||||||
|
currentValue; // Variabile locale per gestire il conteggio nella dialog
|
||||||
|
|
||||||
|
final result = await showDialog<int>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: Text("Imposta $title"),
|
||||||
|
content: QuickCounter(
|
||||||
|
initialValue: tempValue,
|
||||||
|
onChanged: (val) => tempValue =
|
||||||
|
val, // Aggiorna il valore locale quando il counter cambia
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text("Annulla"),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.pop(context, tempValue),
|
||||||
|
child: const Text("Conferma"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
onSave(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Widget Interno Specifico per il Counter Veloce ---
|
||||||
|
class QuickCounter extends StatefulWidget {
|
||||||
|
final int initialValue;
|
||||||
|
final ValueChanged<int>
|
||||||
|
onChanged; // Callback per notificare il padre dei cambiamenti
|
||||||
|
|
||||||
|
const QuickCounter({
|
||||||
|
super.key,
|
||||||
|
required this.initialValue,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<QuickCounter> createState() => _QuickCounterState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QuickCounterState extends State<QuickCounter> {
|
||||||
|
late int _value;
|
||||||
|
Timer? _longPressTimer; // Il timer per l'auto-incremento
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_value = widget.initialValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_longPressTimer
|
||||||
|
?.cancel(); // IMPORTANTE: Annulla sempre il timer alla distruzione
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logica comune per incremento/decremento singolo o rapido
|
||||||
|
void _update(int delta) {
|
||||||
|
setState(() {
|
||||||
|
_value += delta;
|
||||||
|
if (_value < 0) _value = 0; // Impedisci numeri negativi
|
||||||
|
});
|
||||||
|
widget.onChanged(_value); // Notifica il padre
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestione dell'inizio della pressione prolungata
|
||||||
|
void _startLongPress(int delta) {
|
||||||
|
_update(delta); // Esegui subito il primo aggiornamento al tocco iniziale
|
||||||
|
_longPressTimer = Timer.periodic(const Duration(milliseconds: 100), (
|
||||||
|
timer,
|
||||||
|
) {
|
||||||
|
_update(delta); // Aggiorna velocemente finché la pressione continua
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gestione della fine della pressione prolungata
|
||||||
|
void _stopLongPress() {
|
||||||
|
_longPressTimer?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final canDecrement = _value > 0;
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// --- Pulsante MENO ---
|
||||||
|
GestureDetector(
|
||||||
|
onLongPressStart: canDecrement ? (_) => _startLongPress(-1) : null,
|
||||||
|
onLongPressEnd: (_) => _stopLongPress(),
|
||||||
|
onLongPressCancel: () => _stopLongPress(),
|
||||||
|
onTap: canDecrement ? () => _update(-1) : null,
|
||||||
|
child: Opacity(
|
||||||
|
// Visivamente disabilitato se < 0
|
||||||
|
opacity: canDecrement ? 1.0 : 0.4,
|
||||||
|
child: const ActionButton(icon: Icons.remove, color: Colors.red),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- Valore Centrale ---
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
||||||
|
child: Text(
|
||||||
|
_value.toString(),
|
||||||
|
style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- Pulsante PIU' ---
|
||||||
|
GestureDetector(
|
||||||
|
onLongPressStart: (_) => _startLongPress(1),
|
||||||
|
onLongPressEnd: (_) => _stopLongPress(),
|
||||||
|
onLongPressCancel: () => _stopLongPress(),
|
||||||
|
onTap: () => _update(1),
|
||||||
|
child: const ActionButton(icon: Icons.add, color: Colors.green),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Piccolo widget di utilità per l'aspetto del pulsante
|
||||||
|
class ActionButton extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
const ActionButton({super.key, required this.icon, required this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: color, width: 2),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color, size: 30),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/features/services/blocs/services_cubit.dart';
|
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||||
|
import 'package:flux/features/services/ui/service_form_screen/customer_section.dart';
|
||||||
import 'package:flux/features/services/ui/service_form_screen/general_info_section.dart';
|
import 'package:flux/features/services/ui/service_form_screen/general_info_section.dart';
|
||||||
|
import 'package:flux/features/services/ui/service_form_screen/services_grid.dart';
|
||||||
|
|
||||||
class ServiceFormScreen extends StatelessWidget {
|
class ServiceFormScreen extends StatelessWidget {
|
||||||
const ServiceFormScreen({super.key});
|
const ServiceFormScreen({super.key});
|
||||||
@@ -30,7 +32,7 @@ class ServiceFormScreen extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// SEZIONE 1: CLIENTE
|
// SEZIONE 1: CLIENTE
|
||||||
const _CustomerSection(),
|
CustomerSection(service: service),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// SEZIONE 2: INFO GENERALI (Da fare)
|
// SEZIONE 2: INFO GENERALI (Da fare)
|
||||||
@@ -38,13 +40,7 @@ class ServiceFormScreen extends StatelessWidget {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// SEZIONE 3: I MODULI (Da fare)
|
// SEZIONE 3: I MODULI (Da fare)
|
||||||
Text(
|
ServicesGrid(service: service),
|
||||||
"Servizi e Accessori",
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
// const _ServicesGrid(),
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
// SEZIONE 4: ALLEGATI (Da fare)
|
// SEZIONE 4: ALLEGATI (Da fare)
|
||||||
@@ -90,88 +86,3 @@ class _SaveButton extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _CustomerSection extends StatelessWidget {
|
|
||||||
const _CustomerSection();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return BlocBuilder<ServicesCubit, ServicesState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
final service = state.currentService!;
|
|
||||||
final hasCustomer = service.customerId != null;
|
|
||||||
|
|
||||||
return Card(
|
|
||||||
elevation: 2,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.person,
|
|
||||||
color: Theme.of(context).colorScheme.primary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
"Dati Cliente",
|
|
||||||
style: Theme.of(context).textTheme.titleMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Se non c'è il cliente, mostriamo il tastone per cercarlo
|
|
||||||
if (!hasCustomer)
|
|
||||||
Center(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
// TODO: Aprire modale/dialog per ricerca clienti
|
|
||||||
print("Apro ricerca clienti...");
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.search),
|
|
||||||
label: const Text("Seleziona o Crea Cliente"),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 24,
|
|
||||||
vertical: 12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
// Se c'è, mostriamo chi è e diamo la possibilità di cambiarlo
|
|
||||||
else
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
service.customerDisplayName ?? "Cliente Selezionato",
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
// TODO: Aprire modale/dialog per ricerca clienti
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.edit, size: 18),
|
|
||||||
label: const Text("Cambia"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
168
lib/features/services/ui/service_form_screen/services_grid.dart
Normal file
168
lib/features/services/ui/service_form_screen/services_grid.dart
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||||
|
import 'package:flux/features/services/models/energy_service_model.dart';
|
||||||
|
import 'package:flux/features/services/models/service_model.dart';
|
||||||
|
import 'package:flux/features/services/ui/service_form_screen/action_card.dart';
|
||||||
|
import 'package:flux/features/services/ui/service_form_screen/energy_service_dialog.dart';
|
||||||
|
import 'package:flux/features/services/ui/service_form_screen/int_dialogs.dart'; // Assicurati di importare il modello
|
||||||
|
|
||||||
|
class ServicesGrid extends StatelessWidget {
|
||||||
|
final ServiceModel service;
|
||||||
|
|
||||||
|
const ServicesGrid({super.key, required this.service});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.layers_outlined,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
"Servizi e Accessori",
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 16,
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
children: [
|
||||||
|
// --- CONTATORI SEMPLICI ---
|
||||||
|
ActionCard(
|
||||||
|
label: "AL",
|
||||||
|
count: service.al,
|
||||||
|
icon: Icons.sim_card,
|
||||||
|
color: Colors.blue,
|
||||||
|
onTap: () => updateCountDialog(
|
||||||
|
context,
|
||||||
|
"AL",
|
||||||
|
service.al,
|
||||||
|
(val) =>
|
||||||
|
context.read<ServicesCubit>().updateField(al: val),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ActionCard(
|
||||||
|
label: "MNP",
|
||||||
|
count: service.mnp,
|
||||||
|
icon: Icons.phone_android,
|
||||||
|
color: Colors.indigo,
|
||||||
|
onTap: () => updateCountDialog(
|
||||||
|
context,
|
||||||
|
"MNP",
|
||||||
|
service.mnp,
|
||||||
|
(val) =>
|
||||||
|
context.read<ServicesCubit>().updateField(mnp: val),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ActionCard(
|
||||||
|
label: "NIP",
|
||||||
|
count: service.nip,
|
||||||
|
icon: Icons.compare_arrows,
|
||||||
|
color: Colors.cyan,
|
||||||
|
onTap: () => updateCountDialog(
|
||||||
|
context,
|
||||||
|
"NIP",
|
||||||
|
service.nip,
|
||||||
|
(val) =>
|
||||||
|
context.read<ServicesCubit>().updateField(nip: val),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ActionCard(
|
||||||
|
label: "Unica",
|
||||||
|
count: service.unica,
|
||||||
|
icon: Icons.all_inclusive,
|
||||||
|
color: Colors.purple,
|
||||||
|
onTap: () => updateCountDialog(
|
||||||
|
context,
|
||||||
|
"Unica",
|
||||||
|
service.unica,
|
||||||
|
(val) =>
|
||||||
|
context.read<ServicesCubit>().updateField(unica: val),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ActionCard(
|
||||||
|
label: "Telepass",
|
||||||
|
count: service.telepass,
|
||||||
|
icon: Icons.directions_car,
|
||||||
|
color: Colors.amber.shade700,
|
||||||
|
onTap: () => updateCountDialog(
|
||||||
|
context,
|
||||||
|
"Telepass",
|
||||||
|
service.telepass,
|
||||||
|
(val) => context.read<ServicesCubit>().updateField(
|
||||||
|
telepass: val,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// --- MODULI COMPLESSI (Le liste) ---
|
||||||
|
ActionCard(
|
||||||
|
label: "Energia",
|
||||||
|
count: service.energyServices.length,
|
||||||
|
icon: Icons.bolt,
|
||||||
|
color: Colors.green,
|
||||||
|
onTap: () async {
|
||||||
|
// Apriamo la modale e aspettiamo il risultato
|
||||||
|
final result = await showDialog<List<EnergyServiceModel>>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => EnergyServiceDialog(
|
||||||
|
currentStoreId: service.storeId,
|
||||||
|
initialServices: service
|
||||||
|
.energyServices, // Passiamo la lista attuale
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Se l'utente ha premuto "Conferma" e non "Annulla" o tap fuori
|
||||||
|
if (result != null && context.mounted) {
|
||||||
|
context.read<ServicesCubit>().updateEnergyServices(
|
||||||
|
result,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ActionCard(
|
||||||
|
label: "Finanziam.",
|
||||||
|
count: service.finServices.length,
|
||||||
|
icon: Icons.euro_symbol,
|
||||||
|
color: Colors.teal,
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Aprire la Dialog Finanziamenti complessa
|
||||||
|
print("Apri Fin Dialog");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ActionCard(
|
||||||
|
label: "Contenuti",
|
||||||
|
count: service.entertainmentServices.length,
|
||||||
|
icon: Icons.tv,
|
||||||
|
color: Colors.redAccent,
|
||||||
|
onTap: () {
|
||||||
|
// TODO: Aprire la Dialog Contenuti complessa
|
||||||
|
print("Apri Contenuti Dialog");
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user