maremma maiala impestata, buonissima base dopo ultra refactor

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-02 12:19:04 +02:00
parent 1721b2ff89
commit 67e8b8b654
23 changed files with 706 additions and 2730 deletions

View File

@@ -12,7 +12,6 @@ import 'package:flux/features/customers/models/customer_model.dart';
import 'package:flux/features/customers/ui/customer_detail_screen.dart';
import 'package:flux/features/customers/ui/customer_mobile_upload_screen.dart';
import 'package:flux/features/customers/ui/customers_content.dart';
import 'package:flux/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart';
import 'package:flux/features/home/ui/home_screen.dart';
import 'package:flux/features/master_data/master_data_hub_content.dart';
import 'package:flux/features/master_data/products/ui/products_screen.dart';
@@ -23,8 +22,8 @@ import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
import 'package:flux/features/onboarding/ui/onboarding_screen.dart';
import 'package:flux/features/operations/blocs/operation_files_bloc.dart';
import 'package:flux/features/operations/models/operation_model.dart';
import 'package:flux/features/operations/ui/operation_form_screen/operation_form_screen.dart';
import 'package:flux/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart';
import 'package:flux/features/operations/ui/operation_form_screen.dart';
import 'package:flux/features/operations/ui/operation_mobile_upload_screen.dart';
import 'package:flux/features/operations/ui/operations_screen.dart';
import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart';

View File

@@ -6,31 +6,31 @@ import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/features/customers/models/customer_model.dart';
import 'package:get_it/get_it.dart';
part 'customer_state.dart';
part 'customers_state.dart';
class CustomerCubit extends Cubit<CustomerState> {
class CustomersCubit extends Cubit<CustomersState> {
final CustomerRepository _repository = GetIt.I<CustomerRepository>();
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
// Variabile per gestire il debounce della ricerca
Timer? _searchDebounce;
CustomerCubit() : super(const CustomerState());
CustomersCubit() : super(const CustomersState());
// --- LETTURA ---
Future<void> loadCustomers() async {
emit(state.copyWith(status: CustomerStatus.loading));
emit(state.copyWith(status: CustomersStatus.loading));
try {
final customers = await _repository.getCustomers(
_sessionCubit.state.company!.id!,
);
emit(
state.copyWith(status: CustomerStatus.success, customers: customers),
state.copyWith(status: CustomersStatus.success, customers: customers),
);
} catch (e) {
emit(
state.copyWith(
status: CustomerStatus.failure,
status: CustomersStatus.failure,
errorMessage: e.toString(),
),
);
@@ -39,7 +39,7 @@ class CustomerCubit extends Cubit<CustomerState> {
// --- CREAZIONE ---
Future<void> createCustomer(CustomerModel customer) async {
emit(state.copyWith(status: CustomerStatus.loading));
emit(state.copyWith(status: CustomersStatus.loading));
try {
final newCustomer = await _repository.saveCustomer(customer);
@@ -49,7 +49,7 @@ class CustomerCubit extends Cubit<CustomerState> {
emit(
state.copyWith(
status: CustomerStatus.success,
status: CustomersStatus.success,
customers: updatedList,
lastCreatedCustomer: newCustomer,
),
@@ -57,7 +57,7 @@ class CustomerCubit extends Cubit<CustomerState> {
} catch (e) {
emit(
state.copyWith(
status: CustomerStatus.failure,
status: CustomersStatus.failure,
errorMessage: e.toString(),
),
);
@@ -66,7 +66,7 @@ class CustomerCubit extends Cubit<CustomerState> {
// --- AGGIORNAMENTO ---
Future<void> updateCustomer(CustomerModel customer) async {
emit(state.copyWith(status: CustomerStatus.loading));
emit(state.copyWith(status: CustomersStatus.loading));
try {
final updatedCustomer = await _repository.updateCustomer(customer);
@@ -79,7 +79,7 @@ class CustomerCubit extends Cubit<CustomerState> {
emit(
state.copyWith(
status: CustomerStatus.success,
status: CustomersStatus.success,
customers: updatedList,
lastCreatedCustomer:
updatedCustomer, // Utile se modifichi un cliente appena creato
@@ -88,7 +88,7 @@ class CustomerCubit extends Cubit<CustomerState> {
} catch (e) {
emit(
state.copyWith(
status: CustomerStatus.failure,
status: CustomersStatus.failure,
errorMessage: e.toString(),
),
);
@@ -115,12 +115,12 @@ class CustomerCubit extends Cubit<CustomerState> {
query,
);
emit(
state.copyWith(status: CustomerStatus.success, customers: results),
state.copyWith(status: CustomersStatus.success, customers: results),
);
} catch (e) {
emit(
state.copyWith(
status: CustomerStatus.failure,
status: CustomersStatus.failure,
errorMessage: e.toString(),
),
);

View File

@@ -1,6 +1,6 @@
part of 'customer_cubit.dart';
part of 'customers_cubit.dart';
enum CustomerStatus {
enum CustomersStatus {
initial,
loading,
filesLoading,
@@ -9,26 +9,26 @@ enum CustomerStatus {
failure,
}
class CustomerState extends Equatable {
final CustomerStatus status;
class CustomersState extends Equatable {
final CustomersStatus status;
final List<CustomerModel> customers;
final CustomerModel? lastCreatedCustomer;
final String? errorMessage;
const CustomerState({
this.status = CustomerStatus.initial,
const CustomersState({
this.status = CustomersStatus.initial,
this.customers = const [],
this.lastCreatedCustomer,
this.errorMessage,
});
CustomerState copyWith({
CustomerStatus? status,
CustomersState copyWith({
CustomersStatus? status,
List<CustomerModel>? customers,
CustomerModel? lastCreatedCustomer,
String? errorMessage,
}) {
return CustomerState(
return CustomersState(
status: status ?? this.status,
customers: customers ?? this.customers,
lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer,

View File

@@ -1,202 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/customers/blocs/customer_cubit.dart';
import 'package:flux/features/customers/models/customer_model.dart';
import 'package:flux/features/customers/ui/quick_customer_dialog.dart';
import 'package:flux/features/operations/blocs/operations_cubit.dart';
class CustomerSearchSheet extends StatefulWidget {
const CustomerSearchSheet({super.key});
@override
State<CustomerSearchSheet> createState() => _CustomerSearchSheetState();
}
class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
final TextEditingController _searchController = TextEditingController();
@override
void initState() {
super.initState();
context.read<CustomerCubit>().loadCustomers();
}
@override
void dispose() {
_searchController.dispose();
super.dispose();
}
void _onSearchChanged(String query) {
context.read<CustomerCubit>().searchCustomers(query);
}
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: BoxConstraints(
maxHeight: MediaQuery.of(context).size.height * 0.85,
),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- HEADER ---
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Trova Cliente",
style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
tooltip: "Chiudi",
),
],
),
const SizedBox(height: 16),
// --- BARRA DI RICERCA ---
TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: "Cerca per nome, cognome o CF...",
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
fillColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3),
suffixIcon: IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
_onSearchChanged("");
},
),
),
onChanged: _onSearchChanged,
),
const SizedBox(height: 16),
// --- TASTO NUOVO CLIENTE ---
SizedBox(
width: double.infinity,
child: IconButton(
icon: const Icon(Icons.person_add),
onPressed: () async {
final operationsCubit = context.read<OperationsCubit>();
// Apriamo la dialog passando la query attuale
final CustomerModel? nuovoCliente = await showDialog(
context: context,
builder: (context) => QuickCustomerDialog(
initialQuery: _searchController.text,
),
);
if (nuovoCliente != null) {
operationsCubit.updateField(
customerId: nuovoCliente.id,
customerDisplayName: nuovoCliente.name,
);
setState(() {
_searchController.clear();
});
}
},
),
),
const SizedBox(height: 24),
// --- LISTA RISULTATI CON BLOC BUILDER ---
const Text(
"Risultati",
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey),
),
const SizedBox(height: 8),
Expanded(
// AGGANCIO AL CUBIT REALE
child: BlocBuilder<CustomerCubit, CustomerState>(
builder: (context, state) {
// 1. Stato di caricamento
if (state.status == CustomerStatus.loading) {
return const Center(child: CircularProgressIndicator());
}
// 2. Nessun risultato trovato
if (state.customers.isEmpty) {
return const Center(
child: Text(
"Nessun cliente trovato.\nProva a cambiare i termini di ricerca.",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
);
}
// 3. Mostriamo la lista vera
return ListView.separated(
itemCount: state.customers.length,
separatorBuilder: (context, index) =>
const Divider(height: 1),
itemBuilder: (context, index) {
final customer = state.customers[index];
// Assumo che il tuo CustomerModel abbia le proprietà name e surname.
// Adatta queste variabili al tuo modello reale!
final displayName = customer.name.trim();
return ListTile(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
backgroundColor: Theme.of(
context,
).colorScheme.primaryContainer,
foregroundColor: Theme.of(
context,
).colorScheme.onPrimaryContainer,
// Mostra l'iniziale
child: Text(
displayName.isNotEmpty
? displayName[0].toUpperCase()
: "?",
),
),
title: Text(
displayName,
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(customer.email),
trailing: const Icon(
Icons.check_circle_outline,
color: Colors.grey,
),
onTap: () {
// Salviamo l'ID e il nome formattato nel form dei servizi
context.read<OperationsCubit>().updateField(
customerId: customer.id,
customerDisplayName: displayName,
);
// Chiudiamo la modale
Navigator.pop(context);
},
);
},
);
},
),
),
],
),
),
);
}
}

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/customers/blocs/customer_cubit.dart';
import 'package:flux/features/customers/blocs/customers_cubit.dart';
import 'package:flux/features/customers/models/customer_model.dart';
import 'package:flux/features/customers/ui/customer_form.dart';
import 'package:go_router/go_router.dart';
@@ -26,14 +26,14 @@ class _CustomersContentState extends State<CustomersContent> {
void _loadInitialCustomers() {
final companyId = context.read<SessionCubit>().state.company?.id;
if (companyId != null) {
context.read<CustomerCubit>().loadCustomers();
context.read<CustomersCubit>().loadCustomers();
}
}
void _onSearch(String query) {
final companyId = context.read<SessionCubit>().state.company?.id;
if (companyId != null) {
context.read<CustomerCubit>().searchCustomers(query);
context.read<CustomersCubit>().searchCustomers(query);
}
}
@@ -86,9 +86,9 @@ class _CustomersContentState extends State<CustomersContent> {
// LISTA CLIENTI
Expanded(
child: BlocBuilder<CustomerCubit, CustomerState>(
child: BlocBuilder<CustomersCubit, CustomersState>(
builder: (context, state) {
if (state.status == CustomerStatus.loading &&
if (state.status == CustomersStatus.loading &&
state.customers.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
@@ -242,12 +242,12 @@ void openCustomerForm({
if (customer == null) {
// CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create
context.read<CustomerCubit>().createCustomer(
context.read<CustomersCubit>().createCustomer(
customerFromForm.copyWith(companyId: companyId),
);
} else {
// CASO MODIFICA: L'ID e il companyId sono già nel modello
context.read<CustomerCubit>().updateCustomer(customerFromForm);
context.read<CustomersCubit>().updateCustomer(customerFromForm);
}
Navigator.pop(dialogContext);
},

View File

@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/customers/blocs/customer_cubit.dart';
import 'package:flux/features/customers/blocs/customers_cubit.dart';
class QuickCustomerDialog extends StatefulWidget {
final String initialQuery;
@@ -42,7 +42,9 @@ class _QuickCustomerDialogState extends State<QuickCustomerDialog> {
setState(() => _isLoading = true);
// Chiamata al Cubit (aggiorna i parametri in base a come li hai definiti)
final newCustomer = await context.read<CustomerCubit>().quickCreateCustomer(
final newCustomer = await context
.read<CustomersCubit>()
.quickCreateCustomer(
name: _nameCtrl.text.trim(),
phone: _phoneCtrl.text.trim(),
// Aggiungi questi se li hai inseriti nel tuo CustomerCubit:

View File

@@ -210,12 +210,40 @@ class OperationsCubit extends Cubit<OperationsState> {
.toList();
}
void updateField({String? customerId, String? customerDisplayName}) {
// --- GESTIONE DELLO STATO DEL FORM IN TEMPO REALE ---
void updateOperationFields({
String? customerId,
String? customerDisplayName,
String? type,
String? providerId,
String? subtype,
DateTime? expirationDate,
int? quantity,
// Aggiungiamo questi flag per forzare la pulizia dei campi quando cambi tipo
bool clearProvider = false,
bool clearType = false,
bool clearSubtype = false,
bool clearExpiration = false,
}) {
if (state.currentOperation == null) return;
final updated = state.currentOperation!.copyWith(
final current = state.currentOperation!;
// Creiamo il modello aggiornato
// ATTENZIONE: adatta questa logica in base a come è scritto il tuo copyWith!
final updated = current.copyWith(
customerId: customerId,
customerDisplayName: customerDisplayName,
type: clearType ? null : type,
subtype: clearSubtype ? null : subtype,
expirationDate: clearExpiration ? null : expirationDate,
// Se clearProvider è true, forziamo una stringa vuota (o null se il tuo modello lo supporta)
providerId: clearProvider ? null : (providerId ?? current.providerId),
// Idem per subtype e date.
// Se expirationDate è nullabile nel copyWith, dovresti poterlo gestire
quantity: quantity ?? current.quantity,
);
emit(state.copyWith(currentOperation: updated));
}
}

View File

@@ -1,72 +0,0 @@
import 'package:equatable/equatable.dart';
enum EnergyType { luce, gas } // Mappa il tuo public.energy_type
class EnergyOperationModel extends Equatable {
final String? id;
final DateTime? createdAt;
final EnergyType type;
final DateTime expiration;
final String providerId;
final String? operationId;
const EnergyOperationModel({
this.id,
this.createdAt,
required this.type,
required this.expiration,
required this.providerId,
this.operationId,
});
EnergyOperationModel copyWith({
String? id,
DateTime? createdAt,
EnergyType? type,
DateTime? expiration,
String? providerId,
String? operationId,
}) {
return EnergyOperationModel(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
type: type ?? this.type,
expiration: expiration ?? this.expiration,
providerId: providerId ?? this.providerId,
operationId: operationId ?? this.operationId,
);
}
@override
List<Object?> get props => [
id,
createdAt,
type,
expiration,
providerId,
operationId,
];
factory EnergyOperationModel.fromMap(Map<String, dynamic> map) {
return EnergyOperationModel(
id: map['id'],
createdAt: map['created_at'] != null
? DateTime.parse(map['created_at'])
: null,
type: map['type'] == 'gas' ? EnergyType.gas : EnergyType.luce,
expiration: DateTime.parse(map['expiration']),
providerId: map['provider_id'],
operationId: map['operation_id'],
);
}
Map<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
'type': type.name, // .name trasforma l'enum in 'luce' o 'gas'
'expiration': expiration.toIso8601String(),
'provider_id': providerId,
'operation_id': operationId,
};
}
}

View File

@@ -1,77 +0,0 @@
import 'package:equatable/equatable.dart';
class EntertainmentOperationModel extends Equatable {
final String? id;
final DateTime? createdAt;
final String type; // es. Sky, DAZN, ecc.
final bool constrained; // Vincolato?
final DateTime constrainExpiration;
final String? operationId;
final String? providerId;
const EntertainmentOperationModel({
this.id,
this.createdAt,
required this.type,
required this.constrained,
required this.constrainExpiration,
this.operationId,
this.providerId,
});
EntertainmentOperationModel copyWith({
String? id,
DateTime? createdAt,
String? type,
bool? constrained,
DateTime? constrainExpiration,
String? operationId,
String? providerId,
}) {
return EntertainmentOperationModel(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
type: type ?? this.type,
constrained: constrained ?? this.constrained,
constrainExpiration: constrainExpiration ?? this.constrainExpiration,
operationId: operationId ?? this.operationId,
providerId: providerId ?? this.providerId,
);
}
@override
List<Object?> get props => [
id,
createdAt,
type,
constrained,
constrainExpiration,
operationId,
providerId,
];
factory EntertainmentOperationModel.fromMap(Map<String, dynamic> map) {
return EntertainmentOperationModel(
id: map['id'],
createdAt: map['created_at'] != null
? DateTime.parse(map['created_at'])
: null,
type: map['type'],
constrained: map['constrained'] ?? false,
constrainExpiration: DateTime.parse(map['constrain_expiration']),
operationId: map['operation_id'],
providerId: map['provider_id'],
);
}
Map<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
'type': type,
'constrained': constrained,
'constrain_expiration': constrainExpiration.toIso8601String(),
'operation_id': operationId,
'provider_id': providerId,
};
}
}

View File

@@ -1,63 +0,0 @@
import 'package:equatable/equatable.dart';
class FinOperationModel extends Equatable {
final String? id;
final DateTime? createdAt;
final DateTime expiration;
final String? operationId;
final String? modelId; // FK verso model (es. iPhone, Samsung, ecc.)
final String? providerId;
const FinOperationModel({
this.id,
this.createdAt,
required this.expiration,
this.operationId,
this.modelId,
this.providerId,
});
FinOperationModel copyWith({
String? id,
DateTime? createdAt,
DateTime? expiration,
String? operationId,
String? modelId,
String? providerId,
}) {
return FinOperationModel(
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
expiration: expiration ?? this.expiration,
operationId: operationId ?? this.operationId,
modelId: modelId ?? this.modelId,
providerId: providerId ?? this.providerId,
);
}
@override
List<Object?> get props => [id, createdAt, expiration, operationId, modelId];
factory FinOperationModel.fromMap(Map<String, dynamic> map) {
return FinOperationModel(
id: map['id'],
createdAt: map['created_at'] != null
? DateTime.parse(map['created_at'])
: null,
expiration: DateTime.parse(map['expiration']),
operationId: map['operation_id'],
modelId: map['model_id'],
providerId: map['provider_id'],
);
}
Map<String, dynamic> toMap() {
return {
if (id != null) 'id': id,
'expiration': expiration.toIso8601String(),
'operation_id': operationId,
'model_id': modelId,
'provider_id': providerId,
};
}
}

View File

@@ -27,6 +27,7 @@ class OperationModel extends Equatable {
final String? id;
final DateTime? createdAt;
final String type;
final String? subType;
final String? providerId;
final String? providerDisplayName;
final String? modelId;
@@ -55,6 +56,7 @@ class OperationModel extends Equatable {
this.id,
this.createdAt,
this.type = '',
this.subType,
this.providerId,
this.providerDisplayName,
this.modelId,
@@ -82,6 +84,7 @@ class OperationModel extends Equatable {
String? id,
DateTime? createdAt,
String? type,
String? subtype,
String? providerId,
String? providerDisplayName,
String? modelId,
@@ -107,6 +110,7 @@ class OperationModel extends Equatable {
id: id ?? this.id,
createdAt: createdAt ?? this.createdAt,
type: type ?? this.type,
subType: subtype ?? this.subType,
providerId: providerId ?? this.providerId,
providerDisplayName: providerDisplayName ?? this.providerDisplayName,
modelId: modelId ?? this.modelId,
@@ -135,6 +139,7 @@ class OperationModel extends Equatable {
id,
createdAt,
type,
subType,
providerId,
providerDisplayName,
modelId,
@@ -169,6 +174,7 @@ class OperationModel extends Equatable {
? DateTime.parse(map['created_at'])
: null,
type: map['type'] as String? ?? '',
subType: map['sub_type'] as String?,
providerId: map['provider_id'] as String? ?? '',
providerDisplayName: "${map['provider']['name']}".myFormat(),
modelId: map['model_id'] as String? ?? '',
@@ -206,6 +212,7 @@ class OperationModel extends Equatable {
return {
if (id != null) 'id': id,
'type': type,
'sub_type': subType,
'provider_id': providerId,
'model_id': modelId,
'description': description,

View File

@@ -0,0 +1,624 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/customers/blocs/customers_cubit.dart';
import 'package:flux/features/operations/blocs/operations_cubit.dart';
import 'package:flux/features/operations/models/operation_model.dart';
// import 'package:flux/features/attachments/ui/operation_files_section.dart';
class OperationFormScreen extends StatefulWidget {
final String? operationId;
final OperationModel? existingOperation;
const OperationFormScreen({
super.key,
this.operationId,
this.existingOperation,
});
@override
State<OperationFormScreen> createState() => _OperationFormScreenState();
}
class _OperationFormScreenState extends State<OperationFormScreen> {
final _formKey = GlobalKey<FormState>();
// TEXT CONTROLLERS (Unici detentori di stato locale per evitare lag)
final _referenceController = TextEditingController();
final _noteController = TextEditingController();
final _customSubtypeController = TextEditingController();
final List<String> _availableTypes = [
'AL',
'MNP',
'NIP',
'UNICA',
'TELEPASS',
'Energy',
'Fin',
'Entertainment',
'Custom',
];
bool _isInitialized = false;
@override
void initState() {
super.initState();
// Inizializziamo il form nel Cubit
context.read<OperationsCubit>().initOperationForm(
existingOperation: widget.existingOperation,
operationId: widget.operationId,
);
}
@override
void dispose() {
_referenceController.dispose();
_noteController.dispose();
_customSubtypeController.dispose();
super.dispose();
}
// Sincronizza SOLO i testi liberi quando il Cubit ha caricato da DB
void _syncTextControllers(OperationModel model) {
if (_referenceController.text.isEmpty && model.reference.isNotEmpty) {
_referenceController.text = model.reference;
}
if (_noteController.text.isEmpty && model.note.isNotEmpty) {
_noteController.text = model.note;
}
_isInitialized = true;
}
// --- LOGICA DI SALVATAGGIO ---
void _saveOperation({required bool keepAdding}) {
if (_formKey.currentState!.validate()) {
final cubit = context.read<OperationsCubit>();
final currentOperation = cubit.state.currentOperation!;
// 1. "Travasiamo" i testi liberi dai controller al Modello prima di salvare
final operationToSave = currentOperation.copyWith(
reference: _referenceController.text,
note: _noteController.text,
// subtype: currentOperation.type == 'Custom' ? _customSubtypeController.text : currentOperation.subtype, // <-- Scommenta quando aggiungi subtype
);
// 2. Aggiorniamo il Cubit con i testi
cubit.initOperationForm(existingOperation: operationToSave);
// 3. Salviamo!
cubit.saveCurrentOperation(
targetStatus: OperationStatus.ok,
shouldPop: !keepAdding,
);
}
}
// --- MODALE SELEZIONE CLIENTE ---
void _showCustomerModal() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (modalContext) {
return DraggableScrollableSheet(
initialChildSize: 0.8,
minChildSize: 0.5,
maxChildSize: 0.95,
expand: false,
builder: (_, scrollController) {
return Column(
children: [
// Header
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Seleziona Cliente',
style: Theme.of(context).textTheme.titleLarge,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(modalContext),
),
],
),
),
// Barra di Ricerca
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: TextField(
decoration: InputDecoration(
hintText: 'Cerca per nome, telefono o email...',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
),
onChanged: (query) {
// Evento di ricerca (usa debouncer nel cubit!)
// context.read<CustomersCubit>().searchCustomers(query);
},
),
),
// Pulsante Nuovo Cliente
Padding(
padding: const EdgeInsets.all(16.0),
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
icon: const Icon(Icons.person_add),
label: const Text('Crea Nuovo Cliente'),
onPressed: () {
// Apri form nuovo cliente...
},
),
),
const Divider(),
// Lista Clienti dal Bloc
Expanded(
child: BlocBuilder<CustomersCubit, CustomersState>(
builder: (context, state) {
/* Decommenta e adatta al tuo CustomersState
if (state.status == CustomersStatus.loading) {
return const Center(child: CircularProgressIndicator());
}
if (state.customers.isEmpty) {
return const Center(child: Text('Nessun cliente trovato.', style: TextStyle(color: Colors.grey)));
}
*/
return ListView.builder(
controller: scrollController,
itemCount: 10, // Sostituisci con state.customers.length
itemBuilder: (context, index) {
// final customer = state.customers[index];
return ListTile(
leading: const CircleAvatar(
child: Icon(Icons.person),
),
title: Text(
'Cliente $index',
style: const TextStyle(
fontWeight: FontWeight.bold,
),
), // Sostituisci con customer.name
subtitle: const Text(
'333 1234567',
), // Sostituisci con customer.phoneNumber
onTap: () {
// Aggiorniamo il form tramite il Cubit delle operazioni
context
.read<OperationsCubit>()
.updateOperationFields(
customerId:
'id_del_cliente_$index', // customer.id
customerDisplayName:
'Cliente $index', // customer.name
);
Navigator.pop(modalContext);
},
);
},
);
},
),
),
],
);
},
);
},
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocConsumer<OperationsCubit, OperationsState>(
listenWhen: (previous, current) =>
previous.status != current.status ||
previous.currentOperation?.id != current.currentOperation?.id,
listener: (context, state) {
// Sincronizzazione iniziale
if (state.status == OperationsStatus.ready &&
state.currentOperation != null &&
!_isInitialized) {
_syncTextControllers(state.currentOperation!);
}
if (state.status == OperationsStatus.saved) {
Navigator.of(context).pop();
} else if (state.status == OperationsStatus.savedNoPop) {
context.read<OperationsCubit>().prepareNextOperationInBatch();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Servizio aggiunto! Inserisci il prossimo.'),
),
);
// Ripuliamo SOLO i testi liberi (il Cubit gestisce già i suoi reset)
_referenceController.clear();
_noteController.clear();
_customSubtypeController.clear();
} else if (state.status == OperationsStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? 'Errore'),
backgroundColor: theme.colorScheme.error,
),
);
}
},
builder: (context, state) {
// Loader iniziale
if (!_isInitialized &&
(widget.operationId != null || widget.existingOperation != null) &&
state.status == OperationsStatus.loading) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
}
return Scaffold(
appBar: AppBar(
title: Text(
state.currentOperation?.id == null
? 'Nuova Pratica'
: 'Modifica Pratica',
),
),
body: Form(
key: _formKey,
child: LayoutBuilder(
builder: (context, constraints) {
final isDesktop = constraints.maxWidth > 900;
if (isDesktop) {
// --- LAYOUT DESKTOP ---
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 7,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: _buildMainFormContent(theme, state),
),
),
VerticalDivider(width: 1, color: theme.dividerColor),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: _buildNotesSection(isDesktop: true),
),
),
],
);
} else {
// --- LAYOUT MOBILE ---
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMainFormContent(theme, state),
const Divider(height: 32),
_buildNotesSection(isDesktop: false),
const SizedBox(height: 80),
],
),
);
}
},
),
),
// --- LA CASSA ---
bottomNavigationBar: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Expanded(
flex: 1,
child: OutlinedButton(
onPressed: state.status == OperationsStatus.saving
? null
: () => _saveOperation(keepAdding: true),
child: const Text(
'Salva e Aggiungi Altro',
textAlign: TextAlign.center,
),
),
),
const SizedBox(width: 12),
Expanded(
flex: 1,
child: ElevatedButton(
onPressed: state.status == OperationsStatus.saving
? null
: () => _saveOperation(keepAdding: false),
child: state.status == OperationsStatus.saving
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
)
: const Text('Salva ed Esci'),
),
),
],
),
),
),
);
},
);
}
// --- COSTRUTTORI UI COMPONENTI ---
Widget _buildMainFormContent(ThemeData theme, OperationsState state) {
final currentOp = state.currentOperation;
final currentType = currentOp?.type ?? 'AL';
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- BLOCCO 1: CONTESTO ---
_buildSectionTitle('Cliente & Riferimento'),
_buildCustomerSelector(currentOp),
const SizedBox(height: 16),
TextFormField(
controller: _referenceController,
decoration: const InputDecoration(
labelText: 'Riferimento (es. numero di telefono, targa...)',
prefixIcon: Icon(Icons.tag),
),
),
const Divider(height: 32),
// --- BLOCCO 2: TIPO DI OPERAZIONE ---
_buildSectionTitle('Cosa stiamo facendo?'),
Wrap(
spacing: 8.0,
runSpacing: 8.0,
children: _availableTypes.map((type) {
return ChoiceChip(
label: Text(type),
selected: currentType == type,
onSelected: (selected) {
if (selected) {
// Diciamo al Cubit di cambiare tipo e spianare i campi dipendenti
context.read<OperationsCubit>().updateOperationFields(
type: type,
clearProvider: true,
clearSubtype: true,
clearExpiration: true,
);
}
},
);
}).toList(),
),
const Divider(height: 32),
// --- BLOCCO 3: DETTAGLI REATTIVI ---
_buildSectionTitle('Dettagli Servizio'),
// PROVIDER (Mostrato quasi sempre)
ListTile(
title: const Text('Seleziona Gestore'),
subtitle: Text(
currentOp?.providerId ?? 'Nessun gestore selezionato',
), // Adatta se hai displayName
trailing: const Icon(Icons.arrow_drop_down),
shape: RoundedRectangleBorder(
side: BorderSide(color: theme.dividerColor),
borderRadius: BorderRadius.circular(8),
),
onTap: () {
// TODO: Modale o Dropdown Provider
},
),
const SizedBox(height: 16),
// SOTTO-TIPO (Reattivo)
if (['Energy', 'Fin', 'Entertainment'].contains(currentType)) ...[
DropdownButtonFormField<String>(
value:
null, // Sostituisci con currentOp?.subtype quando lo aggiungi
decoration: const InputDecoration(
labelText: 'Dettaglio (es. Luce, Gas...)',
),
items: [
'Luce',
'Gas',
'Dual',
].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
onChanged: (val) {
// context.read<OperationsCubit>().updateOperationFields(subtype: val);
},
),
const SizedBox(height: 16),
],
// SOTTO-TIPO CUSTOM (Reattivo)
if (currentType == 'Custom') ...[
TextFormField(
controller: _customSubtypeController,
decoration: const InputDecoration(
labelText: 'Specifica il servizio (es. Monopattino)',
),
),
const SizedBox(height: 16),
],
// SCADENZA (Reattivo)
if ([
'Energy',
'Fin',
'Entertainment',
'Custom',
].contains(currentType)) ...[
ListTile(
title: const Text('Data di Scadenza'),
subtitle: Text(
currentOp?.expirationDate?.toLocal().toString().split(' ')[0] ??
'Nessuna scadenza',
),
trailing: const Icon(Icons.calendar_today),
shape: RoundedRectangleBorder(
side: BorderSide(color: theme.dividerColor),
borderRadius: BorderRadius.circular(8),
),
onTap: () async {
final date = await showDatePicker(
context: context,
initialDate: DateTime.now().add(const Duration(days: 365)),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 3650)),
);
if (date != null) {
context.read<OperationsCubit>().updateOperationFields(
expirationDate: date,
);
}
},
),
const SizedBox(height: 16),
],
// QUANTITÀ
Row(
children: [
const Text('Quantità: '),
IconButton(
icon: const Icon(Icons.remove),
onPressed: () {
final q = currentOp?.quantity ?? 1;
if (q > 1)
context.read<OperationsCubit>().updateOperationFields(
quantity: q - 1,
);
},
),
Text(
'${currentOp?.quantity ?? 1}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
final q = currentOp?.quantity ?? 1;
context.read<OperationsCubit>().updateOperationFields(
quantity: q + 1,
);
},
),
],
),
const Divider(height: 32),
// --- BLOCCO 5: ALLEGATI ---
_buildSectionTitle('Documenti & Foto'),
const Center(
child: Text(
"Widget File in arrivo...",
style: TextStyle(color: Colors.grey),
),
),
],
);
}
Widget _buildNotesSection({required bool isDesktop}) {
final title = _buildSectionTitle('Note Interne');
final noteField = TextFormField(
controller: _noteController,
keyboardType: TextInputType.multiline,
minLines: isDesktop ? null : 5,
maxLines: null,
expands: isDesktop,
textAlignVertical: TextAlignVertical.top,
decoration: const InputDecoration(
hintText: 'Incolla qui seriali, ICCID, IBAN, indirizzi...',
alignLabelWithHint: true,
border: OutlineInputBorder(),
),
);
if (isDesktop) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title,
const SizedBox(height: 8),
Expanded(child: noteField),
],
);
} else {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [title, const SizedBox(height: 8), noteField],
);
}
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
title,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
);
}
Widget _buildCustomerSelector(OperationModel? currentOp) {
final hasCustomer =
currentOp?.customerId != null && currentOp!.customerId!.isNotEmpty;
return InkWell(
onTap: _showCustomerModal,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).colorScheme.primary),
borderRadius: BorderRadius.circular(8),
color: Theme.of(
context,
).colorScheme.primaryContainer.withValues(alpha: 0.2),
),
child: Row(
children: [
const Icon(Icons.person),
const SizedBox(width: 12),
Expanded(
child: Text(
hasCustomer
? currentOp.customerDisplayName ?? ''
: 'Seleziona Cliente *',
style: TextStyle(
fontWeight: hasCustomer ? FontWeight.bold : FontWeight.normal,
color: hasCustomer ? null : Colors.grey,
),
),
),
const Icon(Icons.search),
],
),
),
);
}
}

View File

@@ -1,85 +0,0 @@
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,
),
),
),
],
],
),
),
);
}
}

View File

@@ -1,376 +0,0 @@
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/utils/extensions.dart';
import 'package:flux/core/widgets/image_viewer_widget.dart';
import 'package:flux/core/widgets/pdf_viewer_widget.dart';
import 'package:flux/core/widgets/qr_upload_dialog.dart';
import 'package:flux/features/attachments/models/attachment_model.dart';
import 'package:flux/features/operations/blocs/operation_files_bloc.dart';
import 'package:flux/features/operations/blocs/operations_cubit.dart';
import 'package:flux/features/operations/models/operation_model.dart';
class AttachmentsSection extends StatelessWidget {
const AttachmentsSection({super.key});
Future<void> _pickFiles(BuildContext context) async {
// Usiamo withData: true fondamentale per avere i bytes e caricare su Supabase Storage
FilePickerResult? result = await FilePicker.pickFiles(
allowMultiple: true,
type: FileType.custom,
allowedExtensions: ['pdf', 'jpg', 'jpeg', 'png'],
withData: true,
);
if (result != null && context.mounted) {
context.read<OperationFilesBloc>().add(
AddOperationFilesEvent(result.files),
);
}
}
@override
Widget build(BuildContext context) {
OperationFilesBloc operationFilesBloc = BlocProvider.of<OperationFilesBloc>(
context,
);
return BlocListener<OperationsCubit, OperationsState>(
listenWhen: (previous, current) =>
previous.currentOperation?.id == null &&
current.currentOperation?.id != null,
listener: (context, state) {
// FIGASSA! La pratica è stata salvata e ora ha un ID.
// Diciamo al Bloc dei file di agganciarsi al database.
final newId = state.currentOperation!.id!;
context.read<OperationFilesBloc>().add(OperationsavedEvent(newId));
},
child: BlocBuilder<OperationFilesBloc, OperationFilesState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- HEADER SEZIONE ---
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"DOCUMENTI ALLEGATI",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
letterSpacing: 1.2,
),
),
Row(
children: [
OutlinedButton.icon(
icon: const Icon(Icons.attach_file),
label: const Text("Aggiungi File"),
onPressed: () => _pickFiles(context),
),
if (!context
.read<SessionCubit>()
.state
.isMobileDevice) ...[
const SizedBox(width: 12),
ElevatedButton.icon(
onPressed: () => _handleGenerateQr(context),
icon: const Icon(Icons.qr_code),
label: const Text("GENERA QR"),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.1),
foregroundColor: Theme.of(
context,
).colorScheme.primary,
elevation: 0,
),
),
],
],
),
],
),
const SizedBox(height: 12),
// --- LISTA VUOTA ---
if (state.allFiles.isEmpty) // Furbata: usiamo allFiles.isEmpty!
Container(
width: double.infinity,
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
border: Border.all(
color: Colors.grey.shade300,
style: BorderStyle.solid,
),
borderRadius: BorderRadius.circular(8),
color: Colors.grey.shade50,
),
child: const Text(
"Nessun documento allegato alla bozza.",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
)
// --- LISTA PIENA ---
else ...[
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: state.allFiles.length,
itemBuilder: (context, index) {
final file = state.allFiles[index];
final sizeMb = (file.fileSize / (1024 * 1024))
.toStringAsFixed(2);
final isPdf = file.extension.toLowerCase() == 'pdf';
final isSelected = state.selectedFiles.contains(file);
return GestureDetector(
onTap: () => operationFilesBloc.add(
ToggleOperationFileSelectionEvent(file),
),
onDoubleTap: () => _handleDoubleClick(context, file),
child: Card(
margin: const EdgeInsets.only(bottom: 8),
elevation: 0,
// UX Fina: cambiamo colore del bordo se selezionato
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(
color: isSelected
? Theme.of(context).colorScheme.primary
: Colors.grey.shade300,
width: isSelected ? 2 : 1,
),
),
// UX Fina: Sfondo leggermente colorato se selezionato
color: isSelected
? Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.05)
: Theme.of(context).colorScheme.surface,
child: ListTile(
leading: Icon(
isSelected
? Icons.check_box
: Icons.check_box_outline_blank,
color: Theme.of(context).colorScheme.primary,
size: 32,
),
title: Text(
file.name,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
file.isLocal ? "$sizeMb MB • (Nuovo)" : " MB",
),
trailing: Icon(
isPdf ? Icons.picture_as_pdf : Icons.image,
color: isPdf ? Colors.red : Colors.blue,
size: 32,
),
),
),
);
},
),
// --- PANNELLO AZIONI CONTESTUALI (LA MAGIA) ---
// Appare SOLO se c'è almeno un file selezionato
if (state.selectedFiles.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 16.0),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.3),
),
),
child: Row(
children: [
// Contatore
Text(
"${state.selectedFiles.length} file selezionati",
style: TextStyle(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.primary,
),
),
const Spacer(),
// Bottone Elimina
TextButton.icon(
style: TextButton.styleFrom(
foregroundColor: Colors.red,
),
icon: const Icon(Icons.delete_outline),
label: const Text("Elimina"),
onPressed: () {
// Qui lancerai l'evento per eliminare i file selezionati!
// Es: operationFilesBloc.add(DeleteSelectedFilesEvent());
},
),
const SizedBox(width: 8),
// Bottone Copia
ElevatedButton.icon(
icon: const Icon(Icons.copy),
label: const Text("Copia in Cliente"),
onPressed: () {
final cubit = context.read<OperationsCubit>();
if (cubit.state.currentOperation?.customerId ==
null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
context
.l10n
.operationFormAttachmentSectionNoCustomer,
),
),
);
} else {
context.read<OperationFilesBloc>().add(
LinkFilesToCustomerEvent(
customerId: cubit
.state
.currentOperation!
.customerId!,
),
);
}
},
),
],
),
),
),
],
],
);
},
),
);
}
Future<void> _handleGenerateQr(BuildContext context) async {
final cubit = context.read<OperationsCubit>();
var currentOperation = cubit.state.currentOperation;
// 1. CATTURIAMO IL BLOC MENTRE SIAMO ANCORA NELLA PAGINA
final operationFilesBloc = context.read<OperationFilesBloc>();
// 2. SE LA PRATICA E' NUOVA (Manca l'ID)
if (currentOperation == null || currentOperation.id == null) {
// NIENTE BlocListener qui! Solo un semplice Dialog di conferma
final bool? confirm = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: const Text("Salvataggio Necessario"),
content: const Text(
"Per generare il QR Code e caricare file dal telefono, la pratica deve essere prima salvata in BOZZA.\n\nVuoi salvare ora?",
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text("Annulla"),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text("Salva in Bozza"),
),
],
),
);
if (confirm != true) return; // Utente ha annullato
// Salviamo forzatamente in bozza
await cubit.saveCurrentOperation(
targetStatus: OperationStatus.draft,
shouldPop: false,
);
// Recuperiamo il servizio aggiornato con l'ID!
currentOperation = cubit.state.currentOperation;
if (currentOperation?.id == null) return;
}
// 3. MOSTRIAMO IL QR CODE (Con il Ponte e l'Auto-Chiusura!)
if (context.mounted) {
final nomePratica =
"Pratica ${currentOperation?.customerDisplayName ?? ''}".trim();
showDialog(
context: context,
builder: (dialogContext) => BlocProvider.value(
// INIETTIAMO IL BLOC NEL CONTESTO DEL DIALOG ALIENO
value: operationFilesBloc,
// ORA METTIAMO L'AUTO-CHIUSURA SUL QR CODE!
child: BlocListener<OperationFilesBloc, OperationFilesState>(
listener: (context, state) {
// Se arrivano file remoti e lo stato è success, chiudiamo il QR!
// (Nota: usiamo dialogContext per assicurarci di chiudere il popup giusto)
if (state.status == OperationFilesStatus.success &&
state.remoteFiles.isNotEmpty) {
Navigator.of(dialogContext).pop();
}
},
child: QrUploadDialog(
deepLinkUrl:
'fluxapp:///operation/${currentOperation!.id}/upload?name=${Uri.encodeComponent(nomePratica)}',
title: 'Scatta per\n$nomePratica',
),
),
),
);
}
}
// --- LOGICA DI VISUALIZZAZIONE OVERLAY ---
void _handleDoubleClick(BuildContext context, AttachmentModel file) {
showDialog(
context: context,
barrierDismissible: true,
builder: (ctx) => Dialog(
insetPadding: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: SizedBox(
width: double.infinity,
height: MediaQuery.of(context).size.height * 0.8,
child: file.isPdf
? PdfViewerWidget(
storagePath: file.storagePath.isNotEmpty
? file.storagePath
: null,
bytes: file.localBytes,
)
: ImageViewerWidget(
storagePath: file.storagePath.isNotEmpty
? file.storagePath
: null,
bytes: file.localBytes,
),
),
),
),
);
}
}

View File

@@ -1,96 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flux/features/customers/ui/customer_search_sheet.dart';
import 'package:flux/features/operations/models/operation_model.dart';
class CustomerSection extends StatelessWidget {
final OperationModel operation;
const CustomerSection({super.key, required this.operation});
void _openCustomerSearch(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (modalContext) {
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(modalContext).viewInsets.bottom,
),
// La modale di ricerca
child: const CustomerSearchSheet(),
);
},
);
}
@override
Widget build(BuildContext context) {
// Niente BlocBuilder qui! Leggiamo solo la variabile 'operation'
final hasCustomer = operation.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),
if (!hasCustomer)
Center(
child: ElevatedButton.icon(
onPressed: () => _openCustomerSearch(context),
icon: const Icon(Icons.search),
label: const Text("Seleziona o Crea Cliente"),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
),
)
else
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
operation.customerDisplayName ?? "Cliente Selezionato",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
TextButton.icon(
onPressed: () => _openCustomerSearch(context),
icon: const Icon(Icons.edit, size: 18),
label: const Text("Cambia"),
),
],
),
],
),
),
);
}
}

View File

@@ -1,417 +0,0 @@
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/operations/models/energy_operation_model.dart'; // Assicurati degli import
class EnergyOperationDialog extends StatefulWidget {
final List<EnergyOperationModel> initialOperations;
final String
currentStoreId; // Ci serve per sapere per quale negozio caricare i gestori
const EnergyOperationDialog({
super.key,
required this.initialOperations,
required this.currentStoreId,
});
@override
State<EnergyOperationDialog> createState() => _EnergyOperationDialogState();
}
class _EnergyOperationDialogState extends State<EnergyOperationDialog> {
// Lista temporanea per non "sporcare" il cubit finché non si preme Conferma
late List<EnergyOperationModel> _tempList;
bool _isAddingNew = false;
@override
void initState() {
super.initState();
_tempList = List.from(widget.initialOperations);
// 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: (newOperation) {
setState(() {
_tempList.add(newOperation);
_isAddingNew = false; // Torna alla lista
});
},
onCancel: () {
setState(() => _isAddingNew = false);
},
)
: _EnergyList(
operations: _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<EnergyOperationModel> operations;
final List<ProviderModel>
activeProviders; // <--- NUOVO: La lista vera dal Cubit
final Function(int) onDelete;
final VoidCallback onAddTap;
const _EnergyList({
required this.operations,
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 (operations.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: operations.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final s = operations[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)
: '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(EnergyOperationModel) 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;
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 {
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: 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
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));
}).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 newOperation = EnergyOperationModel(
type: _selectedType,
expiration: _selectedExpiration!,
providerId: _selectedProviderId!,
);
widget.onSave(newOperation);
},
child: const Text("Salva Contratto"),
),
],
),
],
);
}
}

View File

@@ -1,393 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.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/operations/data/operations_repository.dart';
import 'package:flux/features/operations/models/entertainment_operation_model.dart';
import 'package:get_it/get_it.dart';
class EntertainmentOperationDialog extends StatefulWidget {
final List<EntertainmentOperationModel> initialOperations;
final String currentStoreId;
const EntertainmentOperationDialog({
super.key,
required this.initialOperations,
required this.currentStoreId,
});
@override
State<EntertainmentOperationDialog> createState() =>
_EntertainmentOperationDialogState();
}
class _EntertainmentOperationDialogState
extends State<EntertainmentOperationDialog> {
late List<EntertainmentOperationModel> _tempList;
bool _isAddingNew = false;
@override
void initState() {
super.initState();
_tempList = List.from(widget.initialOperations);
// Carichiamo i provider attivi per lo store corrente
context.read<ProvidersCubit>().loadActiveProvidersForStore(
widget.currentStoreId,
);
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Row(
children: [
Icon(
Icons.movie_filter_outlined,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(_isAddingNew ? "Nuovo Servizio" : "Servizi Intrattenimento"),
],
),
content: AnimatedSize(
duration: const Duration(milliseconds: 300),
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.9,
child: _isAddingNew
? _EntertainmentForm(
// Il form che abbiamo creato prima
onSave: (newOperation) => setState(() {
_tempList.add(newOperation);
_isAddingNew = false;
}),
onCancel: () => setState(() => _isAddingNew = false),
)
: BlocBuilder<ProvidersCubit, ProvidersState>(
builder: (context, state) {
// Passiamo allProviders per garantire la visione dello storico
return _EntertainmentList(
operations: _tempList,
allProviders: state.allProviders,
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 Tutti"),
),
]
: null, // I pulsanti del form sono interni al form stesso
);
}
}
class _EntertainmentList extends StatelessWidget {
final List<EntertainmentOperationModel> operations;
final List<ProviderModel> allProviders;
final Function(int) onDelete;
final VoidCallback onAddTap;
const _EntertainmentList({
required this.operations,
required this.allProviders,
required this.onDelete,
required this.onAddTap,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (operations.isEmpty)
const Padding(
padding: EdgeInsets.symmetric(vertical: 32.0),
child: Text(
"Nessun servizio intrattenimento.",
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey),
),
)
else
Flexible(
child: ListView.separated(
shrinkWrap: true,
itemCount: operations.length,
separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) {
final s = operations[index];
final providerName = allProviders
.firstWhere(
(p) => p.id == s.providerId,
orElse: () => ProviderModel(
id: '',
nome: 'Fornitore Storico',
companyId: '',
isActive: false,
energia: false,
telefoniaFissa: false,
telefoniaMobile: false,
assicurazioni: false,
finanziamenti: false,
altro: false,
intrattenimento: false,
),
)
.nome;
return ListTile(
contentPadding: EdgeInsets.zero,
leading: CircleAvatar(
backgroundColor: Colors.purple.shade100,
child: const Icon(
Icons.movie_creation_outlined,
color: Colors.purple,
),
),
title: Text(
"${s.type}$providerName",
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Text(
s.constrained
? "Vincolo fino al: ${s.constrainExpiration.day}/${s.constrainExpiration.month}/${s.constrainExpiration.year}"
: "Senza vincoli",
style: TextStyle(
color: s.constrained
? Colors.red.shade700
: Colors.green.shade700,
),
),
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 Servizio"),
),
],
);
}
}
// ---ENTERTAINMENT FORM (MODALE)---
class _EntertainmentForm extends StatefulWidget {
final Function(EntertainmentOperationModel) onSave;
final VoidCallback onCancel;
const _EntertainmentForm({required this.onSave, required this.onCancel});
@override
State<_EntertainmentForm> createState() => _EntertainmentFormState();
}
class _EntertainmentFormState extends State<_EntertainmentForm> {
String? _selectedProviderId;
final TextEditingController _typeController = TextEditingController();
bool _isConstrained = false;
DateTime _expirationDate = DateTime.now().add(
const Duration(days: 365),
); // Default 12 mesi
// Preset rapidi per il vincolo (es: 12, 24 mesi)
int? _selectedPresetMonths;
void _applyPreset(int months) {
setState(() {
_selectedPresetMonths = months;
_isConstrained = true;
final now = DateTime.now();
_expirationDate = DateTime(now.year, now.month + months, now.day);
});
}
Future<void> _pickDate() async {
final picked = await showDatePicker(
context: context,
initialDate: _expirationDate,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365 * 10)),
);
if (picked != null) {
setState(() {
_expirationDate = picked;
_selectedPresetMonths = null;
_isConstrained = true;
});
}
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 1. GESTORE (Filtro intrattenimento)
BlocBuilder<ProvidersCubit, ProvidersState>(
builder: (context, state) {
final filtered = state.activeProviders
.where((p) => p.intrattenimento)
.toList();
return DropdownButtonFormField<String>(
decoration: const InputDecoration(
labelText: "Fornitore (es: Sky, TIM)",
border: OutlineInputBorder(),
),
items: filtered
.map(
(p) => DropdownMenuItem(value: p.id, child: Text(p.nome)),
)
.toList(),
onChanged: (val) => setState(() => _selectedProviderId = val),
);
},
),
const SizedBox(height: 16),
// 2. TIPO SERVIZIO (TextField con suggerimenti rapidi sotto)
TextFormField(
controller: _typeController,
decoration: const InputDecoration(
labelText: "Servizio",
hintText: "es: Netflix, DAZN, Disney+",
border: OutlineInputBorder(),
),
onChanged: (val) => setState(() {}),
),
const SizedBox(height: 8),
// Suggerimenti rapidi (Chip)
FutureBuilder<List<String>>(
future: GetIt.I<OperationsRepository>().fetchTopEntertainmentTypes(
GetIt.I<SessionCubit>().state.company!.id!,
),
builder: (context, snapshot) {
final suggestions = snapshot.data ?? ["Netflix", "DAZN", "Sky"];
return Wrap(
spacing: 8,
children: suggestions.map((s) {
return ActionChip(
label: Text(s, style: const TextStyle(fontSize: 12)),
onPressed: () => setState(() => _typeController.text = s),
);
}).toList(),
);
},
),
const SizedBox(height: 16),
// 3. VINCOLO CONTRATTUALE
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Vincolo di permanenza",
style: TextStyle(fontWeight: FontWeight.bold),
),
Switch(
value: _isConstrained,
onChanged: (val) => setState(() {
_isConstrained = val;
if (!val) _selectedPresetMonths = null;
}),
),
],
),
if (_isConstrained) ...[
const SizedBox(height: 8),
SegmentedButton<int?>(
segments: const [
ButtonSegment(value: 12, label: Text("12m")),
ButtonSegment(value: 24, label: Text("24m")),
ButtonSegment(
value: null,
label: Icon(Icons.calendar_month, size: 20),
),
],
selected: {_selectedPresetMonths},
onSelectionChanged: (val) {
if (val.first == null) {
_pickDate();
} else {
_applyPreset(val.first!);
}
},
),
const SizedBox(height: 12),
// Box data scadenza vincolo
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.event_busy, size: 18, color: Colors.redAccent),
const SizedBox(width: 8),
Text(
"Scadenza vincolo: ${_expirationDate.day.toString().padLeft(2, '0')}/${_expirationDate.month.toString().padLeft(2, '0')}/${_expirationDate.year}",
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
],
const SizedBox(height: 24),
// PULSANTI
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: widget.onCancel,
child: const Text("Annulla"),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed:
(_selectedProviderId == null || _typeController.text.isEmpty)
? null
: () => widget.onSave(
EntertainmentOperationModel(
providerId: _selectedProviderId!,
type: _typeController.text,
constrained: _isConstrained,
constrainExpiration: _expirationDate,
),
),
child: const Text("Aggiungi"),
),
],
),
],
);
}
}

View File

@@ -1,479 +0,0 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
import 'package:flux/features/master_data/products/models/model_model.dart';
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
import 'package:flux/features/operations/models/fin_operation_model.dart';
import 'package:flux/features/master_data/providers/models/provider_model.dart';
// ===========================================================================
// DIALOG PRINCIPALE
// ===========================================================================
class FinanceOperationDialog extends StatefulWidget {
final List<FinOperationModel> initialOperations;
final String currentStoreId;
final ProductCubit productCubit;
const FinanceOperationDialog({
super.key,
required this.initialOperations,
required this.currentStoreId,
required this.productCubit,
});
@override
State<FinanceOperationDialog> createState() => _FinanceOperationDialogState();
}
class _FinanceOperationDialogState extends State<FinanceOperationDialog> {
late List<FinOperationModel> _tempList;
bool _isAddingNew = false;
@override
void initState() {
super.initState();
_tempList = List.from(widget.initialOperations);
// Carichiamo i dati necessari dai Cubit
context.read<ProvidersCubit>().loadActiveProvidersForStore(
widget.currentStoreId,
);
context.read<ProductCubit>().loadBrands();
}
@override
Widget build(BuildContext context) {
return BlocProvider.value(
value: widget.productCubit,
child: 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(
operations: _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<FinOperationModel> operations;
final List<ProviderModel> allProviders;
final List<ModelModel> allModels;
final Function(int) onDelete;
final VoidCallback onAddTap;
const _FinanceList({
required this.operations,
required this.allProviders,
required this.allModels,
required this.onDelete,
required this.onAddTap,
});
@override
Widget build(BuildContext context) {
if (operations.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: operations.length,
separatorBuilder: (_, _) => const Divider(),
itemBuilder: (context, index) {
final s = operations[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,
finanziamenti: 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(FinOperationModel) 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
Timer? _debounce;
final TextEditingController _searchController = TextEditingController();
late DateTime _selectedExpirationDate;
@override
void initState() {
super.initState();
final now = DateTime.now();
_selectedExpirationDate = DateTime(
now.year,
now.month + _selectedMonths,
now.day,
); // Inizialmente 30 mesi dalla data attuale
}
void _onSearchChanged(String query) {
if (_debounce?.isActive ?? false) _debounce!.cancel();
_debounce = Timer(const Duration(milliseconds: 500), () {
context.read<ProductCubit>().searchModels(query);
});
}
// Funzione per aggiornare la data quando si clicca sui segmenti 24, 30, 48
void _updateExpirationByMonths(int months) {
setState(() {
_selectedMonths = months;
final now = DateTime.now();
// Calcolo preciso: aggiungiamo i mesi alla data attuale
_selectedExpirationDate = DateTime(now.year, now.month + months, now.day);
});
}
// Funzione per il picker manuale
Future<void> _selectManualDate() async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedExpirationDate,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(
const Duration(days: 365 * 10),
), // Fino a 10 anni
);
if (picked != null && picked != _selectedExpirationDate) {
setState(() {
_selectedExpirationDate = picked;
_selectedMonths = 0; // Resettiamo i segmenti perché è una data custom
});
}
}
@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
.where((p) => p.finanziamenti)
.toList(); // Già filtrati dal caricamento della dialog
return DropdownButtonFormField<String>(
initialValue: _selectedProviderId,
decoration: const InputDecoration(
labelText: "Gestore",
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) {
_onSearchChanged(val);
},
),
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) => _updateExpirationByMonths(val.first),
),
const SizedBox(height: 16),
// RIEPILOGO DATA E PICKER MANUALE (Stile Energia)
const Text(
"Scadenza Finanziamento",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 12,
color: Colors.grey,
),
),
const SizedBox(height: 4),
InkWell(
onTap: _selectManualDate,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
const Icon(
Icons.calendar_today,
size: 18,
color: Colors.blue,
),
const SizedBox(width: 12),
Text(
"${_selectedExpirationDate.day.toString().padLeft(2, '0')}/${_selectedExpirationDate.month.toString().padLeft(2, '0')}/${_selectedExpirationDate.year}",
style: const TextStyle(fontSize: 16),
),
],
),
const Icon(Icons.edit, size: 18, color: Colors.grey),
],
),
),
),
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(
FinOperationModel(
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

@@ -1,65 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flux/features/operations/models/operation_model.dart';
class GeneralInfoSection extends StatelessWidget {
final OperationModel operation;
const GeneralInfoSection({super.key, required this.operation});
@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.info_outline,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Text(
"Info Generali",
style: Theme.of(context).textTheme.titleMedium,
),
],
),
const SizedBox(height: 16),
// Numero di Riferimento / Telefono
TextFormField(
initialValue: operation.reference,
keyboardType: TextInputType
.phone, // Fa aprire il tastierino numerico su mobile
decoration: const InputDecoration(
labelText: "Numero di Telefono / Riferimento",
hintText: "Es. 3331234567",
border: OutlineInputBorder(),
prefixIcon: Icon(Icons.phone),
),
),
const SizedBox(height: 16),
// Campo Note
TextFormField(
initialValue: operation.note,
maxLines: 4,
minLines: 2,
decoration: const InputDecoration(
labelText: "Note Operazione",
hintText:
"Scrivi qui eventuali dettagli o richieste del cliente...",
border: OutlineInputBorder(),
alignLabelWithHint: true,
),
),
],
),
),
);
}
}

View File

@@ -1,158 +0,0 @@
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),
);
}
}

View File

@@ -1,201 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/operations/blocs/operations_cubit.dart';
import 'package:flux/features/operations/models/operation_model.dart';
import 'package:flux/features/operations/ui/operation_form_screen/attachment_section.dart';
import 'package:flux/features/operations/ui/operation_form_screen/customer_section.dart';
import 'package:flux/features/operations/ui/operation_form_screen/general_info_section.dart';
class OperationFormScreen extends StatefulWidget {
final String? operationId;
final OperationModel? existingOperation; // <-- AGGIUNTO
const OperationFormScreen({
super.key,
this.operationId,
this.existingOperation, // <-- AGGIUNTO
});
@override
State<OperationFormScreen> createState() => _OperationFormScreenState();
}
class _OperationFormScreenState extends State<OperationFormScreen> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
// Diamo in pasto al Cubit tutto quello che abbiamo!
context.read<OperationsCubit>().initOperationForm(
existingOperation: widget.existingOperation,
operationId: widget.operationId,
);
});
}
void _performSave(
BuildContext context, {
required OperationStatus targetStatus,
required bool shouldPop,
}) {
FocusScope.of(context).unfocus();
context.read<OperationsCubit>().saveCurrentOperation(
targetStatus: targetStatus,
shouldPop: shouldPop,
);
}
@override
Widget build(BuildContext context) {
return BlocConsumer<OperationsCubit, OperationsState>(
listener: (context, state) {
if (state.status == OperationsStatus.saved) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Pratica salvata con successo!"),
backgroundColor: Colors.green,
),
);
Navigator.pop(context);
}
if (state.status == OperationsStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text("Errore: ${state.errorMessage ?? ''}"),
backgroundColor: Colors.red,
),
);
}
if (state.status == OperationsStatus.savedNoPop) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text("Pratica salvata con successo!"),
backgroundColor: Colors.green,
),
);
}
},
builder: (context, state) {
final operation = state.currentOperation;
final isSaving = state.status == OperationsStatus.saving;
final isEditMode = widget.operationId != null;
return Scaffold(
appBar: AppBar(
title: Text(isEditMode ? "Modifica Pratica" : "Nuova Pratica"),
actions: [
if (isSaving)
const Padding(
padding: EdgeInsets.only(right: 20.0),
child: Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
),
),
)
else if (operation != null) ...[
IconButton(
icon: const Icon(Icons.edit_note),
tooltip: "Salva come Bozza",
onPressed: () => _performSave(
context,
targetStatus: OperationStatus.draft,
shouldPop: false,
),
),
IconButton(
icon: const Icon(
Icons.check_circle_outline,
color: Colors.green,
),
tooltip: "Conferma Pratica",
onPressed: () => _performSave(
context,
targetStatus: OperationStatus.ok,
shouldPop: true,
),
),
const SizedBox(width: 8),
],
],
),
body: (operation == null)
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
CustomerSection(operation: operation),
const SizedBox(height: 24),
GeneralInfoSection(operation: operation),
const SizedBox(height: 24),
AttachmentsSection(),
const SizedBox(height: 32),
_buildBottomActionButtons(context, isSaving: isSaving),
const SizedBox(height: 32),
],
),
),
);
},
);
}
Widget _buildBottomActionButtons(
BuildContext context, {
required bool isSaving,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(
flex: 1,
child: OutlinedButton.icon(
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
icon: const Icon(Icons.edit_note),
label: const Text("Salva in Bozza"),
onPressed: isSaving
? null
: () => _performSave(
context,
targetStatus: OperationStatus.draft,
shouldPop: false,
),
),
),
const SizedBox(width: 16),
Expanded(
flex: 2,
child: ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green.shade600,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
),
icon: const Icon(Icons.check_circle_outline),
label: const Text(
"CONFERMA PRATICA",
style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1),
),
onPressed: isSaving
? null
: () => _performSave(
context,
targetStatus: OperationStatus.ok,
shouldPop: true,
),
),
),
],
);
}
}

View File

@@ -1,9 +1,9 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/operations/blocs/operation_files_bloc.dart';
import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flux/features/operations/blocs/operation_files_bloc.dart';
class OperationMobileUploadScreen extends StatefulWidget {
final String operationId;

View File

@@ -16,7 +16,7 @@ import 'package:flux/core/data/core_repository.dart';
import 'package:flux/core/routes/app_router.dart';
import 'package:flux/core/theme/theme.dart';
import 'package:flux/core/theme/bloc/theme_bloc.dart';
import 'package:flux/features/customers/blocs/customer_cubit.dart';
import 'package:flux/features/customers/blocs/customers_cubit.dart';
import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
import 'package:flux/features/master_data/products/data/product_repository.dart';
@@ -48,7 +48,7 @@ void main() async {
// Cubit delle feature
BlocProvider<StoreCubit>(create: (_) => StoreCubit()),
BlocProvider<CustomerCubit>(create: (_) => CustomerCubit()),
BlocProvider<CustomersCubit>(create: (_) => CustomersCubit()),
BlocProvider<ProductCubit>(create: (_) => ProductCubit()),
BlocProvider<StaffCubit>(create: (_) => StaffCubit()),
BlocProvider<OperationsCubit>(create: (_) => OperationsCubit()),