feat-ultimi_servizi-contratti_in_scadenza #12
@@ -8,13 +8,16 @@ import 'package:flux/core/utils/extensions.dart';
|
|||||||
import 'package:flux/core/widgets/set_password_screen.dart';
|
import 'package:flux/core/widgets/set_password_screen.dart';
|
||||||
import 'package:flux/features/auth/ui/auth_screen.dart';
|
import 'package:flux/features/auth/ui/auth_screen.dart';
|
||||||
import 'package:flux/features/customers/blocs/customer_files_bloc.dart';
|
import 'package:flux/features/customers/blocs/customer_files_bloc.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_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/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/blocs/product_cubit.dart';
|
||||||
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
||||||
|
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
|
||||||
import 'package:flux/features/master_data/providers/ui/providers_master_data_screen.dart';
|
import 'package:flux/features/master_data/providers/ui/providers_master_data_screen.dart';
|
||||||
import 'package:flux/features/master_data/staff/ui/staff_screen.dart';
|
import 'package:flux/features/master_data/staff/ui/staff_screen.dart';
|
||||||
import 'package:flux/features/master_data/store/ui/stores_screen.dart';
|
import 'package:flux/features/master_data/store/ui/stores_screen.dart';
|
||||||
@@ -97,7 +100,11 @@ class AppRouter {
|
|||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'products', // Diventa /master-data/products
|
path: 'products', // Diventa /master-data/products
|
||||||
builder: (context, state) => const ProductsScreen(),
|
builder: (context, state) {
|
||||||
|
context.read<ProductsCubit>().refreshCubit();
|
||||||
|
|
||||||
|
return const ProductsScreen();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'staff', // Diventa /master-data/staff
|
path: 'staff', // Diventa /master-data/staff
|
||||||
@@ -172,6 +179,12 @@ class AppRouter {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final existingOperation = state.extra as OperationModel?;
|
final existingOperation = state.extra as OperationModel?;
|
||||||
final operationId = state.uri.queryParameters['operationId'];
|
final operationId = state.uri.queryParameters['operationId'];
|
||||||
|
context.read<CustomersCubit>().loadCustomers();
|
||||||
|
context.read<ProvidersCubit>().loadActiveProvidersForStore(
|
||||||
|
GetIt.I.get<SessionCubit>().state.currentStore!.id!,
|
||||||
|
);
|
||||||
|
context.read<ProductsCubit>().loadModels();
|
||||||
|
context.read<ProductsCubit>().loadBrands();
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) => OperationFilesBloc(
|
create: (context) => OperationFilesBloc(
|
||||||
operationId: operationId ?? existingOperation?.id,
|
operationId: operationId ?? existingOperation?.id,
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class CustomerRepository {
|
|||||||
.from('customer')
|
.from('customer')
|
||||||
.select()
|
.select()
|
||||||
.eq('company_id', companyId)
|
.eq('company_id', companyId)
|
||||||
.or('nome.ilike.%$query%,telefono.ilike.%$query%')
|
.or('name.ilike.%$query%,phone_number.ilike.%$query%')
|
||||||
.limit(10);
|
.limit(10);
|
||||||
|
|
||||||
return (response as List).map((c) => CustomerModel.fromMap(c)).toList();
|
return (response as List).map((c) => CustomerModel.fromMap(c)).toList();
|
||||||
|
|||||||
@@ -9,19 +9,17 @@ import 'package:get_it/get_it.dart';
|
|||||||
|
|
||||||
part 'product_state.dart';
|
part 'product_state.dart';
|
||||||
|
|
||||||
class ProductCubit extends Cubit<ProductState> {
|
class ProductsCubit extends Cubit<ProductState> {
|
||||||
final ProductRepository _repository = GetIt.I<ProductRepository>();
|
final ProductRepository _repository = GetIt.I<ProductRepository>();
|
||||||
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||||
|
|
||||||
ProductCubit() : super(const ProductState());
|
ProductsCubit() : super(const ProductState());
|
||||||
|
|
||||||
// Caricamento iniziale dei Brand
|
// Caricamento iniziale dei Brand
|
||||||
Future<void> loadBrands() async {
|
Future<void> loadBrands() async {
|
||||||
emit(state.copyWith(status: ProductStatus.loading));
|
emit(state.copyWith(status: ProductStatus.loading));
|
||||||
try {
|
try {
|
||||||
final brands = await _repository.getBrands(
|
final brands = await _repository.getBrands();
|
||||||
_sessionCubit.state.company!.id!,
|
|
||||||
);
|
|
||||||
emit(state.copyWith(status: ProductStatus.success, brands: brands));
|
emit(state.copyWith(status: ProductStatus.success, brands: brands));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(
|
emit(
|
||||||
@@ -30,6 +28,27 @@ class ProductCubit extends Cubit<ProductState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> loadModels() async {
|
||||||
|
emit(state.copyWith(status: ProductStatus.loading));
|
||||||
|
try {
|
||||||
|
final models = await _repository.getModels();
|
||||||
|
emit(state.copyWith(status: ProductStatus.success, models: models));
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(status: ProductStatus.error, errorMessage: e.toString()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> refreshCubit() async {
|
||||||
|
if (state.selectedBrand != null) {
|
||||||
|
await selectBrand(state.selectedBrand);
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(status: ProductStatus.initial));
|
||||||
|
await loadBrands();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Selezione Brand e caricamento Modelli
|
// Selezione Brand e caricamento Modelli
|
||||||
Future<void> selectBrand(BrandModel? brand) async {
|
Future<void> selectBrand(BrandModel? brand) async {
|
||||||
if (brand == null) {
|
if (brand == null) {
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import '../models/brand_model.dart';
|
import '../models/brand_model.dart';
|
||||||
@@ -5,16 +6,17 @@ import '../models/model_model.dart';
|
|||||||
|
|
||||||
class ProductRepository {
|
class ProductRepository {
|
||||||
final SupabaseClient _supabase = GetIt.I<SupabaseClient>();
|
final SupabaseClient _supabase = GetIt.I<SupabaseClient>();
|
||||||
|
final String _companyId = GetIt.I<SessionCubit>().state.company!.id!;
|
||||||
|
|
||||||
// --- BRAND ---
|
// --- BRAND ---
|
||||||
|
|
||||||
/// Recupera tutti i brand dell'azienda
|
/// Recupera tutti i brand dell'azienda
|
||||||
Future<List<BrandModel>> getBrands(String companyId) async {
|
Future<List<BrandModel>> getBrands() async {
|
||||||
try {
|
try {
|
||||||
final response = await _supabase
|
final response = await _supabase
|
||||||
.from('brand')
|
.from('brand')
|
||||||
.select()
|
.select()
|
||||||
.eq('company_id', companyId)
|
.eq('company_id', _companyId)
|
||||||
.eq('is_active', true)
|
.eq('is_active', true)
|
||||||
.order('name');
|
.order('name');
|
||||||
|
|
||||||
@@ -57,6 +59,19 @@ class ProductRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<ModelModel>> getModels() async {
|
||||||
|
try {
|
||||||
|
final response = await _supabase
|
||||||
|
.from('model')
|
||||||
|
.select()
|
||||||
|
.eq('is_active', true)
|
||||||
|
.order('name');
|
||||||
|
return (response as List).map((m) => ModelModel.fromJson(m)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw '$e';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Crea o aggiorna un modello
|
/// Crea o aggiorna un modello
|
||||||
/// NOTA: name_with_brand verrà gestito dal trigger SQL che hai lanciato!
|
/// NOTA: name_with_brand verrà gestito dal trigger SQL che hai lanciato!
|
||||||
Future<ModelModel> upsertModel(ModelModel model) async {
|
Future<ModelModel> upsertModel(ModelModel model) async {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class BrandSelector extends StatelessWidget {
|
|||||||
return DropdownMenuItem(value: brand, child: Text(brand.name));
|
return DropdownMenuItem(value: brand, child: Text(brand.name));
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (brand) =>
|
onChanged: (brand) =>
|
||||||
context.read<ProductCubit>().selectBrand(brand),
|
context.read<ProductsCubit>().selectBrand(brand),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ class ModelsList extends StatelessWidget {
|
|||||||
color: model.isActive ? context.accent : Colors.grey,
|
color: model.isActive ? context.accent : Colors.grey,
|
||||||
),
|
),
|
||||||
onPressed: () => context
|
onPressed: () => context
|
||||||
.read<ProductCubit>()
|
.read<ProductsCubit>()
|
||||||
.toggleStatus('model', model.id!, model.isActive),
|
.toggleStatus('model', model.id!, model.isActive),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ void _submitBrand(
|
|||||||
BrandModel? brand,
|
BrandModel? brand,
|
||||||
) {
|
) {
|
||||||
if (controller.text.trim().isNotEmpty) {
|
if (controller.text.trim().isNotEmpty) {
|
||||||
context.read<ProductCubit>().saveBrand(controller.text, id: brand?.id);
|
context.read<ProductsCubit>().saveBrand(controller.text, id: brand?.id);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ void _submitModel(
|
|||||||
ModelModel? model,
|
ModelModel? model,
|
||||||
) {
|
) {
|
||||||
if (controller.text.isNotEmpty) {
|
if (controller.text.isNotEmpty) {
|
||||||
context.read<ProductCubit>().saveModel(controller.text, id: model?.id);
|
context.read<ProductsCubit>().saveModel(controller.text, id: model?.id);
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class ProductsScreen extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// Carichiamo i brand appena la pagina viene creata
|
// Carichiamo i brand appena la pagina viene creata
|
||||||
context.read<ProductCubit>().loadBrands();
|
context.read<ProductsCubit>().loadBrands();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: context.background,
|
backgroundColor: context.background,
|
||||||
@@ -33,7 +33,7 @@ class ProductsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: BlocConsumer<ProductCubit, ProductState>(
|
body: BlocConsumer<ProductsCubit, ProductState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.status == ProductStatus.error) {
|
if (state.status == ProductStatus.error) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class _QuickProductDialogState extends State<QuickProductDialog> {
|
|||||||
|
|
||||||
setState(() => _isLoading = true);
|
setState(() => _isLoading = true);
|
||||||
|
|
||||||
final newModel = await context.read<ProductCubit>().quickCreateProduct(
|
final newModel = await context.read<ProductsCubit>().quickCreateProduct(
|
||||||
brandName: _selectedBrandName.trim(),
|
brandName: _selectedBrandName.trim(),
|
||||||
modelName: _modelCtrl.text.trim(),
|
modelName: _modelCtrl.text.trim(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ class ProviderModel extends Equatable {
|
|||||||
final bool assicurazioni;
|
final bool assicurazioni;
|
||||||
final bool intrattenimento;
|
final bool intrattenimento;
|
||||||
final bool finanziamenti;
|
final bool finanziamenti;
|
||||||
|
final bool telepass;
|
||||||
final bool altro;
|
final bool altro;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final String companyId;
|
final String companyId;
|
||||||
@@ -24,6 +25,7 @@ class ProviderModel extends Equatable {
|
|||||||
required this.assicurazioni,
|
required this.assicurazioni,
|
||||||
required this.intrattenimento,
|
required this.intrattenimento,
|
||||||
required this.finanziamenti,
|
required this.finanziamenti,
|
||||||
|
required this.telepass,
|
||||||
required this.altro,
|
required this.altro,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
required this.companyId,
|
required this.companyId,
|
||||||
@@ -51,6 +53,7 @@ class ProviderModel extends Equatable {
|
|||||||
assicurazioni: map['assicurazioni'] ?? false,
|
assicurazioni: map['assicurazioni'] ?? false,
|
||||||
intrattenimento: map['intrattenimento'] ?? false,
|
intrattenimento: map['intrattenimento'] ?? false,
|
||||||
finanziamenti: map['finanziamenti'] ?? false,
|
finanziamenti: map['finanziamenti'] ?? false,
|
||||||
|
telepass: map['telepass'] ?? false,
|
||||||
altro: map['altro'] ?? false,
|
altro: map['altro'] ?? false,
|
||||||
isActive: map['is_active'] ?? true,
|
isActive: map['is_active'] ?? true,
|
||||||
companyId: map['company_id'],
|
companyId: map['company_id'],
|
||||||
@@ -67,6 +70,7 @@ class ProviderModel extends Equatable {
|
|||||||
'assicurazioni': assicurazioni,
|
'assicurazioni': assicurazioni,
|
||||||
'intrattenimento': intrattenimento,
|
'intrattenimento': intrattenimento,
|
||||||
'finanziamenti': finanziamenti,
|
'finanziamenti': finanziamenti,
|
||||||
|
'telepass': telepass,
|
||||||
'altro': altro,
|
'altro': altro,
|
||||||
'is_active': isActive,
|
'is_active': isActive,
|
||||||
'company_id': companyId,
|
'company_id': companyId,
|
||||||
@@ -89,6 +93,7 @@ class ProviderModel extends Equatable {
|
|||||||
assicurazioni,
|
assicurazioni,
|
||||||
intrattenimento,
|
intrattenimento,
|
||||||
finanziamenti,
|
finanziamenti,
|
||||||
|
telepass,
|
||||||
altro,
|
altro,
|
||||||
isActive,
|
isActive,
|
||||||
companyId,
|
companyId,
|
||||||
@@ -104,6 +109,7 @@ class ProviderModel extends Equatable {
|
|||||||
bool? assicurazioni,
|
bool? assicurazioni,
|
||||||
bool? intrattenimento,
|
bool? intrattenimento,
|
||||||
bool? finanziamenti,
|
bool? finanziamenti,
|
||||||
|
bool? telepass,
|
||||||
bool? altro,
|
bool? altro,
|
||||||
bool? isActive,
|
bool? isActive,
|
||||||
String? companyId,
|
String? companyId,
|
||||||
@@ -118,6 +124,7 @@ class ProviderModel extends Equatable {
|
|||||||
assicurazioni: assicurazioni ?? this.assicurazioni,
|
assicurazioni: assicurazioni ?? this.assicurazioni,
|
||||||
intrattenimento: intrattenimento ?? this.intrattenimento,
|
intrattenimento: intrattenimento ?? this.intrattenimento,
|
||||||
finanziamenti: finanziamenti ?? this.finanziamenti,
|
finanziamenti: finanziamenti ?? this.finanziamenti,
|
||||||
|
telepass: telepass ?? this.telepass,
|
||||||
altro: altro ?? this.altro,
|
altro: altro ?? this.altro,
|
||||||
isActive: isActive ?? this.isActive,
|
isActive: isActive ?? this.isActive,
|
||||||
companyId: companyId ?? this.companyId,
|
companyId: companyId ?? this.companyId,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
|||||||
late bool _assicurazioni;
|
late bool _assicurazioni;
|
||||||
late bool _intrattenimento;
|
late bool _intrattenimento;
|
||||||
late bool _finanziamenti;
|
late bool _finanziamenti;
|
||||||
|
late bool _telepass;
|
||||||
late bool _altro;
|
late bool _altro;
|
||||||
late bool _isActive;
|
late bool _isActive;
|
||||||
final List<String> _tempSelectedStoreIds =
|
final List<String> _tempSelectedStoreIds =
|
||||||
@@ -40,6 +41,7 @@ class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
|||||||
_assicurazioni = p?.assicurazioni ?? false;
|
_assicurazioni = p?.assicurazioni ?? false;
|
||||||
_intrattenimento = p?.intrattenimento ?? false;
|
_intrattenimento = p?.intrattenimento ?? false;
|
||||||
_finanziamenti = p?.finanziamenti ?? false;
|
_finanziamenti = p?.finanziamenti ?? false;
|
||||||
|
_telepass = p?.telepass ?? false;
|
||||||
_altro = p?.altro ?? false;
|
_altro = p?.altro ?? false;
|
||||||
_isActive = p?.isActive ?? true;
|
_isActive = p?.isActive ?? true;
|
||||||
}
|
}
|
||||||
@@ -64,6 +66,7 @@ class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
|||||||
assicurazioni: _assicurazioni,
|
assicurazioni: _assicurazioni,
|
||||||
intrattenimento: _intrattenimento,
|
intrattenimento: _intrattenimento,
|
||||||
finanziamenti: _finanziamenti,
|
finanziamenti: _finanziamenti,
|
||||||
|
telepass: _telepass,
|
||||||
altro: _altro,
|
altro: _altro,
|
||||||
isActive: _isActive,
|
isActive: _isActive,
|
||||||
companyId:
|
companyId:
|
||||||
@@ -138,6 +141,11 @@ class _ProviderFormSheetState extends State<ProviderFormSheet> {
|
|||||||
_finanziamenti,
|
_finanziamenti,
|
||||||
(v) => setState(() => _finanziamenti = v),
|
(v) => setState(() => _finanziamenti = v),
|
||||||
),
|
),
|
||||||
|
_buildSwitch(
|
||||||
|
"Telepass",
|
||||||
|
_telepass,
|
||||||
|
(v) => setState(() => _telepass = v),
|
||||||
|
),
|
||||||
_buildSwitch(
|
_buildSwitch(
|
||||||
"Altro/Accessori",
|
"Altro/Accessori",
|
||||||
_altro,
|
_altro,
|
||||||
|
|||||||
@@ -146,6 +146,8 @@ class _ProvidersMasterDataScreenState extends State<ProvidersMasterDataScreen> {
|
|||||||
if (p.energia) _smallTag("⚡ Energy", Colors.orange),
|
if (p.energia) _smallTag("⚡ Energy", Colors.orange),
|
||||||
if (p.assicurazioni) _smallTag("🛡️ Assic", Colors.teal),
|
if (p.assicurazioni) _smallTag("🛡️ Assic", Colors.teal),
|
||||||
if (p.intrattenimento) _smallTag("📺 Ent", Colors.red),
|
if (p.intrattenimento) _smallTag("📺 Ent", Colors.red),
|
||||||
|
if (p.finanziamenti) _smallTag("💰 Fin", Colors.purple),
|
||||||
|
if (p.telepass) _smallTag("🏎️ Telepass", Colors.yellow),
|
||||||
if (p.altro) _smallTag("📦 Altro", Colors.grey),
|
if (p.altro) _smallTag("📦 Altro", Colors.grey),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -213,14 +213,19 @@ class OperationsCubit extends Cubit<OperationsState> {
|
|||||||
String? customerDisplayName,
|
String? customerDisplayName,
|
||||||
String? type,
|
String? type,
|
||||||
String? providerId,
|
String? providerId,
|
||||||
|
String? providerDisplayName,
|
||||||
String? subtype,
|
String? subtype,
|
||||||
DateTime? expirationDate,
|
DateTime? expirationDate,
|
||||||
int? quantity,
|
int? quantity,
|
||||||
|
String? modelId,
|
||||||
|
String? modelDisplayName,
|
||||||
// Aggiungiamo questi flag per forzare la pulizia dei campi quando cambi tipo
|
// Aggiungiamo questi flag per forzare la pulizia dei campi quando cambi tipo
|
||||||
bool clearProvider = false,
|
bool clearProvider = false,
|
||||||
bool clearType = false,
|
bool clearType = false,
|
||||||
bool clearSubtype = false,
|
bool clearSubtype = false,
|
||||||
bool clearExpiration = false,
|
bool clearExpiration = false,
|
||||||
|
bool clearQuantity = false,
|
||||||
|
bool clearModel = false,
|
||||||
}) {
|
}) {
|
||||||
if (state.currentOperation == null) return;
|
if (state.currentOperation == null) return;
|
||||||
|
|
||||||
@@ -231,16 +236,48 @@ class OperationsCubit extends Cubit<OperationsState> {
|
|||||||
final updated = current.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)
|
// Se clearProvider è true, forziamo una stringa vuota (o null se il tuo modello lo supporta)
|
||||||
providerId: clearProvider ? null : (providerId ?? current.providerId),
|
providerId: clearProvider ? null : (providerId ?? current.providerId),
|
||||||
// Idem per subtype e date.
|
providerDisplayName: clearProvider
|
||||||
// Se expirationDate è nullabile nel copyWith, dovresti poterlo gestire
|
? null
|
||||||
quantity: quantity ?? current.quantity,
|
: (providerDisplayName ?? current.providerDisplayName),
|
||||||
|
quantity: clearQuantity ? 1 : (quantity ?? current.quantity),
|
||||||
|
type: clearType ? null : (type ?? current.type),
|
||||||
|
subtype: clearSubtype ? null : (subtype ?? current.subtype),
|
||||||
|
expirationDate: clearExpiration
|
||||||
|
? null
|
||||||
|
: (expirationDate ?? current.expirationDate),
|
||||||
|
modelId: clearModel ? null : (modelId ?? current.modelId),
|
||||||
|
modelDisplayName: clearModel
|
||||||
|
? null
|
||||||
|
: (modelDisplayName ?? current.modelDisplayName),
|
||||||
);
|
);
|
||||||
|
|
||||||
emit(state.copyWith(currentOperation: updated));
|
emit(state.copyWith(currentOperation: updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metodo di utilità per calcolare la data X mesi da oggi
|
||||||
|
DateTime _calculateMonths(int months) {
|
||||||
|
final now = DateTime.now();
|
||||||
|
return DateTime(now.year, now.month + months, now.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quando l'utente seleziona un tipo, impostiamo il default
|
||||||
|
void setTypeWithSmartDefault(String type) {
|
||||||
|
DateTime? defaultDate;
|
||||||
|
|
||||||
|
if (type == 'Energy') defaultDate = _calculateMonths(24);
|
||||||
|
if (type == 'Fin') defaultDate = _calculateMonths(30);
|
||||||
|
if (type == 'Entertainment') defaultDate = _calculateMonths(12);
|
||||||
|
|
||||||
|
updateOperationFields(
|
||||||
|
type: type,
|
||||||
|
expirationDate: defaultDate,
|
||||||
|
clearProvider: true,
|
||||||
|
clearSubtype: true,
|
||||||
|
clearModel: true,
|
||||||
|
clearQuantity: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
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/customers_cubit.dart';
|
import 'package:flux/features/customers/blocs/customers_cubit.dart';
|
||||||
|
import 'package:flux/features/customers/ui/quick_customer_dialog.dart';
|
||||||
|
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
|
||||||
|
import 'package:flux/features/master_data/products/ui/quick_product_dialog.dart';
|
||||||
|
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
|
||||||
import 'package:flux/features/operations/blocs/operations_cubit.dart';
|
import 'package:flux/features/operations/blocs/operations_cubit.dart';
|
||||||
import 'package:flux/features/operations/models/operation_model.dart';
|
import 'package:flux/features/operations/models/operation_model.dart';
|
||||||
// import 'package:flux/features/attachments/ui/operation_files_section.dart';
|
// import 'package:flux/features/attachments/ui/operation_files_section.dart';
|
||||||
@@ -25,7 +29,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
// TEXT CONTROLLERS (Unici detentori di stato locale per evitare lag)
|
// TEXT CONTROLLERS (Unici detentori di stato locale per evitare lag)
|
||||||
final _referenceController = TextEditingController();
|
final _referenceController = TextEditingController();
|
||||||
final _noteController = TextEditingController();
|
final _noteController = TextEditingController();
|
||||||
final _customSubtypeController = TextEditingController();
|
final _freeTextSubtypeController = TextEditingController();
|
||||||
|
|
||||||
final List<String> _availableTypes = [
|
final List<String> _availableTypes = [
|
||||||
'AL',
|
'AL',
|
||||||
@@ -39,6 +43,34 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
'Custom',
|
'Custom',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
bool _doesProviderMatchOperationType(dynamic provider, String operationType) {
|
||||||
|
// Se è Custom o non riconosciuto, mostriamo tutto
|
||||||
|
if (operationType == 'Custom') return true;
|
||||||
|
|
||||||
|
// Qui mappiamo il tipo di operazione scelto con i bool del ProviderModel
|
||||||
|
switch (operationType) {
|
||||||
|
case 'AL':
|
||||||
|
return provider.telefoniaMobile == true;
|
||||||
|
case 'MNP':
|
||||||
|
return provider.telefoniaMobile == true;
|
||||||
|
case 'NIP':
|
||||||
|
return provider.telefoniaFissa == true;
|
||||||
|
case 'UNICA':
|
||||||
|
return provider.telefoniaFissa == true ||
|
||||||
|
provider.telefoniaMobile == true;
|
||||||
|
case 'Energy':
|
||||||
|
return provider.energia == true;
|
||||||
|
case 'Fin':
|
||||||
|
return provider.finanziamenti == true;
|
||||||
|
case 'Entertainment':
|
||||||
|
return provider.intrattenimento == true;
|
||||||
|
case 'TELEPASS':
|
||||||
|
return provider.telepass == true;
|
||||||
|
default:
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -55,7 +87,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
void dispose() {
|
void dispose() {
|
||||||
_referenceController.dispose();
|
_referenceController.dispose();
|
||||||
_noteController.dispose();
|
_noteController.dispose();
|
||||||
_customSubtypeController.dispose();
|
_freeTextSubtypeController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,6 +99,11 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
if (_noteController.text.isEmpty && model.note.isNotEmpty) {
|
if (_noteController.text.isEmpty && model.note.isNotEmpty) {
|
||||||
_noteController.text = model.note;
|
_noteController.text = model.note;
|
||||||
}
|
}
|
||||||
|
if (_freeTextSubtypeController.text.isEmpty &&
|
||||||
|
model.subtype != null &&
|
||||||
|
model.subtype!.isNotEmpty) {
|
||||||
|
_freeTextSubtypeController.text = model.subtype!;
|
||||||
|
}
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +133,8 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
|
|
||||||
// --- MODALE SELEZIONE CLIENTE ---
|
// --- MODALE SELEZIONE CLIENTE ---
|
||||||
void _showCustomerModal() {
|
void _showCustomerModal() {
|
||||||
|
String currentSearchQuery = '';
|
||||||
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
@@ -132,6 +171,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: TextField(
|
child: TextField(
|
||||||
|
autofocus: true,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Cerca per nome, telefono o email...',
|
hintText: 'Cerca per nome, telefono o email...',
|
||||||
prefixIcon: const Icon(Icons.search),
|
prefixIcon: const Icon(Icons.search),
|
||||||
@@ -140,8 +180,8 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
onChanged: (query) {
|
onChanged: (query) {
|
||||||
// Evento di ricerca (usa debouncer nel cubit!)
|
currentSearchQuery = query;
|
||||||
// context.read<CustomersCubit>().searchCustomers(query);
|
context.read<CustomersCubit>().searchCustomers(query);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -154,8 +194,37 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
),
|
),
|
||||||
icon: const Icon(Icons.person_add),
|
icon: const Icon(Icons.person_add),
|
||||||
label: const Text('Crea Nuovo Cliente'),
|
label: const Text('Crea Nuovo Cliente'),
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
// Apri form nuovo cliente...
|
final OperationsCubit operationsCubit = context
|
||||||
|
.read<OperationsCubit>();
|
||||||
|
|
||||||
|
// APRIAMO LA DIALOG RAPIDA CON LA MAGIA DEL BLOC PROVIDER
|
||||||
|
final newCustomer = await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: context.read<CustomersCubit>(),
|
||||||
|
child: QuickCustomerDialog(
|
||||||
|
initialQuery:
|
||||||
|
currentSearchQuery, // <-- Passiamo quello che ha digitato!
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Se l'ha creato davvero (e non ha premuto annulla)...
|
||||||
|
if (newCustomer != null) {
|
||||||
|
// 1. Aggiorniamo il form delle operazioni
|
||||||
|
operationsCubit.updateOperationFields(
|
||||||
|
customerId: newCustomer.id,
|
||||||
|
customerDisplayName: newCustomer.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. Chiudiamo la BottomSheet dei clienti per tornare alla form!
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.pop(modalContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -249,7 +318,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
// Ripuliamo SOLO i testi liberi (il Cubit gestisce già i suoi reset)
|
// Ripuliamo SOLO i testi liberi (il Cubit gestisce già i suoi reset)
|
||||||
_referenceController.clear();
|
_referenceController.clear();
|
||||||
_noteController.clear();
|
_noteController.clear();
|
||||||
_customSubtypeController.clear();
|
_freeTextSubtypeController.clear();
|
||||||
} else if (state.status == OperationsStatus.failure) {
|
} else if (state.status == OperationsStatus.failure) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
@@ -403,12 +472,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
onSelected: (selected) {
|
onSelected: (selected) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
// Diciamo al Cubit di cambiare tipo e spianare i campi dipendenti
|
// Diciamo al Cubit di cambiare tipo e spianare i campi dipendenti
|
||||||
context.read<OperationsCubit>().updateOperationFields(
|
context.read<OperationsCubit>().setTypeWithSmartDefault(type);
|
||||||
type: type,
|
|
||||||
clearProvider: true,
|
|
||||||
clearSubtype: true,
|
|
||||||
clearExpiration: true,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -423,73 +487,138 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Seleziona Gestore'),
|
title: const Text('Seleziona Gestore'),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
currentOp?.providerId ?? 'Nessun gestore selezionato',
|
(currentOp?.providerDisplayName != null &&
|
||||||
), // Adatta se hai displayName
|
currentOp!.providerDisplayName!.isNotEmpty)
|
||||||
|
? currentOp.providerDisplayName!
|
||||||
|
: 'Nessun gestore selezionato',
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
(currentOp?.providerId == null ||
|
||||||
|
currentOp!.providerId!.isEmpty)
|
||||||
|
? Colors.grey
|
||||||
|
: null,
|
||||||
|
fontWeight:
|
||||||
|
(currentOp?.providerId == null ||
|
||||||
|
currentOp!.providerId!.isEmpty)
|
||||||
|
? FontWeight.normal
|
||||||
|
: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
trailing: const Icon(Icons.arrow_drop_down),
|
trailing: const Icon(Icons.arrow_drop_down),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
side: BorderSide(color: theme.dividerColor),
|
side: BorderSide(color: theme.dividerColor),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// TODO: Modale o Dropdown Provider
|
_showProviderModal(currentType);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// SOTTO-TIPO (Reattivo)
|
// 1. SCENARIO ENERGY (Dropdown Fisso)
|
||||||
if (['Energy', 'Fin', 'Entertainment'].contains(currentType)) ...[
|
if (currentType == 'Energy') ...[
|
||||||
DropdownButtonFormField<String?>(
|
DropdownButtonFormField<String>(
|
||||||
initialValue:
|
initialValue:
|
||||||
null, // Sostituisci con currentOp?.subtype quando lo aggiungi
|
(currentOp?.subtype != null && currentOp!.subtype!.isNotEmpty)
|
||||||
decoration: const InputDecoration(
|
? currentOp.subtype
|
||||||
labelText: 'Dettaglio (es. Luce, Gas...)',
|
: null,
|
||||||
),
|
decoration: const InputDecoration(labelText: 'Dettaglio Fornitura'),
|
||||||
items: [
|
items: [
|
||||||
'Luce',
|
'Luce',
|
||||||
'Gas',
|
'Gas',
|
||||||
'Dual',
|
'Dual',
|
||||||
].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
|
].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
// context.read<OperationsCubit>().updateOperationFields(subtype: val);
|
if (val != null) {
|
||||||
|
context.read<OperationsCubit>().updateOperationFields(
|
||||||
|
subtype: val,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
|
|
||||||
// SOTTO-TIPO CUSTOM (Reattivo)
|
// 2. SCENARIO FIN (Ricerca Modello/Prodotto)
|
||||||
if (currentType == 'Custom') ...[
|
if (currentType == 'Fin') ...[
|
||||||
|
ListTile(
|
||||||
|
title: const Text('Seleziona Dispositivo/Prodotto'),
|
||||||
|
subtitle: Text(
|
||||||
|
(currentOp?.modelDisplayName != null &&
|
||||||
|
currentOp!.modelDisplayName!.isNotEmpty)
|
||||||
|
? currentOp.modelDisplayName!
|
||||||
|
: 'Nessun modello selezionato',
|
||||||
|
style: TextStyle(
|
||||||
|
color:
|
||||||
|
(currentOp?.modelId == null || currentOp!.modelId!.isEmpty)
|
||||||
|
? Colors.grey
|
||||||
|
: null,
|
||||||
|
fontWeight:
|
||||||
|
(currentOp?.modelId == null || currentOp!.modelId!.isEmpty)
|
||||||
|
? FontWeight.normal
|
||||||
|
: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.arrow_drop_down),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
side: BorderSide(color: theme.dividerColor),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
onTap: _showModelModal,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 3. SCENARIO ENTERTAINMENT O CUSTOM (Testo libero)
|
||||||
|
if (currentType == 'Entertainment' || currentType == 'Custom') ...[
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _customSubtypeController,
|
controller: _freeTextSubtypeController,
|
||||||
decoration: const InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Specifica il servizio (es. Monopattino)',
|
labelText: currentType == 'Entertainment'
|
||||||
|
? 'Piattaforma (es. Netflix, DAZN, Spotify...)'
|
||||||
|
: 'Specifica il servizio (es. Monopattino)',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
],
|
],
|
||||||
|
|
||||||
// SCADENZA (Reattivo)
|
// SCADENZA (Reattivo per tipi complessi)
|
||||||
if ([
|
if ([
|
||||||
'Energy',
|
'Energy',
|
||||||
'Fin',
|
'Fin',
|
||||||
'Entertainment',
|
'Entertainment',
|
||||||
'Custom',
|
'Custom',
|
||||||
].contains(currentType)) ...[
|
].contains(currentType)) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// --- I CHIPS RAPIDI ---
|
||||||
|
_buildDurationQuickPicks(currentOp),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// --- IL SELETTORE MANUALE ---
|
||||||
ListTile(
|
ListTile(
|
||||||
title: const Text('Data di Scadenza'),
|
title: const Text('Data di Scadenza Effettiva'),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
currentOp?.expirationDate?.toLocal().toString().split(' ')[0] ??
|
currentOp?.expirationDate != null
|
||||||
'Nessuna scadenza',
|
? "${currentOp!.expirationDate!.day}/${currentOp.expirationDate!.month}/${currentOp.expirationDate!.year}"
|
||||||
|
: 'Nessuna scadenza impostata',
|
||||||
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||||
),
|
),
|
||||||
trailing: const Icon(Icons.calendar_today),
|
trailing: const Icon(Icons.calendar_month, color: Colors.blue),
|
||||||
|
tileColor: Colors.blue.withValues(alpha: 0.05),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
side: BorderSide(color: theme.dividerColor),
|
borderRadius: BorderRadius.circular(12),
|
||||||
borderRadius: BorderRadius.circular(8),
|
side: const BorderSide(color: Colors.blue, width: 0.5),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final operationsCubit = context.read<OperationsCubit>();
|
final OperationsCubit operationsCubit = context
|
||||||
|
.read<OperationsCubit>();
|
||||||
final date = await showDatePicker(
|
final date = await showDatePicker(
|
||||||
context: context,
|
context: context,
|
||||||
initialDate: DateTime.now().add(const Duration(days: 365)),
|
initialDate:
|
||||||
|
currentOp?.expirationDate ??
|
||||||
|
DateTime.now().add(const Duration(days: 365)),
|
||||||
firstDate: DateTime.now(),
|
firstDate: DateTime.now(),
|
||||||
lastDate: DateTime.now().add(const Duration(days: 3650)),
|
lastDate: DateTime.now().add(const Duration(days: 3650)),
|
||||||
);
|
);
|
||||||
@@ -545,6 +674,50 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildDurationQuickPicks(OperationModel? currentOp) {
|
||||||
|
final durations = [3, 6, 12, 24, 30, 36, 48];
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
"Imposta durata rapida (mesi):",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: durations.map((months) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: ActionChip(
|
||||||
|
label: Text("$months m"),
|
||||||
|
backgroundColor: Colors.blue.withValues(alpha: 0.05),
|
||||||
|
onPressed: () {
|
||||||
|
final now = DateTime.now();
|
||||||
|
final newDate = DateTime(
|
||||||
|
now.year,
|
||||||
|
now.month + months,
|
||||||
|
now.day,
|
||||||
|
);
|
||||||
|
context.read<OperationsCubit>().updateOperationFields(
|
||||||
|
expirationDate: newDate,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildNotesSection({required bool isDesktop}) {
|
Widget _buildNotesSection({required bool isDesktop}) {
|
||||||
final title = _buildSectionTitle('Note Interne');
|
final title = _buildSectionTitle('Note Interne');
|
||||||
final noteField = TextFormField(
|
final noteField = TextFormField(
|
||||||
@@ -626,4 +799,247 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _showProviderModal(String currentOperationType) {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (modalContext) {
|
||||||
|
return DraggableScrollableSheet(
|
||||||
|
initialChildSize: 0.5, // Parte a metà schermo
|
||||||
|
minChildSize: 0.4,
|
||||||
|
maxChildSize: 0.8,
|
||||||
|
expand: false,
|
||||||
|
builder: (_, scrollController) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Seleziona Gestore',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () => Navigator.pop(modalContext),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Expanded(
|
||||||
|
child: BlocBuilder<ProvidersCubit, ProvidersState>(
|
||||||
|
// <--- Usa il tuo Cubit dei provider
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.isLoading) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simuliamo la lista di provider caricata dal tuo stato
|
||||||
|
final allProviders = state.activeProviders;
|
||||||
|
|
||||||
|
// Applichiamo il nostro filtro magico!
|
||||||
|
final filteredProviders = allProviders
|
||||||
|
.where(
|
||||||
|
(p) => _doesProviderMatchOperationType(
|
||||||
|
p,
|
||||||
|
currentOperationType,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
if (filteredProviders.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text(
|
||||||
|
'Nessun gestore compatibile con questo servizio.',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
itemCount: filteredProviders.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final provider = filteredProviders[index];
|
||||||
|
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.business),
|
||||||
|
title: Text(
|
||||||
|
provider.nome,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
// Selezione effettuata! Diciamo al Cubit delle operazioni di aggiornarsi
|
||||||
|
context
|
||||||
|
.read<OperationsCubit>()
|
||||||
|
.updateOperationFields(
|
||||||
|
providerId: provider.id,
|
||||||
|
providerDisplayName: provider
|
||||||
|
.nome, // Fondamentale per la UI!
|
||||||
|
);
|
||||||
|
Navigator.pop(modalContext);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MODALE SELEZIONE MODELLO (PER FINANZIAMENTI) ---
|
||||||
|
void _showModelModal() {
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (modalContext) {
|
||||||
|
return DraggableScrollableSheet(
|
||||||
|
initialChildSize: 0.6,
|
||||||
|
minChildSize: 0.4,
|
||||||
|
maxChildSize: 0.9,
|
||||||
|
expand: false,
|
||||||
|
builder: (_, scrollController) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Seleziona Modello',
|
||||||
|
style: Theme.of(context).textTheme.titleLarge,
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
onPressed: () => Navigator.pop(modalContext),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
|
child: TextField(
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Cerca modello (es. iPhone 15...)',
|
||||||
|
prefixIcon: const Icon(Icons.search),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onChanged: (query) {
|
||||||
|
context.read<ProductsCubit>().searchModels(query);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size.fromHeight(48),
|
||||||
|
),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Aggiungi Modello al Volo'),
|
||||||
|
onPressed: () async {
|
||||||
|
final OperationsCubit operationsCubit = context
|
||||||
|
.read<OperationsCubit>();
|
||||||
|
|
||||||
|
// 1. Recuperiamo la lista dei brand (adatta questo in base a dove tieni i brand nel tuo stato)
|
||||||
|
final existingBrands = context
|
||||||
|
.read<ProductsCubit>()
|
||||||
|
.state
|
||||||
|
.brands; // <-- Verifica che sia corretto!
|
||||||
|
|
||||||
|
// 2. Apriamo il tuo Dialog.
|
||||||
|
// ATTENZIONE DA CECCHINO: showDialog crea una nuova "rotta" sopra l'albero dei widget,
|
||||||
|
// quindi dobbiamo passargli il Cubit usando BlocProvider.value per non farglielo perdere!
|
||||||
|
final newModel = await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (dialogContext) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: context.read<ProductsCubit>(),
|
||||||
|
child: QuickProductDialog(
|
||||||
|
existingBrands: existingBrands,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Se l'utente ha effettivamente creato un modello e non ha premuto "Annulla"...
|
||||||
|
if (newModel != null) {
|
||||||
|
// A. Aggiorniamo il form del Cubit delle operazioni con il nuovo nato!
|
||||||
|
operationsCubit.updateOperationFields(
|
||||||
|
modelId: newModel.id,
|
||||||
|
modelDisplayName: newModel
|
||||||
|
.nameWithBrand, // <-- Verifica il nome della property
|
||||||
|
);
|
||||||
|
|
||||||
|
// B. Chiudiamo ANCHE la BottomSheet dei modelli per far tornare l'utente al form principale
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.pop(modalContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Expanded(
|
||||||
|
child: BlocBuilder<ProductsCubit, ProductState>(
|
||||||
|
// <--- Usa il tuo Cubit dei modelli!
|
||||||
|
builder: (context, state) {
|
||||||
|
return ListView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
itemCount: state
|
||||||
|
.models
|
||||||
|
.length, // Sostituisci con state.models.length
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final deviceModel = state.models[index];
|
||||||
|
return ListTile(
|
||||||
|
leading: const Icon(Icons.devices),
|
||||||
|
title: Text(
|
||||||
|
deviceModel.nameWithBrand,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
context.read<OperationsCubit>().updateOperationFields(
|
||||||
|
modelId:
|
||||||
|
'id_del_modello_$index', // deviceModel.id
|
||||||
|
// Assicurati di avere questo campo in _updateOperationFields nel Cubit!
|
||||||
|
// modelDisplayName: deviceModel.nameWithBrand,
|
||||||
|
);
|
||||||
|
Navigator.pop(modalContext);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ void main() async {
|
|||||||
// Cubit delle feature
|
// Cubit delle feature
|
||||||
BlocProvider<StoreCubit>(create: (_) => StoreCubit()),
|
BlocProvider<StoreCubit>(create: (_) => StoreCubit()),
|
||||||
BlocProvider<CustomersCubit>(create: (_) => CustomersCubit()),
|
BlocProvider<CustomersCubit>(create: (_) => CustomersCubit()),
|
||||||
BlocProvider<ProductCubit>(create: (_) => ProductCubit()),
|
BlocProvider<ProductsCubit>(create: (_) => ProductsCubit()),
|
||||||
BlocProvider<StaffCubit>(create: (_) => StaffCubit()),
|
BlocProvider<StaffCubit>(create: (_) => StaffCubit()),
|
||||||
BlocProvider<OperationsCubit>(create: (_) => OperationsCubit()),
|
BlocProvider<OperationsCubit>(create: (_) => OperationsCubit()),
|
||||||
BlocProvider<ProvidersCubit>(create: (_) => ProvidersCubit()),
|
BlocProvider<ProvidersCubit>(create: (_) => ProvidersCubit()),
|
||||||
|
|||||||
Reference in New Issue
Block a user