new operation form almost ready

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-05-03 10:08:57 +02:00
parent 40ca1a9160
commit 4580173edf
15 changed files with 578 additions and 61 deletions

View File

@@ -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,

View File

@@ -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();

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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),

View File

@@ -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),
), ),
], ],

View File

@@ -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);
} }
} }

View File

@@ -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(

View File

@@ -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(),
); );

View File

@@ -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,

View File

@@ -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,

View File

@@ -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),
], ],
); );

View File

@@ -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,
);
}
} }

View File

@@ -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);
},
);
},
);
},
),
),
],
);
},
);
},
);
}
} }

View File

@@ -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()),