diff --git a/lib/core/theme/theme.dart b/lib/core/theme/theme.dart index 10a5439..e0c362a 100644 --- a/lib/core/theme/theme.dart +++ b/lib/core/theme/theme.dart @@ -243,6 +243,11 @@ ThemeData fluxLightTheme = ThemeData( ); extension FluxThemeContext on BuildContext { + // Scorciatoie per il TextTheme + TextStyle get titleLarge => Theme.of(this).textTheme.titleLarge!; + TextStyle get titleMedium => Theme.of(this).textTheme.titleMedium!; + TextStyle get bodyMedium => Theme.of(this).textTheme.bodyMedium!; + // --- Colori del Brand --- Color get primary => Theme.of(this).colorScheme.primary; // Blu Flux Color get accent => Theme.of(this).colorScheme.secondary; // Turchese Flux diff --git a/lib/core/widgets/flux_text_field.dart b/lib/core/widgets/flux_text_field.dart index 67a42af..67b0204 100644 --- a/lib/core/widgets/flux_text_field.dart +++ b/lib/core/widgets/flux_text_field.dart @@ -12,6 +12,8 @@ class FluxTextField extends StatelessWidget { final int? minLines; final int? maxLines; final Function(String)? onSubmitted; + final int? maxLenght; + final Function(String)? onChanged; const FluxTextField({ super.key, // Usiamo super.key per Flutter moderno @@ -24,6 +26,8 @@ class FluxTextField extends StatelessWidget { this.minLines, this.maxLines = 1, this.onSubmitted, + this.maxLenght, + this.onChanged, }); @override @@ -59,6 +63,8 @@ class FluxTextField extends StatelessWidget { ), ), onSubmitted: onSubmitted, + onChanged: onChanged, + maxLength: maxLenght, ); } } diff --git a/lib/features/anagrafiche/master_data_hub_content.dart b/lib/features/anagrafiche/master_data_hub_content.dart index ecb4c97..97bc343 100644 --- a/lib/features/anagrafiche/master_data_hub_content.dart +++ b/lib/features/anagrafiche/master_data_hub_content.dart @@ -3,6 +3,7 @@ import 'package:flux/core/theme/theme.dart'; import 'package:flux/features/customers/ui/customers_content.dart'; import 'package:flux/features/products/ui/products_screen.dart'; import 'package:flux/features/staff/ui/staff_screen.dart'; +import 'package:flux/features/store/ui/stores_screen.dart'; class MasterDataHubContent extends StatelessWidget { final Function(Widget) onOpenPage; @@ -63,7 +64,7 @@ class MasterDataHubContent extends StatelessWidget { label: 'Negozi', icon: Icons.storefront_outlined, color: Colors.purple, - onTap: () {}, // Coming soon + onTap: () => onOpenPage(const StoresScreen()), // Coming soon ), ], ), diff --git a/lib/features/staff/ui/staff_screen.dart b/lib/features/staff/ui/staff_screen.dart index 35845ec..dcdb339 100644 --- a/lib/features/staff/ui/staff_screen.dart +++ b/lib/features/staff/ui/staff_screen.dart @@ -22,8 +22,8 @@ class _StaffScreenState extends State { void initState() { super.initState(); // Carichiamo subito tutto - context.read().loadAllStaff(); _selectedStoreId = context.read().state.selectedStore!.id; + context.read().loadStaffForStore(_selectedStoreId!); } @override diff --git a/lib/features/store/data/store_repository.dart b/lib/features/store/data/store_repository.dart index 746e91f..b4de907 100644 --- a/lib/features/store/data/store_repository.dart +++ b/lib/features/store/data/store_repository.dart @@ -41,6 +41,10 @@ class StoreRepository { } } + Future updateStoreStatus(String id, bool isActive) async { + await _supabase.from('store').update({'is_active': isActive}).eq('id', id); + } + // --- LOGICA DI GIUNZIONE (Staff <-> Store) --- // Recupera i membri assegnati a uno specifico negozio diff --git a/lib/features/store/ui/stores_screen.dart b/lib/features/store/ui/stores_screen.dart new file mode 100644 index 0000000..447991b --- /dev/null +++ b/lib/features/store/ui/stores_screen.dart @@ -0,0 +1,298 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_bloc.dart'; +import 'package:flux/core/theme/theme.dart'; +import 'package:flux/core/widgets/flux_text_field.dart'; +import 'package:flux/features/staff/blocs/staff_cubit.dart'; +import 'package:flux/features/store/bloc/store_bloc.dart'; +import 'package:flux/features/store/models/store_model.dart'; + +class StoresScreen extends StatefulWidget { + const StoresScreen({super.key}); + + @override + State createState() => _StoresScreenState(); +} + +class _StoresScreenState extends State { + @override + void initState() { + super.initState(); + // Carichiamo i negozi e anche lo staff (per poterlo assegnare) + context.read().add(LoadStoresRequested()); + context.read().loadAllStaff(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text("I Tuoi Negozi")), + body: BlocBuilder( + builder: (context, state) { + if (state.status == StoreStatus.loading) + return const Center(child: CircularProgressIndicator()); + + return ListView.builder( + padding: const EdgeInsets.all(16), + itemCount: state.stores.length, + itemBuilder: (context, index) { + final store = state.stores[index]; + return _buildStoreCard(store); + }, + ); + }, + ), + floatingActionButton: FloatingActionButton.extended( + onPressed: () => _openStoreForm(context), + label: const Text("Nuovo Negozio"), + icon: const Icon(Icons.store), + ), + ); + } + + Widget _buildStoreCard(StoreModel store) { + return Card( + margin: const EdgeInsets.only(bottom: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: store.isActive + ? Colors.transparent + : Colors.red.withOpacity(0.3), + ), + ), + child: Column( + children: [ + ListTile( + leading: Icon( + Icons.storefront, + color: store.isActive ? context.accent : Colors.grey, + ), + title: Text( + store.nome, + style: const TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text( + "${store.comune} (${store.provincia}) - ${store.indirizzo}", + ), + trailing: Switch( + value: store.isActive, + onChanged: (val) { + // context.read().add(ToggleStoreStatus(store.id, val)); + }, + ), + ), + const Divider(height: 1), + Padding( + padding: const EdgeInsets.all(12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Mostra quanti dipendenti ci sono (usando lo StaffCubit) + BlocBuilder( + builder: (context, staffState) { + final staffCount = + staffState.staffByStore[store.id]?.length ?? 0; + return ActionChip( + avatar: const Icon(Icons.people, size: 16), + label: Text("$staffCount Dipendenti"), + onPressed: () => _manageStoreStaff(store), + ); + }, + ), + TextButton.icon( + onPressed: () => _openStoreForm(context, store: store), + icon: const Icon(Icons.edit, size: 18), + label: const Text("Modifica"), + ), + ], + ), + ), + ], + ), + ); + } + + void _openStoreForm(BuildContext context, {StoreModel? store}) { + final nomeController = TextEditingController(text: store?.nome); + final indirizzoController = TextEditingController(text: store?.indirizzo); + final capController = TextEditingController(text: store?.cap); + final comuneController = TextEditingController(text: store?.comune); + final provinciaController = TextEditingController(text: store?.provincia); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(24)), + ), + padding: EdgeInsets.only( + top: 24, + left: 24, + right: 24, + bottom: MediaQuery.of(context).viewInsets.bottom + 24, + ), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + store == null ? "Nuovo Punto Vendita" : "Modifica Negozio", + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 24), + + // --- DATI PRINCIPALI --- + FluxTextField( + controller: nomeController, + label: "Nome Negozio (es. Flux Milano)", + icon: Icons.storefront_rounded, + keyboardType: TextInputType.text, + ), + const SizedBox(height: 16), + FluxTextField( + controller: indirizzoController, + label: "Indirizzo", + icon: Icons.map_outlined, + keyboardType: TextInputType.streetAddress, + ), + const SizedBox(height: 16), + + // --- CAP, COMUNE, PROVINCIA (In riga) --- + Row( + children: [ + Expanded( + flex: 2, + child: FluxTextField( + controller: capController, + label: "CAP", + icon: Icons.local_post_office_outlined, + keyboardType: TextInputType.number, + maxLenght: 5, + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 4, + child: FluxTextField( + controller: comuneController, + label: "Comune", + icon: Icons.location_city_rounded, + keyboardType: TextInputType.text, + ), + ), + const SizedBox(width: 8), + Expanded( + flex: 2, + child: FluxTextField( + controller: provinciaController, + label: "Prov", + icon: Icons.explore_outlined, + keyboardType: TextInputType.text, + maxLenght: 2, + onChanged: (val) => provinciaController.text = val + .toUpperCase(), // Rimpiazzo automatico in maiuscolo + // Qui potresti aggiungere un rimpiazzo automatico in maiuscolo + ), + ), + ], + ), + + const SizedBox(height: 32), + + // --- TASTO SALVA --- + SizedBox( + width: double.infinity, + height: 50, + child: ElevatedButton( + onPressed: () { + if (nomeController.text.isEmpty) return; + + final storeData = StoreModel( + id: store?.id, // Se nullo, Supabase ne crea uno nuovo + nome: nomeController.text, + indirizzo: indirizzoController.text, + cap: capController.text, + comune: comuneController.text, + provincia: provinciaController.text, + companyId: context + .read() + .state + .company! + .id, // Recuperiamo la companyId + isActive: store?.isActive ?? true, + isPaid: store?.isPaid ?? false, + paymentExpiration: store?.paymentExpiration, + ); + + // Chiamata al Bloc per il salvataggio + context.read().add( + CreateStoreRequested(store: storeData), + ); + + Navigator.pop(context); + }, + child: Text(store == null ? "CREA NEGOZIO" : "AGGIORNA DATI"), + ), + ), + ], + ), + ), + ), + ); + } + + void _manageStoreStaff(StoreModel store) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => BlocBuilder( + builder: (context, state) { + return Container( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text("Personale di ${store.nome}", style: context.titleLarge), + const SizedBox(height: 16), + // Lista di TUTTO lo staff dell'azienda con checkbox + ...state.allStaff.map((person) { + final bool isAssigned = + state.staffByStore[store.id!]?.any( + (s) => s.id == person.id, + ) ?? + false; + + return CheckboxListTile( + title: Text(person.name), + value: isAssigned, + onChanged: (selected) { + if (selected == true) { + context.read().assignMemberToStore( + person.id!, + store.id!, + ); + } else { + context.read().removeMemberFromStore( + person.id!, + store.id!, + ); + } + }, + ); + }), + ], + ), + ); + }, + ), + ); + } +}