From 4a3230419b403bbd539fdfa57f0204f8ad9ca05a Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Thu, 16 Apr 2026 12:13:18 +0200 Subject: [PATCH 1/4] start provider screens --- .../providers/blocs/provider_cubit.dart | 17 +- .../providers/models/provider_model.dart | 12 +- .../providers/ui/provider_form_sheet.dart | 159 ++++++++++++++++++ .../ui/providers_master_data_screen.dart | 99 +++++++++++ lib/main.dart | 6 + 5 files changed, 284 insertions(+), 9 deletions(-) create mode 100644 lib/features/master_data/providers/ui/provider_form_sheet.dart create mode 100644 lib/features/master_data/providers/ui/providers_master_data_screen.dart diff --git a/lib/features/master_data/providers/blocs/provider_cubit.dart b/lib/features/master_data/providers/blocs/provider_cubit.dart index 07ea2d7..517ddb1 100644 --- a/lib/features/master_data/providers/blocs/provider_cubit.dart +++ b/lib/features/master_data/providers/blocs/provider_cubit.dart @@ -1,6 +1,8 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_bloc.dart'; import 'package:flux/features/master_data/providers/data/provider_repository.dart'; +import 'package:flux/features/master_data/store/models/store_model.dart'; import 'package:get_it/get_it.dart'; import '../models/provider_model.dart'; @@ -43,18 +45,21 @@ class ProvidersState extends Equatable { class ProvidersCubit extends Cubit { final ProviderRepository _repository = GetIt.I(); + final SessionBloc _sessionBloc; - ProvidersCubit() : super(const ProvidersState()); + ProvidersCubit(this._sessionBloc) : super(const ProvidersState()); // Carica i provider della company e quelli associati a uno store specifico - Future loadProviders(String companyId, String? storeId) async { + Future loadProviders(StoreModel? store) async { emit(state.copyWith(isLoading: true)); try { - final all = await _repository.fetchAllCompanyProviders(companyId); + final all = await _repository.fetchAllCompanyProviders( + _sessionBloc.state.company!.id, + ); List associated = []; - if (storeId != null) { - associated = await _repository.fetchAssociatedProviderIds(storeId); + if (store != null) { + associated = await _repository.fetchAssociatedProviderIds(store.id!); } emit( @@ -105,7 +110,7 @@ class ProvidersCubit extends Cubit { try { await _repository.saveProvider(provider); // Ricarichiamo la lista per vedere le modifiche - await loadProviders(provider.companyId, null); + await loadProviders(null); } catch (e) { emit(state.copyWith(isLoading: false, errorMessage: e.toString())); } diff --git a/lib/features/master_data/providers/models/provider_model.dart b/lib/features/master_data/providers/models/provider_model.dart index 55cb4f8..62d60f2 100644 --- a/lib/features/master_data/providers/models/provider_model.dart +++ b/lib/features/master_data/providers/models/provider_model.dart @@ -1,7 +1,7 @@ import 'package:equatable/equatable.dart'; class ProviderModel extends Equatable { - final String id; + final String? id; final String nome; final bool telefoniaFissa; final bool telefoniaMobile; @@ -13,7 +13,7 @@ class ProviderModel extends Equatable { final String companyId; const ProviderModel({ - required this.id, + this.id, required this.nome, required this.telefoniaFissa, required this.telefoniaMobile, @@ -41,7 +41,7 @@ class ProviderModel extends Equatable { } Map toMap() { - return { + final map = { 'nome': nome, 'telefonia_fissa': telefoniaFissa, 'telefonia_mobile': telefoniaMobile, @@ -52,6 +52,12 @@ class ProviderModel extends Equatable { 'is_active': isActive, 'company_id': companyId, }; + // AGGIUNGIAMO L'ID SOLO SE NON È NULLO + // Senza questo, l'upsert non sa dove andare a parare + if (id != null) { + map['id'] = id!; + } + return map; } @override diff --git a/lib/features/master_data/providers/ui/provider_form_sheet.dart b/lib/features/master_data/providers/ui/provider_form_sheet.dart new file mode 100644 index 0000000..35a41b7 --- /dev/null +++ b/lib/features/master_data/providers/ui/provider_form_sheet.dart @@ -0,0 +1,159 @@ +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'; + +class ProviderFormSheet extends StatefulWidget { + final ProviderModel? initialProvider; + + const ProviderFormSheet({super.key, this.initialProvider}); + + @override + State createState() => _ProviderFormSheetState(); +} + +class _ProviderFormSheetState extends State { + late TextEditingController _nameController; + late bool _telefoniaFissa; + late bool _telefoniaMobile; + late bool _energia; + late bool _assicurazioni; + late bool _intrattenimento; + late bool _altro; + late bool _isActive; + + @override + void initState() { + super.initState(); + final p = widget.initialProvider; + _nameController = TextEditingController(text: p?.nome ?? ''); + _telefoniaFissa = p?.telefoniaFissa ?? false; + _telefoniaMobile = p?.telefoniaMobile ?? false; + _energia = p?.energia ?? false; + _assicurazioni = p?.assicurazioni ?? false; + _intrattenimento = p?.intrattenimento ?? false; + _altro = p?.altro ?? false; + _isActive = p?.isActive ?? true; + } + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + void _save() { + if (_nameController.text.trim().isEmpty) return; + + final provider = ProviderModel( + id: widget.initialProvider?.id, // Se nullo, Supabase farà insert + nome: _nameController.text.trim(), + telefoniaFissa: _telefoniaFissa, + telefoniaMobile: _telefoniaMobile, + energia: _energia, + assicurazioni: _assicurazioni, + intrattenimento: _intrattenimento, + altro: _altro, + isActive: _isActive, + companyId: + '', // Verrà ignorato dal toMap e gestito dal Cubit/SessionBloc se hai messo la logica lì + ); + + context.read().saveProvider(provider); + Navigator.pop(context); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of( + context, + ).viewInsets.bottom, // Gestisce la tastiera + left: 16, + right: 16, + top: 16, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + widget.initialProvider == null + ? "Nuovo Provider" + : "Modifica Provider", + style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + TextField( + controller: _nameController, + decoration: const InputDecoration( + labelText: "Nome Gestore/Brand", + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + const Text( + "Servizi Abilitati", + style: TextStyle(fontWeight: FontWeight.bold), + ), + _buildSwitch( + "Energia (Luce/Gas)", + _energia, + (v) => setState(() => _energia = v), + ), + _buildSwitch( + "Telefonia Fissa", + _telefoniaFissa, + (v) => setState(() => _telefoniaFissa = v), + ), + _buildSwitch( + "Telefonia Mobile", + _telefoniaMobile, + (v) => setState(() => _telefoniaMobile = v), + ), + _buildSwitch( + "Assicurazioni", + _assicurazioni, + (v) => setState(() => _assicurazioni = v), + ), + _buildSwitch( + "Intrattenimento", + _intrattenimento, + (v) => setState(() => _intrattenimento = v), + ), + _buildSwitch( + "Altro/Accessori", + _altro, + (v) => setState(() => _altro = v), + ), + const Divider(), + _buildSwitch( + "Stato Attivo", + _isActive, + (v) => setState(() => _isActive = v), + ), + const SizedBox(height: 24), + ElevatedButton( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(50), + ), + onPressed: _save, + child: const Text("SALVA ANAGRAFICA"), + ), + const SizedBox(height: 24), + ], + ), + ), + ); + } + + Widget _buildSwitch(String title, bool value, Function(bool) onChanged) { + return SwitchListTile( + title: Text(title), + value: value, + onChanged: onChanged, + contentPadding: EdgeInsets.zero, + ); + } +} diff --git a/lib/features/master_data/providers/ui/providers_master_data_screen.dart b/lib/features/master_data/providers/ui/providers_master_data_screen.dart new file mode 100644 index 0000000..7ea2325 --- /dev/null +++ b/lib/features/master_data/providers/ui/providers_master_data_screen.dart @@ -0,0 +1,99 @@ +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/master_data/providers/ui/provider_form_sheet.dart'; + +class ProvidersMasterDataScreen extends StatefulWidget { + const ProvidersMasterDataScreen({super.key}); + + @override + State createState() => + _ProvidersMasterDataScreenState(); +} + +class _ProvidersMasterDataScreenState extends State { + @override + void initState() { + super.initState(); + // Carichiamo i provider della company (senza store specifico per ora) + context.read().loadProviders(null); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("Anagrafica Provider")), + body: BlocBuilder( + builder: (context, state) { + if (state.isLoading && state.allProviders.isEmpty) { + return const Center(child: CircularProgressIndicator()); + } + + return ListView.separated( + itemCount: state.allProviders.length, + separatorBuilder: (context, index) => const Divider(height: 1), + itemBuilder: (context, index) { + final provider = state.allProviders[index]; + return ListTile( + leading: CircleAvatar( + backgroundColor: provider.isActive + ? Colors.green.shade100 + : Colors.grey.shade300, + child: Icon( + Icons.business, + color: provider.isActive ? Colors.green : Colors.grey, + ), + ), + title: Text( + provider.nome, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: _buildProviderBadges(provider), + trailing: const Icon(Icons.edit_outlined), + onTap: () => _showProviderForm(context, provider), + ); + }, + ); + }, + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _showProviderForm(context, null), + child: const Icon(Icons.add), + ), + ); + } + + // Visualizza i servizi abilitati per quel provider nella lista + Widget _buildProviderBadges(ProviderModel p) { + return Wrap( + spacing: 4, + children: [ + if (p.telefoniaFissa || p.telefoniaMobile) + _smallTag("📞 Tel", Colors.blue), + if (p.energia) _smallTag("⚡ Energy", Colors.orange), + if (p.assicurazioni) _smallTag("🛡️ Assic", Colors.teal), + if (p.intrattenimento) _smallTag("📺 Ent", Colors.red), + if (p.altro) _smallTag("📦 Altro", Colors.grey), + ], + ); + } + + Widget _smallTag(String label, Color color) { + return Text( + label, + style: TextStyle(color: color, fontSize: 10, fontWeight: FontWeight.w600), + ); + } + + // DIALOG PER INSERIMENTO/MODIFICA + void _showProviderForm(BuildContext context, ProviderModel? provider) { + // Implementeremo qui il form con i vari SwitchListTile + // Per ora facciamo un segnaposto o passiamo a scriverlo seriamente + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (_) => ProviderFormSheet(initialProvider: provider), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 147acc4..2a92bdf 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -14,6 +14,8 @@ import 'package:flux/features/customers/blocs/customer_bloc.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; import 'package:flux/features/master_data/products/data/product_repository.dart'; +import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; +import 'package:flux/features/master_data/providers/data/provider_repository.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:flux/features/master_data/staff/data/staff_repository.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; @@ -64,6 +66,7 @@ Future setupLocator() async { getIt.registerLazySingleton(() => ProductRepository()); getIt.registerLazySingleton(() => StaffRepository()); getIt.registerLazySingleton(() => ServicesRepository()); + getIt.registerLazySingleton(() => ProviderRepository()); } class FluxApp extends StatefulWidget { @@ -103,6 +106,9 @@ class _FluxAppState extends State { BlocProvider( create: (_) => ServicesCubit(context.read()), ), + BlocProvider( + create: (_) => ProvidersCubit(context.read()), + ), ], child: BlocBuilder( builder: (context, state) { -- 2.43.0 From c7eedba5ac280c28022f2b711699a7e529dab92c Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Thu, 16 Apr 2026 12:19:18 +0200 Subject: [PATCH 2/4] ok --- lib/main.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 2a92bdf..e45f98b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -- 2.43.0 From aa18b7dd1f6bcf8c5469d22c83ea2ba7b49bfd4a Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Thu, 16 Apr 2026 19:25:42 +0200 Subject: [PATCH 3/4] Add Providers Management to Master Data Hub and Enhance Provider Model --- .../master_data/master_data_hub_content.dart | 120 ++++++++++++------ .../providers/blocs/provider_cubit.dart | 5 +- .../providers/data/provider_repository.dart | 23 +++- .../providers/models/provider_model.dart | 10 ++ .../ui/providers_master_data_screen.dart | 76 ++++++++++- 5 files changed, 188 insertions(+), 46 deletions(-) diff --git a/lib/features/master_data/master_data_hub_content.dart b/lib/features/master_data/master_data_hub_content.dart index a55d2ac..03082d8 100644 --- a/lib/features/master_data/master_data_hub_content.dart +++ b/lib/features/master_data/master_data_hub_content.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/features/customers/ui/customers_content.dart'; import 'package:flux/features/master_data/products/ui/products_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/store/ui/stores_screen.dart'; @@ -38,34 +39,57 @@ class MasterDataHubContent extends StatelessWidget { mainAxisSpacing: 16, crossAxisSpacing: 16, children: [ - _HubCard( - label: 'Prodotti', + _buildHubCard( + context, + title: 'Prodotti', + subtitle: 'Anagrafica di Marche e Modelli', icon: Icons.inventory_2_outlined, color: Colors.blue, onTap: () => onOpenPage( const ProductsScreen(), ), // Apre ProductsScreen, // Indice per ProductsScreen ), - _HubCard( - label: 'Clienti', + _buildHubCard( + context, + title: 'Clienti', + subtitle: 'Anagrafica dei clienti del tuo business', icon: Icons.people_outlined, color: Colors.orange, onTap: () => onOpenPage( const CustomersContent(), ), // Indice per CustomersContent ), - _HubCard( - label: 'Commessi', + _buildHubCard( + context, + title: 'Addetti', + subtitle: 'Anagrafica del personale e dei collaboratori', icon: Icons.badge_outlined, color: Colors.teal, onTap: () => onOpenPage(const StaffScreen()), ), - _HubCard( - label: 'Negozi', + _buildHubCard( + context, + title: 'Negozi', + subtitle: 'Anagrafica punti vendita della tua azienda', icon: Icons.storefront_outlined, color: Colors.purple, onTap: () => onOpenPage(const StoresScreen()), ), + _buildHubCard( + context, + title: 'Gestione Provider', + subtitle: 'Anagrafica mandati e servizi abilitati', + icon: Icons.handshake_rounded, + color: Colors.indigo, + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => const ProvidersMasterDataScreen(), + ), + ); + }, + ), ], ), ), @@ -75,41 +99,57 @@ class MasterDataHubContent extends StatelessWidget { } } -// Widget semplice per le card dell'hub -class _HubCard extends StatelessWidget { - final String label; - final IconData icon; - final Color color; - final VoidCallback onTap; - - const _HubCard({ - required this.label, - required this.icon, - required this.color, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Card( - elevation: 0, - color: context.background, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: BorderSide(color: context.accent.withValues(alpha: 0.1)), - ), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(16), +Widget _buildHubCard( + BuildContext context, { + required String title, + required String subtitle, + required IconData icon, + required Color color, + required VoidCallback onTap, +}) { + return Card( + clipBehavior: Clip.antiAlias, + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), // Un pelo più arrotondato + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all( + 24.0, + ), // Aumentiamo il padding per dare respiro child: Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, // CENTRA VERTICALMENTE + crossAxisAlignment: + CrossAxisAlignment.center, // CENTRA ORIZZONTALMENTE children: [ - Icon(icon, color: color, size: 40), - const SizedBox(height: 12), - Text(label, style: const TextStyle(fontWeight: FontWeight.bold)), + // Icona con un leggero sfondo circolare opaco per farla risaltare + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), // <--- API moderna + shape: BoxShape.circle, + ), + child: Icon(icon, size: 48, color: color), + ), + const SizedBox(height: 16), + Text( + title, + textAlign: TextAlign.center, // Centra il testo + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + subtitle, + textAlign: TextAlign.center, // Centra il sottotitolo + style: TextStyle(fontSize: 13, color: Colors.grey.shade500), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), ], ), ), - ); - } + ), + ); } diff --git a/lib/features/master_data/providers/blocs/provider_cubit.dart b/lib/features/master_data/providers/blocs/provider_cubit.dart index 517ddb1..a51ff6d 100644 --- a/lib/features/master_data/providers/blocs/provider_cubit.dart +++ b/lib/features/master_data/providers/blocs/provider_cubit.dart @@ -108,7 +108,10 @@ class ProvidersCubit extends Cubit { Future saveProvider(ProviderModel provider) async { emit(state.copyWith(isLoading: true)); try { - await _repository.saveProvider(provider); + final providerWithCompanyId = provider.copyWith( + companyId: _sessionBloc.state.company!.id, + ); + await _repository.saveProvider(providerWithCompanyId); // Ricarichiamo la lista per vedere le modifiche await loadProviders(null); } catch (e) { diff --git a/lib/features/master_data/providers/data/provider_repository.dart b/lib/features/master_data/providers/data/provider_repository.dart index 46bb48a..c155133 100644 --- a/lib/features/master_data/providers/data/provider_repository.dart +++ b/lib/features/master_data/providers/data/provider_repository.dart @@ -40,14 +40,29 @@ class ProviderRepository { // Recupera tutti i provider di una company (per la lista generale) Future> fetchAllCompanyProviders(String companyId) async { try { + // La magia è qui: selezioniamo tutto e chiediamo il conteggio (count) + // della tabella pivot providers_in_stores final response = await _supabase .from('provider') - .select() - .eq('company_id', companyId); + .select(''' + *, + providers_in_stores(count) + ''') + .eq('company_id', companyId) + .order('nome'); - return (response as List).map((m) => ProviderModel.fromMap(m)).toList(); + return (response as List).map((m) { + // Estraiamo il conteggio dalla struttura restituita da Supabase + // La risposta per ogni riga sarà tipo: { "id": "...", "providers_in_stores": [{"count": 5}] } + final storesList = m['providers_in_stores'] as List?; + final count = (storesList != null && storesList.isNotEmpty) + ? storesList[0]['count'] as int + : 0; + + return ProviderModel.fromMap(m).copyWith(storesCount: count); + }).toList(); } catch (e) { - throw Exception('Errore fetch provider: $e'); + throw Exception('Errore fetch all providers: $e'); } } diff --git a/lib/features/master_data/providers/models/provider_model.dart b/lib/features/master_data/providers/models/provider_model.dart index 62d60f2..716bdc3 100644 --- a/lib/features/master_data/providers/models/provider_model.dart +++ b/lib/features/master_data/providers/models/provider_model.dart @@ -11,6 +11,7 @@ class ProviderModel extends Equatable { final bool altro; final bool isActive; final String companyId; + final int storesCount; const ProviderModel({ this.id, @@ -23,6 +24,7 @@ class ProviderModel extends Equatable { required this.altro, required this.isActive, required this.companyId, + this.storesCount = 0, // Numero di store associati, default a 0 }); factory ProviderModel.fromMap(Map map) { @@ -37,6 +39,11 @@ class ProviderModel extends Equatable { altro: map['altro'] ?? false, isActive: map['is_active'] ?? true, companyId: map['company_id'], + storesCount: + map['providers_in_stores'] != null && + map['providers_in_stores'].isNotEmpty + ? map['providers_in_stores'][0]['count'] as int + : 0, // Assumiamo che l'API possa restituire questo campo ); } @@ -72,6 +79,7 @@ class ProviderModel extends Equatable { altro, isActive, companyId, + storesCount, ]; ProviderModel copyWith({ @@ -85,6 +93,7 @@ class ProviderModel extends Equatable { bool? altro, bool? isActive, String? companyId, + int? storesCount, }) { return ProviderModel( id: id ?? this.id, @@ -97,6 +106,7 @@ class ProviderModel extends Equatable { altro: altro ?? this.altro, isActive: isActive ?? this.isActive, companyId: companyId ?? this.companyId, + storesCount: storesCount ?? this.storesCount, ); } } diff --git a/lib/features/master_data/providers/ui/providers_master_data_screen.dart b/lib/features/master_data/providers/ui/providers_master_data_screen.dart index 7ea2325..39fdb9a 100644 --- a/lib/features/master_data/providers/ui/providers_master_data_screen.dart +++ b/lib/features/master_data/providers/ui/providers_master_data_screen.dart @@ -30,6 +30,54 @@ class _ProvidersMasterDataScreenState extends State { return const Center(child: CircularProgressIndicator()); } + if (state.allProviders.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(32.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // Un'icona grande e stilizzata + Icon( + Icons.handshake_outlined, + size: 80, + color: Colors.indigo.withValues(alpha: 0.3), + ), + const SizedBox(height: 24), + const Text( + "Nessun Provider configurato", + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 12), + const Text( + "Aggiungi i partner con cui collabori (es. Enel, WindTre, ecc.) per poter gestire i servizi e i mandati nei tuoi negozi.", + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey), + ), + const SizedBox(height: 32), + // Un bel bottone centrato per chi non vuole usare il FAB in basso + ElevatedButton.icon( + onPressed: () => _showProviderForm(context, null), + icon: const Icon(Icons.add), + label: const Text("AGGIUNGI IL PRIMO PROVIDER"), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.indigo, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + ), + ), + ], + ), + ), + ); + } + return ListView.separated( itemCount: state.allProviders.length, separatorBuilder: (context, index) => const Divider(height: 1), @@ -49,7 +97,9 @@ class _ProvidersMasterDataScreenState extends State { provider.nome, style: const TextStyle(fontWeight: FontWeight.bold), ), - subtitle: _buildProviderBadges(provider), + subtitle: _buildCardSubtitle( + provider, + ), // Una funzione che costruisce il sottotitolo con i badge trailing: const Icon(Icons.edit_outlined), onTap: () => _showProviderForm(context, provider), ); @@ -64,6 +114,30 @@ class _ProvidersMasterDataScreenState extends State { ); } + Widget _buildCardSubtitle(ProviderModel provider) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildProviderBadges(provider), // I badge che abbiamo fatto prima + const SizedBox(height: 4), + BlocBuilder( + builder: (context, state) { + // Un piccolo testo che indica il numero di store associati + // Nota: Dovrai assicurarti che il Cubit carichi queste info + return Text( + "Disponibile in ${provider.storesCount} negozi", + style: TextStyle( + fontSize: 11, + color: Colors.indigo.withValues(alpha: 0.7), + ), + ); + }, + ), + _buildProviderBadges(provider), + ], + ); + } + // Visualizza i servizi abilitati per quel provider nella lista Widget _buildProviderBadges(ProviderModel p) { return Wrap( -- 2.43.0 From 11248b5bf3ded54041b364bf08ed4575ebb70b00 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Fri, 17 Apr 2026 10:12:39 +0200 Subject: [PATCH 4/4] Enhance Provider Management: Update saveProvider method to handle store associations and add temporary store selection in ProviderFormSheet --- .../providers/blocs/provider_cubit.dart | 43 +++++++++++++++--- .../providers/data/provider_repository.dart | 40 ++++++++++++++++- .../providers/ui/provider_form_sheet.dart | 44 +++++++++++++++++-- .../ui/providers_master_data_screen.dart | 12 ++++- 4 files changed, 125 insertions(+), 14 deletions(-) diff --git a/lib/features/master_data/providers/blocs/provider_cubit.dart b/lib/features/master_data/providers/blocs/provider_cubit.dart index a51ff6d..6d1c3fd 100644 --- a/lib/features/master_data/providers/blocs/provider_cubit.dart +++ b/lib/features/master_data/providers/blocs/provider_cubit.dart @@ -105,14 +105,45 @@ class ProvidersCubit extends Cubit { } // Salvataggio/Update anagrafica (nuovo o modifica) - Future saveProvider(ProviderModel provider) async { + Future saveProvider( + ProviderModel provider, + List selectedStoreIds, + ) async { + emit(state.copyWith(isLoading: true)); + // Assicuriamoci di settare la companyId prima di salvare + provider = provider.copyWith(companyId: _sessionBloc.state.company!.id); + try { + // 1. Salviamo l'anagrafica (upsert) + // Se è un nuovo provider, l'ID potrebbe essere generato qui dal DB + // Quindi carichiamo il risultato del salvataggio per avere l'ID + final response = await _repository.saveProvider(provider); + + // Assumiamo che il saveProvider restituisca l'oggetto salvato con l'ID + final pId = provider.id ?? response.id; + + // 2. Sincronizziamo i negozi + await _repository.syncProviderStores(pId!, selectedStoreIds); + + // 3. Ricarichiamo tutto + await loadProviders(null); + } catch (e) { + emit(state.copyWith(isLoading: false, errorMessage: e.toString())); + } + } + + Future saveProviderWithStores( + ProviderModel provider, + List storeIds, + ) async { emit(state.copyWith(isLoading: true)); try { - final providerWithCompanyId = provider.copyWith( - companyId: _sessionBloc.state.company!.id, - ); - await _repository.saveProvider(providerWithCompanyId); - // Ricarichiamo la lista per vedere le modifiche + // 1. Salva l'anagrafica provider + await _repository.saveProvider(provider); + + // 2. Sincronizza i negozi (la via più semplice è cancellare e reinserire + // o fare un confronto tra i presenti e i nuovi) + await _repository.syncProviderStores(provider.id!, storeIds); + await loadProviders(null); } catch (e) { emit(state.copyWith(isLoading: false, errorMessage: e.toString())); diff --git a/lib/features/master_data/providers/data/provider_repository.dart b/lib/features/master_data/providers/data/provider_repository.dart index c155133..3f21fcf 100644 --- a/lib/features/master_data/providers/data/provider_repository.dart +++ b/lib/features/master_data/providers/data/provider_repository.dart @@ -102,7 +102,43 @@ class ProviderRepository { } // Salva o aggiorna l'anagrafica del Provider - Future saveProvider(ProviderModel provider) async { - await _supabase.from('provider').upsert(provider.toMap()); + Future saveProvider(ProviderModel provider) async { + try { + // .select().single() è fondamentale per farsi restituire + // l'oggetto appena creato/aggiornato con l'ID + final response = await _supabase + .from('provider') + .upsert(provider.toMap()) + .select() + .single(); + + return ProviderModel.fromMap(response); // <--- DEVE ESSERCI IL RETURN + } catch (e) { + rethrow; // <--- Rilancia l'errore al Cubit, non ritornare null! + } + } + + Future syncProviderStores( + String providerId, + List storeIds, + ) async { + try { + // 1. Eliminiamo tutte le associazioni correnti per questo provider + await _supabase + .from('providers_in_stores') + .delete() + .eq('provider_id', providerId); + + // 2. Se ci sono nuovi store da associare, li inseriamo + if (storeIds.isNotEmpty) { + final inserts = storeIds + .map((sId) => {'provider_id': providerId, 'store_id': sId}) + .toList(); + + await _supabase.from('providers_in_stores').insert(inserts); + } + } catch (e) { + throw Exception('Errore durante la sincronizzazione store: $e'); + } } } diff --git a/lib/features/master_data/providers/ui/provider_form_sheet.dart b/lib/features/master_data/providers/ui/provider_form_sheet.dart index 35a41b7..1da2781 100644 --- a/lib/features/master_data/providers/ui/provider_form_sheet.dart +++ b/lib/features/master_data/providers/ui/provider_form_sheet.dart @@ -2,6 +2,7 @@ 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/master_data/store/bloc/store_cubit.dart'; class ProviderFormSheet extends StatefulWidget { final ProviderModel? initialProvider; @@ -21,6 +22,8 @@ class _ProviderFormSheetState extends State { late bool _intrattenimento; late bool _altro; late bool _isActive; + final List _tempSelectedStoreIds = + []; // Per gestire la selezione temporanea dei negozi @override void initState() { @@ -43,8 +46,10 @@ class _ProviderFormSheetState extends State { } void _save() { - if (_nameController.text.trim().isEmpty) return; - + if (_nameController.text.trim().isEmpty) { + return; + } + final cubit = context.read(); final provider = ProviderModel( id: widget.initialProvider?.id, // Se nullo, Supabase farà insert nome: _nameController.text.trim(), @@ -58,8 +63,7 @@ class _ProviderFormSheetState extends State { companyId: '', // Verrà ignorato dal toMap e gestito dal Cubit/SessionBloc se hai messo la logica lì ); - - context.read().saveProvider(provider); + cubit.saveProvider(provider, _tempSelectedStoreIds); Navigator.pop(context); } @@ -87,6 +91,7 @@ class _ProviderFormSheetState extends State { const SizedBox(height: 16), TextField( controller: _nameController, + keyboardType: TextInputType.name, decoration: const InputDecoration( labelText: "Nome Gestore/Brand", border: OutlineInputBorder(), @@ -133,6 +138,37 @@ class _ProviderFormSheetState extends State { _isActive, (v) => setState(() => _isActive = v), ), + const Divider(), + const Text( + "Abilita nei Negozi", + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + // Qui usiamo un BlocBuilder per prendere la lista di tutti i negozi della company + BlocBuilder( + builder: (context, storeState) { + return Column( + children: storeState.stores.map((store) { + final isAssociated = _tempSelectedStoreIds.contains( + store.id, + ); + return CheckboxListTile( + title: Text(store.nome), + value: isAssociated, + onChanged: (val) { + setState(() { + if (val == true) { + _tempSelectedStoreIds.add(store.id!); + } else { + _tempSelectedStoreIds.remove(store.id); + } + }); + }, + ); + }).toList(), + ); + }, + ), const SizedBox(height: 24), ElevatedButton( style: ElevatedButton.styleFrom( diff --git a/lib/features/master_data/providers/ui/providers_master_data_screen.dart b/lib/features/master_data/providers/ui/providers_master_data_screen.dart index 39fdb9a..d88ba22 100644 --- a/lib/features/master_data/providers/ui/providers_master_data_screen.dart +++ b/lib/features/master_data/providers/ui/providers_master_data_screen.dart @@ -3,6 +3,7 @@ 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/master_data/providers/ui/provider_form_sheet.dart'; +import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; class ProvidersMasterDataScreen extends StatefulWidget { const ProvidersMasterDataScreen({super.key}); @@ -133,7 +134,6 @@ class _ProvidersMasterDataScreenState extends State { ); }, ), - _buildProviderBadges(provider), ], ); } @@ -162,12 +162,20 @@ class _ProvidersMasterDataScreenState extends State { // DIALOG PER INSERIMENTO/MODIFICA void _showProviderForm(BuildContext context, ProviderModel? provider) { + final providersCubit = context.read(); + final storeCubit = context.read(); // Implementeremo qui il form con i vari SwitchListTile // Per ora facciamo un segnaposto o passiamo a scriverlo seriamente showModalBottomSheet( context: context, isScrollControlled: true, - builder: (_) => ProviderFormSheet(initialProvider: provider), + builder: (modalContext) => MultiBlocProvider( + providers: [ + BlocProvider.value(value: providersCubit), + BlocProvider.value(value: storeCubit), + ], + child: ProviderFormSheet(initialProvider: provider), + ), ); } } -- 2.43.0