maremma maiala impestata, buonissima base dopo ultra refactor
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -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_detail_screen.dart';
|
||||||
import 'package:flux/features/customers/ui/customer_mobile_upload_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/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/home/ui/home_screen.dart';
|
||||||
import 'package:flux/features/master_data/master_data_hub_content.dart';
|
import 'package:flux/features/master_data/master_data_hub_content.dart';
|
||||||
import 'package:flux/features/master_data/products/ui/products_screen.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/onboarding/ui/onboarding_screen.dart';
|
||||||
import 'package:flux/features/operations/blocs/operation_files_bloc.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/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.dart';
|
||||||
import 'package:flux/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart';
|
import 'package:flux/features/operations/ui/operation_mobile_upload_screen.dart';
|
||||||
import 'package:flux/features/operations/ui/operations_screen.dart';
|
import 'package:flux/features/operations/ui/operations_screen.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|||||||
@@ -6,31 +6,31 @@ import 'package:flux/features/customers/data/customer_repository.dart';
|
|||||||
import 'package:flux/features/customers/models/customer_model.dart';
|
import 'package:flux/features/customers/models/customer_model.dart';
|
||||||
import 'package:get_it/get_it.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 CustomerRepository _repository = GetIt.I<CustomerRepository>();
|
||||||
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||||
|
|
||||||
// Variabile per gestire il debounce della ricerca
|
// Variabile per gestire il debounce della ricerca
|
||||||
Timer? _searchDebounce;
|
Timer? _searchDebounce;
|
||||||
|
|
||||||
CustomerCubit() : super(const CustomerState());
|
CustomersCubit() : super(const CustomersState());
|
||||||
|
|
||||||
// --- LETTURA ---
|
// --- LETTURA ---
|
||||||
Future<void> loadCustomers() async {
|
Future<void> loadCustomers() async {
|
||||||
emit(state.copyWith(status: CustomerStatus.loading));
|
emit(state.copyWith(status: CustomersStatus.loading));
|
||||||
try {
|
try {
|
||||||
final customers = await _repository.getCustomers(
|
final customers = await _repository.getCustomers(
|
||||||
_sessionCubit.state.company!.id!,
|
_sessionCubit.state.company!.id!,
|
||||||
);
|
);
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(status: CustomerStatus.success, customers: customers),
|
state.copyWith(status: CustomersStatus.success, customers: customers),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CustomerStatus.failure,
|
status: CustomersStatus.failure,
|
||||||
errorMessage: e.toString(),
|
errorMessage: e.toString(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -39,7 +39,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
|
|
||||||
// --- CREAZIONE ---
|
// --- CREAZIONE ---
|
||||||
Future<void> createCustomer(CustomerModel customer) async {
|
Future<void> createCustomer(CustomerModel customer) async {
|
||||||
emit(state.copyWith(status: CustomerStatus.loading));
|
emit(state.copyWith(status: CustomersStatus.loading));
|
||||||
try {
|
try {
|
||||||
final newCustomer = await _repository.saveCustomer(customer);
|
final newCustomer = await _repository.saveCustomer(customer);
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CustomerStatus.success,
|
status: CustomersStatus.success,
|
||||||
customers: updatedList,
|
customers: updatedList,
|
||||||
lastCreatedCustomer: newCustomer,
|
lastCreatedCustomer: newCustomer,
|
||||||
),
|
),
|
||||||
@@ -57,7 +57,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CustomerStatus.failure,
|
status: CustomersStatus.failure,
|
||||||
errorMessage: e.toString(),
|
errorMessage: e.toString(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -66,7 +66,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
|
|
||||||
// --- AGGIORNAMENTO ---
|
// --- AGGIORNAMENTO ---
|
||||||
Future<void> updateCustomer(CustomerModel customer) async {
|
Future<void> updateCustomer(CustomerModel customer) async {
|
||||||
emit(state.copyWith(status: CustomerStatus.loading));
|
emit(state.copyWith(status: CustomersStatus.loading));
|
||||||
try {
|
try {
|
||||||
final updatedCustomer = await _repository.updateCustomer(customer);
|
final updatedCustomer = await _repository.updateCustomer(customer);
|
||||||
|
|
||||||
@@ -79,7 +79,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CustomerStatus.success,
|
status: CustomersStatus.success,
|
||||||
customers: updatedList,
|
customers: updatedList,
|
||||||
lastCreatedCustomer:
|
lastCreatedCustomer:
|
||||||
updatedCustomer, // Utile se modifichi un cliente appena creato
|
updatedCustomer, // Utile se modifichi un cliente appena creato
|
||||||
@@ -88,7 +88,7 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CustomerStatus.failure,
|
status: CustomersStatus.failure,
|
||||||
errorMessage: e.toString(),
|
errorMessage: e.toString(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -115,12 +115,12 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
query,
|
query,
|
||||||
);
|
);
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(status: CustomerStatus.success, customers: results),
|
state.copyWith(status: CustomersStatus.success, customers: results),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
status: CustomerStatus.failure,
|
status: CustomersStatus.failure,
|
||||||
errorMessage: e.toString(),
|
errorMessage: e.toString(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
part of 'customer_cubit.dart';
|
part of 'customers_cubit.dart';
|
||||||
|
|
||||||
enum CustomerStatus {
|
enum CustomersStatus {
|
||||||
initial,
|
initial,
|
||||||
loading,
|
loading,
|
||||||
filesLoading,
|
filesLoading,
|
||||||
@@ -9,26 +9,26 @@ enum CustomerStatus {
|
|||||||
failure,
|
failure,
|
||||||
}
|
}
|
||||||
|
|
||||||
class CustomerState extends Equatable {
|
class CustomersState extends Equatable {
|
||||||
final CustomerStatus status;
|
final CustomersStatus status;
|
||||||
final List<CustomerModel> customers;
|
final List<CustomerModel> customers;
|
||||||
final CustomerModel? lastCreatedCustomer;
|
final CustomerModel? lastCreatedCustomer;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
const CustomerState({
|
const CustomersState({
|
||||||
this.status = CustomerStatus.initial,
|
this.status = CustomersStatus.initial,
|
||||||
this.customers = const [],
|
this.customers = const [],
|
||||||
this.lastCreatedCustomer,
|
this.lastCreatedCustomer,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
CustomerState copyWith({
|
CustomersState copyWith({
|
||||||
CustomerStatus? status,
|
CustomersStatus? status,
|
||||||
List<CustomerModel>? customers,
|
List<CustomerModel>? customers,
|
||||||
CustomerModel? lastCreatedCustomer,
|
CustomerModel? lastCreatedCustomer,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
}) {
|
}) {
|
||||||
return CustomerState(
|
return CustomersState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
customers: customers ?? this.customers,
|
customers: customers ?? this.customers,
|
||||||
lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer,
|
lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer,
|
||||||
@@ -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);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
import 'package:flux/core/theme/theme.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/models/customer_model.dart';
|
||||||
import 'package:flux/features/customers/ui/customer_form.dart';
|
import 'package:flux/features/customers/ui/customer_form.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -26,14 +26,14 @@ class _CustomersContentState extends State<CustomersContent> {
|
|||||||
void _loadInitialCustomers() {
|
void _loadInitialCustomers() {
|
||||||
final companyId = context.read<SessionCubit>().state.company?.id;
|
final companyId = context.read<SessionCubit>().state.company?.id;
|
||||||
if (companyId != null) {
|
if (companyId != null) {
|
||||||
context.read<CustomerCubit>().loadCustomers();
|
context.read<CustomersCubit>().loadCustomers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSearch(String query) {
|
void _onSearch(String query) {
|
||||||
final companyId = context.read<SessionCubit>().state.company?.id;
|
final companyId = context.read<SessionCubit>().state.company?.id;
|
||||||
if (companyId != null) {
|
if (companyId != null) {
|
||||||
context.read<CustomerCubit>().searchCustomers(query);
|
context.read<CustomersCubit>().searchCustomers(query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,9 +86,9 @@ class _CustomersContentState extends State<CustomersContent> {
|
|||||||
|
|
||||||
// LISTA CLIENTI
|
// LISTA CLIENTI
|
||||||
Expanded(
|
Expanded(
|
||||||
child: BlocBuilder<CustomerCubit, CustomerState>(
|
child: BlocBuilder<CustomersCubit, CustomersState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.status == CustomerStatus.loading &&
|
if (state.status == CustomersStatus.loading &&
|
||||||
state.customers.isEmpty) {
|
state.customers.isEmpty) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
@@ -242,12 +242,12 @@ void openCustomerForm({
|
|||||||
|
|
||||||
if (customer == null) {
|
if (customer == null) {
|
||||||
// CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create
|
// CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create
|
||||||
context.read<CustomerCubit>().createCustomer(
|
context.read<CustomersCubit>().createCustomer(
|
||||||
customerFromForm.copyWith(companyId: companyId),
|
customerFromForm.copyWith(companyId: companyId),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// CASO MODIFICA: L'ID e il companyId sono già nel modello
|
// CASO MODIFICA: L'ID e il companyId sono già nel modello
|
||||||
context.read<CustomerCubit>().updateCustomer(customerFromForm);
|
context.read<CustomersCubit>().updateCustomer(customerFromForm);
|
||||||
}
|
}
|
||||||
Navigator.pop(dialogContext);
|
Navigator.pop(dialogContext);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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/customers/blocs/customer_cubit.dart';
|
import 'package:flux/features/customers/blocs/customers_cubit.dart';
|
||||||
|
|
||||||
class QuickCustomerDialog extends StatefulWidget {
|
class QuickCustomerDialog extends StatefulWidget {
|
||||||
final String initialQuery;
|
final String initialQuery;
|
||||||
@@ -42,13 +42,15 @@ class _QuickCustomerDialogState extends State<QuickCustomerDialog> {
|
|||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
// Chiamata al Cubit (aggiorna i parametri in base a come li hai definiti)
|
// Chiamata al Cubit (aggiorna i parametri in base a come li hai definiti)
|
||||||
final newCustomer = await context.read<CustomerCubit>().quickCreateCustomer(
|
final newCustomer = await context
|
||||||
name: _nameCtrl.text.trim(),
|
.read<CustomersCubit>()
|
||||||
phone: _phoneCtrl.text.trim(),
|
.quickCreateCustomer(
|
||||||
// Aggiungi questi se li hai inseriti nel tuo CustomerCubit:
|
name: _nameCtrl.text.trim(),
|
||||||
// email: _emailCtrl.text.trim(),
|
phone: _phoneCtrl.text.trim(),
|
||||||
// note: _noteCtrl.text.trim(),
|
// Aggiungi questi se li hai inseriti nel tuo CustomerCubit:
|
||||||
);
|
// email: _emailCtrl.text.trim(),
|
||||||
|
// note: _noteCtrl.text.trim(),
|
||||||
|
);
|
||||||
|
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
|
|
||||||
|
|||||||
@@ -210,12 +210,40 @@ class OperationsCubit extends Cubit<OperationsState> {
|
|||||||
.toList();
|
.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;
|
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,
|
customerId: customerId,
|
||||||
customerDisplayName: customerDisplayName,
|
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));
|
emit(state.copyWith(currentOperation: updated));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -27,6 +27,7 @@ class OperationModel extends Equatable {
|
|||||||
final String? id;
|
final String? id;
|
||||||
final DateTime? createdAt;
|
final DateTime? createdAt;
|
||||||
final String type;
|
final String type;
|
||||||
|
final String? subType;
|
||||||
final String? providerId;
|
final String? providerId;
|
||||||
final String? providerDisplayName;
|
final String? providerDisplayName;
|
||||||
final String? modelId;
|
final String? modelId;
|
||||||
@@ -55,6 +56,7 @@ class OperationModel extends Equatable {
|
|||||||
this.id,
|
this.id,
|
||||||
this.createdAt,
|
this.createdAt,
|
||||||
this.type = '',
|
this.type = '',
|
||||||
|
this.subType,
|
||||||
this.providerId,
|
this.providerId,
|
||||||
this.providerDisplayName,
|
this.providerDisplayName,
|
||||||
this.modelId,
|
this.modelId,
|
||||||
@@ -82,6 +84,7 @@ class OperationModel extends Equatable {
|
|||||||
String? id,
|
String? id,
|
||||||
DateTime? createdAt,
|
DateTime? createdAt,
|
||||||
String? type,
|
String? type,
|
||||||
|
String? subtype,
|
||||||
String? providerId,
|
String? providerId,
|
||||||
String? providerDisplayName,
|
String? providerDisplayName,
|
||||||
String? modelId,
|
String? modelId,
|
||||||
@@ -107,6 +110,7 @@ class OperationModel extends Equatable {
|
|||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
createdAt: createdAt ?? this.createdAt,
|
createdAt: createdAt ?? this.createdAt,
|
||||||
type: type ?? this.type,
|
type: type ?? this.type,
|
||||||
|
subType: subtype ?? this.subType,
|
||||||
providerId: providerId ?? this.providerId,
|
providerId: providerId ?? this.providerId,
|
||||||
providerDisplayName: providerDisplayName ?? this.providerDisplayName,
|
providerDisplayName: providerDisplayName ?? this.providerDisplayName,
|
||||||
modelId: modelId ?? this.modelId,
|
modelId: modelId ?? this.modelId,
|
||||||
@@ -135,6 +139,7 @@ class OperationModel extends Equatable {
|
|||||||
id,
|
id,
|
||||||
createdAt,
|
createdAt,
|
||||||
type,
|
type,
|
||||||
|
subType,
|
||||||
providerId,
|
providerId,
|
||||||
providerDisplayName,
|
providerDisplayName,
|
||||||
modelId,
|
modelId,
|
||||||
@@ -169,6 +174,7 @@ class OperationModel extends Equatable {
|
|||||||
? DateTime.parse(map['created_at'])
|
? DateTime.parse(map['created_at'])
|
||||||
: null,
|
: null,
|
||||||
type: map['type'] as String? ?? '',
|
type: map['type'] as String? ?? '',
|
||||||
|
subType: map['sub_type'] as String?,
|
||||||
providerId: map['provider_id'] as String? ?? '',
|
providerId: map['provider_id'] as String? ?? '',
|
||||||
providerDisplayName: "${map['provider']['name']}".myFormat(),
|
providerDisplayName: "${map['provider']['name']}".myFormat(),
|
||||||
modelId: map['model_id'] as String? ?? '',
|
modelId: map['model_id'] as String? ?? '',
|
||||||
@@ -206,6 +212,7 @@ class OperationModel extends Equatable {
|
|||||||
return {
|
return {
|
||||||
if (id != null) 'id': id,
|
if (id != null) 'id': id,
|
||||||
'type': type,
|
'type': type,
|
||||||
|
'sub_type': subType,
|
||||||
'provider_id': providerId,
|
'provider_id': providerId,
|
||||||
'model_id': modelId,
|
'model_id': modelId,
|
||||||
'description': description,
|
'description': description,
|
||||||
|
|||||||
624
lib/features/operations/ui/operation_form_screen.dart
Normal file
624
lib/features/operations/ui/operation_form_screen.dart
Normal 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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
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/operations/blocs/operation_files_bloc.dart';
|
||||||
import 'package:image_picker/image_picker.dart';
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flux/features/operations/blocs/operation_files_bloc.dart';
|
|
||||||
|
|
||||||
class OperationMobileUploadScreen extends StatefulWidget {
|
class OperationMobileUploadScreen extends StatefulWidget {
|
||||||
final String operationId;
|
final String operationId;
|
||||||
@@ -16,7 +16,7 @@ import 'package:flux/core/data/core_repository.dart';
|
|||||||
import 'package:flux/core/routes/app_router.dart';
|
import 'package:flux/core/routes/app_router.dart';
|
||||||
import 'package:flux/core/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
import 'package:flux/core/theme/bloc/theme_bloc.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/customers/data/customer_repository.dart';
|
||||||
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
|
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
|
||||||
import 'package:flux/features/master_data/products/data/product_repository.dart';
|
import 'package:flux/features/master_data/products/data/product_repository.dart';
|
||||||
@@ -48,7 +48,7 @@ void main() async {
|
|||||||
|
|
||||||
// Cubit delle feature
|
// Cubit delle feature
|
||||||
BlocProvider<StoreCubit>(create: (_) => StoreCubit()),
|
BlocProvider<StoreCubit>(create: (_) => StoreCubit()),
|
||||||
BlocProvider<CustomerCubit>(create: (_) => CustomerCubit()),
|
BlocProvider<CustomersCubit>(create: (_) => CustomersCubit()),
|
||||||
BlocProvider<ProductCubit>(create: (_) => ProductCubit()),
|
BlocProvider<ProductCubit>(create: (_) => ProductCubit()),
|
||||||
BlocProvider<StaffCubit>(create: (_) => StaffCubit()),
|
BlocProvider<StaffCubit>(create: (_) => StaffCubit()),
|
||||||
BlocProvider<OperationsCubit>(create: (_) => OperationsCubit()),
|
BlocProvider<OperationsCubit>(create: (_) => OperationsCubit()),
|
||||||
|
|||||||
Reference in New Issue
Block a user