From 510d8e6f159305b5e4653e0a784cd88bb2715e87 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Thu, 9 Apr 2026 11:30:57 +0200 Subject: [PATCH] sistemato assets, finito creazione company, inizio lavoro store --- lib/core/blocs/session/session_bloc.dart | 2 + lib/core/widgets/flux_logo.dart | 7 +- lib/features/company/bloc/company_bloc.dart | 44 +-- lib/features/company/bloc/company_events.dart | 36 +- lib/features/company/bloc/company_state.dart | 19 +- .../company/data/company_repository.dart | 38 ++ .../company/models/company_model.dart | 8 +- .../company/ui/create_company_screen.dart | 327 ++++++++++-------- lib/{ui => features}/settings/settings.dart | 15 + .../settings/settings_view.dart | 2 +- .../settings/theme_settings_view.dart | 0 lib/features/store/bloc/store_bloc.dart | 31 ++ lib/features/store/bloc/store_events.dart | 16 + lib/features/store/bloc/store_state.dart | 30 ++ lib/features/store/data/store_repository.dart | 36 ++ lib/features/store/models/store_model.dart | 102 ++++++ lib/main.dart | 35 +- lib/ui/home_screen.dart | 2 +- pubspec.yaml | 5 +- 19 files changed, 524 insertions(+), 231 deletions(-) create mode 100644 lib/features/company/data/company_repository.dart rename lib/{ui => features}/settings/settings.dart (61%) rename lib/{ui => features}/settings/settings_view.dart (97%) rename lib/{ui => features}/settings/theme_settings_view.dart (100%) create mode 100644 lib/features/store/bloc/store_bloc.dart create mode 100644 lib/features/store/bloc/store_events.dart create mode 100644 lib/features/store/bloc/store_state.dart create mode 100644 lib/features/store/data/store_repository.dart create mode 100644 lib/features/store/models/store_model.dart diff --git a/lib/core/blocs/session/session_bloc.dart b/lib/core/blocs/session/session_bloc.dart index 26f7bf7..54346fc 100644 --- a/lib/core/blocs/session/session_bloc.dart +++ b/lib/core/blocs/session/session_bloc.dart @@ -1,6 +1,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flux/core/enums/enums.dart'; +import 'package:flux/features/settings/settings.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:async'; @@ -31,6 +32,7 @@ class SessionBloc extends Bloc { }); on((event, emit) async { + GetIt.I.get().setCurrentUserId(event.userId); if (event.userId == null) { emit(SessionState.unauthenticated()); return; diff --git a/lib/core/widgets/flux_logo.dart b/lib/core/widgets/flux_logo.dart index 9fd8541..c47140a 100644 --- a/lib/core/widgets/flux_logo.dart +++ b/lib/core/widgets/flux_logo.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; // Classe privata per gestire i percorsi in modo ordinato class _FluxLogoPaths { // Nota: Usa l'estensione .svg - static const String logoLight = 'assets/images/flux_logo_light.png'; - static const String logoDark = 'assets/images/flux_logo_dark.png'; + static const String logoLight = 'assets/svg/flux_logo_light.svg'; + static const String logoDark = 'assets/svg/flux_logo_dark.svg'; } /// Widget base generico per il logo FLUX in formato SVG. @@ -19,7 +20,7 @@ class _FluxLogoBase extends StatelessWidget { @override Widget build(BuildContext context) { // Usiamo SvgPicture.asset per gli SVG - return Image.asset( + return SvgPicture.asset( assetPath, width: width, height: height, diff --git a/lib/features/company/bloc/company_bloc.dart b/lib/features/company/bloc/company_bloc.dart index c385ede..0637471 100644 --- a/lib/features/company/bloc/company_bloc.dart +++ b/lib/features/company/bloc/company_bloc.dart @@ -1,42 +1,32 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flux/features/company/data/company_repository.dart'; +import 'package:flux/features/company/models/company_model.dart'; import 'package:get_it/get_it.dart'; -import 'package:supabase_flutter/supabase_flutter.dart'; part 'company_events.dart'; part 'company_state.dart'; class CompanyBloc extends Bloc { - final _supabase = GetIt.instance(); - + final CompanyRepository _repository = GetIt.I(); CompanyBloc() : super(const CompanyState(status: CompanyStatus.initial)) { - on((event, emit) async { + on((event, emit) async { emit(const CompanyState(status: CompanyStatus.loading)); - try { - // Recuperiamo l'ID utente corrente da Supabase Auth - final userId = _supabase.auth.currentUser!.id; - - await _supabase.from('company').insert({ - 'user_id': userId, - 'ragione_sociale': event.ragioneSociale, - 'partita_iva': event.partitaIva, - // Se il CF è vuoto, usa la P.IVA (logica salva-tempo per ditte individuali) - 'codice_fiscale': event.codiceFiscale.isEmpty - ? event.partitaIva - : event.codiceFiscale, - 'codice_univoco': event.codiceUnivoco, - 'indirizzo': event.indirizzo, - 'cap': event.cap, - 'citta': event.citta, - 'provincia': event.provincia, - 'company_logo': event.companyLogo, - 'is_paid': false, // Di default partono con trial/non pagato - }); - - emit(const CompanyState(status: CompanyStatus.success)); + final createdCompany = await _repository.createCompany(event.company); + emit( + state.copyWith( + status: CompanyStatus.success, + company: createdCompany, + ), + ); } catch (e) { - emit(CompanyState(status: CompanyStatus.failure, error: e.toString())); + emit( + state.copyWith( + status: CompanyStatus.failure, + errorMessage: e.toString(), + ), + ); } }); } diff --git a/lib/features/company/bloc/company_events.dart b/lib/features/company/bloc/company_events.dart index 4b88fd2..041b0fd 100644 --- a/lib/features/company/bloc/company_events.dart +++ b/lib/features/company/bloc/company_events.dart @@ -9,39 +9,11 @@ abstract class CompanyEvent extends Equatable { List get props => []; } -class SaveCompanyRequested extends CompanyEvent { - final String ragioneSociale; - final String partitaIva; - final String codiceFiscale; - final String codiceUnivoco; - final String indirizzo; - final String cap; - final String citta; - final String provincia; - final String companyLogo; +class CreateCompanyRequested extends CompanyEvent { + final CompanyModel company; - const SaveCompanyRequested({ - required this.ragioneSociale, - required this.partitaIva, - required this.codiceFiscale, - required this.codiceUnivoco, - required this.indirizzo, - required this.cap, - required this.citta, - required this.provincia, - this.companyLogo = '', // Default vuoto come da schema SQL - }); + const CreateCompanyRequested({required this.company}); @override - List get props => [ - ragioneSociale, - partitaIva, - codiceFiscale, - codiceUnivoco, - indirizzo, - cap, - citta, - provincia, - companyLogo, - ]; + List get props => [company]; } diff --git a/lib/features/company/bloc/company_state.dart b/lib/features/company/bloc/company_state.dart index 2bb1ecd..5cef955 100644 --- a/lib/features/company/bloc/company_state.dart +++ b/lib/features/company/bloc/company_state.dart @@ -4,10 +4,23 @@ enum CompanyStatus { initial, loading, success, failure } class CompanyState extends Equatable { final CompanyStatus status; - final String? error; + final String? errorMessage; + final CompanyModel? company; - const CompanyState({required this.status, this.error}); + const CompanyState({required this.status, this.errorMessage, this.company}); + + CompanyState copyWith({ + CompanyStatus? status, + String? errorMessage, + CompanyModel? company, + }) { + return CompanyState( + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + company: company ?? this.company, + ); + } @override - List get props => [status, error]; + List get props => [status, errorMessage]; } diff --git a/lib/features/company/data/company_repository.dart b/lib/features/company/data/company_repository.dart new file mode 100644 index 0000000..8416a87 --- /dev/null +++ b/lib/features/company/data/company_repository.dart @@ -0,0 +1,38 @@ +import 'package:supabase_flutter/supabase_flutter.dart'; +import '../models/company_model.dart'; + +class CompanyRepository { + final SupabaseClient _supabase = Supabase.instance.client; + + Future createCompany(CompanyModel company) async { + try { + // .select().single() trasforma la risposta nell'oggetto appena inserito + final response = await _supabase + .from('company') + .insert(company.toJson()) + .select() + .single(); + + return CompanyModel.fromJson(response); + } on PostgrestException catch (e) { + throw e.message; + } catch (e) { + throw 'Errore imprevisto durante la creazione dell\'azienda'; + } + } + + Future getCompany() async { + try { + final userId = _supabase.auth.currentUser?.id; + final response = await _supabase + .from('company') + .select() + .eq('user_id', userId as Object) + .maybeSingle(); + + return response != null ? CompanyModel.fromJson(response) : null; + } catch (e) { + return null; + } + } +} diff --git a/lib/features/company/models/company_model.dart b/lib/features/company/models/company_model.dart index 68e5ca7..84d9c5b 100644 --- a/lib/features/company/models/company_model.dart +++ b/lib/features/company/models/company_model.dart @@ -2,7 +2,7 @@ import 'package:equatable/equatable.dart'; class CompanyModel extends Equatable { final String id; - final DateTime createdAt; + final DateTime? createdAt; final String userId; final String ragioneSociale; final String indirizzo; @@ -17,8 +17,8 @@ class CompanyModel extends Equatable { final String companyLogo; const CompanyModel({ - required this.id, - required this.createdAt, + this.id = '', + this.createdAt, required this.userId, required this.ragioneSociale, required this.indirizzo, @@ -28,7 +28,7 @@ class CompanyModel extends Equatable { required this.partitaIva, required this.codiceFiscale, required this.codiceUnivoco, - required this.isPaid, + this.isPaid = false, this.paymentExpiration, this.companyLogo = '', }); diff --git a/lib/features/company/ui/create_company_screen.dart b/lib/features/company/ui/create_company_screen.dart index a343a73..a56603e 100644 --- a/lib/features/company/ui/create_company_screen.dart +++ b/lib/features/company/ui/create_company_screen.dart @@ -5,6 +5,9 @@ import 'package:flux/features/company/bloc/company_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/company/models/company_model.dart'; +import 'package:flux/features/settings/settings.dart'; +import 'package:get_it/get_it.dart'; class CreateCompanyScreen extends StatefulWidget { const CreateCompanyScreen({super.key}); @@ -18,148 +21,197 @@ class CreateCompanyScreen extends StatefulWidget { class _CreateCompanyScreenState extends State { final _formKey = GlobalKey(); - // Controller per ogni campo dello schema SQL + // Controller per i campi obbligatori final _ragioneSocialeController = TextEditingController(); - final _pIvaController = TextEditingController(); - final _cfController = TextEditingController(); - final _sdiController = TextEditingController(); final _indirizzoController = TextEditingController(); final _capController = TextEditingController(); final _cittaController = TextEditingController(); final _provinciaController = TextEditingController(); + final _pIvaController = TextEditingController(); + final _cfController = TextEditingController(); + final _univocoController = TextEditingController(); + + @override + void dispose() { + // Ricordati sempre di chiuderli! + _ragioneSocialeController.dispose(); + _indirizzoController.dispose(); + _capController.dispose(); + _cittaController.dispose(); + _provinciaController.dispose(); + _pIvaController.dispose(); + _cfController.dispose(); + _univocoController.dispose(); + super.dispose(); + } + + void _onSave() { + if (_formKey.currentState!.validate()) { + // Recuperiamo l'ID utente attuale da Supabase o dal SessionBloc + final userId = GetIt.I.get().currentUserId!; + + final company = CompanyModel( + userId: userId, + ragioneSociale: _ragioneSocialeController.text.trim(), + indirizzo: _indirizzoController.text.trim(), + cap: _capController.text.trim(), + citta: _cittaController.text.trim(), + provincia: _provinciaController.text.trim(), + partitaIva: _pIvaController.text.trim(), + codiceFiscale: _cfController.text.trim(), + codiceUnivoco: _univocoController.text.trim().toUpperCase(), + // Gli altri campi hanno i default nel modello + ); + + // Spariamo l'evento al Bloc + context.read().add(CreateCompanyRequested(company: company)); + } + } @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => CompanyBloc(), - child: Scaffold( - appBar: AppBar( - title: const Text('Configurazione Azienda'), - actions: [ - IconButton( - icon: const Icon(Icons.logout_rounded), - onPressed: () { - // Qui chiami il tuo Bloc dell'autenticazione per fare logout - // Esempio se hai un AuthBloc o SessionBloc: - context.read().add(LogoutRequested()); + return Scaffold( + appBar: AppBar( + title: const Text('Configurazione Azienda'), + actions: [ + IconButton( + icon: const Icon(Icons.logout_rounded), + onPressed: () { + // Qui chiami il tuo Bloc dell'autenticazione per fare logout + // Esempio se hai un AuthBloc o SessionBloc: + context.read().add(LogoutRequested()); - // Se vuoi solo tornare brutalmente alla login per testare il logo: - // Navigator.of(context).pushReplacementNamed('/login'); - }, - ), - ], - ), - body: BlocConsumer( - listener: (context, state) { - if (state.status == CompanyStatus.success) { - context.read().add(AppStarted()); - } - }, - builder: (context, state) { - return SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildHeader(context), - const SizedBox(height: 32), + // Se vuoi solo tornare brutalmente alla login per testare il logo: + // Navigator.of(context).pushReplacementNamed('/login'); + }, + ), + ], + ), + body: BlocConsumer( + listener: (context, state) { + if (state.status == CompanyStatus.success && state.company != null) { + // 1. Aggiorniamo la singleton con i dati reali (ID incluso) + GetIt.I.get().setCurrentCompany(state.company); - // --- SEZIONE 1: IDENTITÀ FISCALE --- - _SectionTitle(title: 'DATI FISCALI'), - const SizedBox(height: 16), - FluxTextField( - label: 'Ragione Sociale', - icon: Icons.business, - controller: _ragioneSocialeController, - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: FluxTextField( - label: 'Partita IVA', - icon: Icons.numbers, - controller: _pIvaController, - ), - ), - const SizedBox(width: 12), - Expanded( - child: FluxTextField( - label: 'Codice Fiscale', - icon: Icons.badge_outlined, - controller: _cfController, - ), - ), - ], - ), - const SizedBox(height: 16), - FluxTextField( - label: 'Codice Univoco (SDI) / PEC', - icon: Icons.send_and_archive_outlined, - controller: _sdiController, - ), + // 2. Notifichiamo il SessionBloc per cambiare pagina + context.read().add(AppStarted()); + } - const SizedBox(height: 32), - - // --- SEZIONE 2: SEDE LEGALE --- - _SectionTitle(title: 'SEDE LEGALE'), - const SizedBox(height: 16), - FluxTextField( - label: 'Indirizzo e n. civico', - icon: Icons.home_work_outlined, - controller: _indirizzoController, - ), - const SizedBox(height: 16), - Row( - children: [ - Expanded( - flex: 2, - child: FluxTextField( - label: 'Città', - icon: Icons.location_city, - controller: _cittaController, - ), - ), - const SizedBox(width: 12), - Expanded( - child: FluxTextField( - label: 'CAP', - icon: Icons.map_outlined, - controller: _capController, - ), - ), - const SizedBox(width: 12), - Expanded( - child: FluxTextField( - label: 'Prov', - icon: Icons.explore_outlined, - controller: _provinciaController, - ), - ), - ], - ), - - const SizedBox(height: 32), - - // --- SEZIONE 3: LOGO AZIENDALE --- - _SectionTitle(title: 'BRANDING'), - const SizedBox(height: 16), - _buildLogoPicker(context), - - const SizedBox(height: 48), - - // --- BOTTONE INVIO --- - _buildSubmitButton(context, state), - ], - ), + if (state.status == CompanyStatus.failure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + state.errorMessage ?? 'Errore durante il salvataggio', ), + backgroundColor: Colors.redAccent, ), ); - }, - ), + } + }, + builder: (context, state) { + return SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.all(24.0), + child: Form( + key: _formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + const SizedBox(height: 32), + + // --- SEZIONE 1: IDENTITÀ FISCALE --- + _SectionTitle(title: 'DATI FISCALI'), + const SizedBox(height: 16), + FluxTextField( + label: 'Ragione Sociale', + icon: Icons.business, + controller: _ragioneSocialeController, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: FluxTextField( + label: 'Partita IVA', + icon: Icons.numbers, + controller: _pIvaController, + ), + ), + const SizedBox(width: 12), + Expanded( + child: FluxTextField( + label: 'Codice Fiscale', + icon: Icons.badge_outlined, + controller: _cfController, + ), + ), + ], + ), + const SizedBox(height: 16), + FluxTextField( + label: 'Codice Univoco (SDI) / PEC', + icon: Icons.send_and_archive_outlined, + controller: _univocoController, + ), + + const SizedBox(height: 32), + + // --- SEZIONE 2: SEDE LEGALE --- + _SectionTitle(title: 'SEDE LEGALE'), + const SizedBox(height: 16), + FluxTextField( + label: 'Indirizzo e n. civico', + icon: Icons.home_work_outlined, + controller: _indirizzoController, + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + flex: 2, + child: FluxTextField( + label: 'Città', + icon: Icons.location_city, + controller: _cittaController, + ), + ), + const SizedBox(width: 12), + Expanded( + child: FluxTextField( + label: 'CAP', + icon: Icons.map_outlined, + controller: _capController, + ), + ), + const SizedBox(width: 12), + Expanded( + child: FluxTextField( + label: 'Prov', + icon: Icons.explore_outlined, + controller: _provinciaController, + ), + ), + ], + ), + + const SizedBox(height: 32), + + // --- SEZIONE 3: LOGO AZIENDALE --- + _SectionTitle(title: 'BRANDING'), + const SizedBox(height: 16), + _buildLogoPicker(context), + + const SizedBox(height: 48), + + // --- BOTTONE INVIO --- + _buildSubmitButton(context, state), + ], + ), + ), + ), + ); + }, ), ); } @@ -207,7 +259,7 @@ class _CreateCompanyScreenState extends State { child: ElevatedButton( onPressed: state.status == CompanyStatus.loading ? null - : () => _submit(context), + : () => _onSave(), child: state.status == CompanyStatus.loading ? const CircularProgressIndicator() : const Text('SALVA AZIENDA'), @@ -215,23 +267,6 @@ class _CreateCompanyScreenState extends State { ); } - void _submit(BuildContext context) { - // Qui chiameremo il Bloc passando tutti i dati raccolti dai controller - context.read().add( - SaveCompanyRequested( - ragioneSociale: _ragioneSocialeController.text, - partitaIva: _pIvaController.text, - codiceFiscale: _cfController.text, - codiceUnivoco: _sdiController.text, - indirizzo: _indirizzoController.text, - cap: _capController.text, - citta: _cittaController.text, - provincia: _provinciaController.text, - companyLogo: '', // Per ora vuoto come da accordi - ), - ); - } - Widget _buildHeader(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/ui/settings/settings.dart b/lib/features/settings/settings.dart similarity index 61% rename from lib/ui/settings/settings.dart rename to lib/features/settings/settings.dart index 308d066..ab80f3e 100644 --- a/lib/ui/settings/settings.dart +++ b/lib/features/settings/settings.dart @@ -1,9 +1,12 @@ +import 'package:flux/features/company/models/company_model.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; class AppSettings { late String _themeModeSetting; + late String? _currentUserId; late SharedPreferences _prefs; + late CompanyModel? _currentCompany; // Singleton @@ -24,4 +27,16 @@ class AppSettings { _themeModeSetting = value; _prefs.setString('theme', value); } + + String? get currentUserId => _currentUserId; + + void setCurrentUserId(String? value) { + _currentUserId = value; + } + + CompanyModel? get currentCompany => _currentCompany; + + void setCurrentCompany(CompanyModel? value) { + _currentCompany = value; + } } diff --git a/lib/ui/settings/settings_view.dart b/lib/features/settings/settings_view.dart similarity index 97% rename from lib/ui/settings/settings_view.dart rename to lib/features/settings/settings_view.dart index bf19f43..264c159 100644 --- a/lib/ui/settings/settings_view.dart +++ b/lib/features/settings/settings_view.dart @@ -1,7 +1,7 @@ // lib/ui/impostazioni/impostazioni_view.dart import 'package:flutter/material.dart'; import 'package:flux/core/theme/theme.dart'; -import 'package:flux/ui/settings/theme_settings_view.dart'; +import 'package:flux/features/settings/theme_settings_view.dart'; class SettingsView extends StatelessWidget { const SettingsView({super.key}); diff --git a/lib/ui/settings/theme_settings_view.dart b/lib/features/settings/theme_settings_view.dart similarity index 100% rename from lib/ui/settings/theme_settings_view.dart rename to lib/features/settings/theme_settings_view.dart diff --git a/lib/features/store/bloc/store_bloc.dart b/lib/features/store/bloc/store_bloc.dart new file mode 100644 index 0000000..64ac561 --- /dev/null +++ b/lib/features/store/bloc/store_bloc.dart @@ -0,0 +1,31 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flux/features/store/data/store_repository.dart'; +import 'package:flux/features/store/models/store_model.dart'; +import 'package:get_it/get_it.dart'; + +part 'store_events.dart'; +part 'store_state.dart'; + +class StoreBloc extends Bloc { + final StoreRepository _repository = GetIt.I(); + + StoreBloc() : super(const StoreState()) { + on(_onCreateStore); + } + + Future _onCreateStore( + CreateStoreRequested event, + Emitter emit, + ) async { + emit(state.copyWith(status: StoreStatus.loading)); + try { + await _repository.createStore(event.store); + emit(state.copyWith(status: StoreStatus.success)); + } catch (e) { + emit( + state.copyWith(status: StoreStatus.failure, errorMessage: e.toString()), + ); + } + } +} diff --git a/lib/features/store/bloc/store_events.dart b/lib/features/store/bloc/store_events.dart new file mode 100644 index 0000000..54206c8 --- /dev/null +++ b/lib/features/store/bloc/store_events.dart @@ -0,0 +1,16 @@ +part of 'store_bloc.dart'; + +abstract class StoreEvent extends Equatable { + const StoreEvent(); + + @override + List get props => []; +} + +class CreateStoreRequested extends StoreEvent { + final StoreModel store; + const CreateStoreRequested({required this.store}); + + @override + List get props => [store]; +} diff --git a/lib/features/store/bloc/store_state.dart b/lib/features/store/bloc/store_state.dart new file mode 100644 index 0000000..a62ae40 --- /dev/null +++ b/lib/features/store/bloc/store_state.dart @@ -0,0 +1,30 @@ +part of 'store_bloc.dart'; + +enum StoreStatus { initial, loading, success, failure } + +class StoreState extends Equatable { + final StoreStatus status; + final StoreModel? store; + final String? errorMessage; + + const StoreState({ + this.status = StoreStatus.initial, + this.store, + this.errorMessage, + }); + + StoreState copyWith({ + StoreStatus? status, + StoreModel? store, + String? errorMessage, + }) { + return StoreState( + status: status ?? this.status, + store: store ?? this.store, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [status, store, errorMessage]; +} diff --git a/lib/features/store/data/store_repository.dart b/lib/features/store/data/store_repository.dart new file mode 100644 index 0000000..96487b7 --- /dev/null +++ b/lib/features/store/data/store_repository.dart @@ -0,0 +1,36 @@ +import 'package:get_it/get_it.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import '../models/store_model.dart'; + +class StoreRepository { + final SupabaseClient _client = GetIt.I.get(); + + /// Crea un nuovo negozio associato alla compagnia dell'utente + Future createStore(StoreModel store) async { + try { + await _client.from('store').insert(store.toJson()); + } on PostgrestException catch (e) { + // Intercettiamo errori specifici del database + throw e.message; + } catch (e) { + throw 'Errore imprevisto durante la creazione del negozio: $e'; + } + } + + /// Recupera tutti i negozi di una determinata compagnia + Future> getStoresByCompany(String companyId) async { + try { + final response = await _client + .from('store') + .select() + .eq('company_id', companyId) + .order('created_at'); + + return (response as List) + .map((json) => StoreModel.fromJson(json)) + .toList(); + } catch (e) { + throw 'Errore nel recupero dei negozi'; + } + } +} diff --git a/lib/features/store/models/store_model.dart b/lib/features/store/models/store_model.dart new file mode 100644 index 0000000..1acda50 --- /dev/null +++ b/lib/features/store/models/store_model.dart @@ -0,0 +1,102 @@ +import 'package:equatable/equatable.dart'; + +class StoreModel extends Equatable { + final String? id; + final String nome; + final String companyId; + final bool isActive; + final bool isPaid; + final DateTime? paymentExpiration; + final String indirizzo; + final String cap; + final String comune; + final String provincia; + + const StoreModel({ + this.id, + required this.nome, + required this.companyId, + this.isActive = true, + this.isPaid = false, + this.paymentExpiration, + required this.indirizzo, + required this.cap, + required this.comune, + required this.provincia, + }); + + // Fondamentale per Equatable: definisce quali proprietà determinano l'uguaglianza + @override + List get props => [ + id, + nome, + companyId, + isActive, + isPaid, + paymentExpiration, + indirizzo, + cap, + comune, + provincia, + ]; + + // Il mitico copyWith per creare nuove istanze modificando solo ciò che serve + StoreModel copyWith({ + String? id, + String? nome, + String? companyId, + bool? isActive, + bool? isPaid, + DateTime? paymentExpiration, + String? indirizzo, + String? cap, + String? comune, + String? provincia, + }) { + return StoreModel( + id: id ?? this.id, + nome: nome ?? this.nome, + companyId: companyId ?? this.companyId, + isActive: isActive ?? this.isActive, + isPaid: isPaid ?? this.isPaid, + paymentExpiration: paymentExpiration ?? this.paymentExpiration, + indirizzo: indirizzo ?? this.indirizzo, + cap: cap ?? this.cap, + comune: comune ?? this.comune, + provincia: provincia ?? this.provincia, + ); + } + + factory StoreModel.fromJson(Map json) { + return StoreModel( + id: json['id'], + nome: json['nome'], + companyId: json['company_id'], + isActive: json['is_active'] ?? true, + isPaid: json['is_paid'] ?? false, + paymentExpiration: json['payment_expiration'] != null + ? DateTime.parse(json['payment_expiration']) + : null, + indirizzo: json['indirizzo'], + cap: json['cap'], + comune: json['comune'], + provincia: json['provincia'], + ); + } + + Map toJson() { + return { + if (id != null) 'id': id, + 'nome': nome, + 'company_id': companyId, + 'is_active': isActive, + 'is_paid': isPaid, + if (paymentExpiration != null) + 'payment_expiration': paymentExpiration!.toIso8601String(), + 'indirizzo': indirizzo, + 'cap': cap, + 'comune': comune, + 'provincia': provincia, + }; + } +} diff --git a/lib/main.dart b/lib/main.dart index 7472517..327ad6c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,27 +5,20 @@ import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/bloc/theme_bloc.dart'; import 'package:flux/features/auth/bloc/auth_bloc.dart'; import 'package:flux/features/auth/ui/auth_screen.dart'; +import 'package:flux/features/company/bloc/company_bloc.dart'; +import 'package:flux/features/company/data/company_repository.dart'; import 'package:flux/features/company/ui/create_company_screen.dart'; +import 'package:flux/features/store/data/store_repository.dart'; import 'package:flux/features/store/ui/create_store_screen.dart'; import 'package:flux/ui/home_screen.dart'; -import 'package:flux/ui/settings/settings.dart'; +import 'package:flux/features/settings/settings.dart'; import 'package:get_it/get_it.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - final GetIt getIt = GetIt.instance; - getIt.registerSingleton( - await SharedPreferences.getInstance(), - ); - getIt.registerSingleton(AppSettings()); - await Supabase.initialize( - url: 'https://pvqpjloswwvtfoxbkfbh.supabase.co', - anonKey: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB2cXBqbG9zd3d2dGZveGJrZmJoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ5MjkyNjgsImV4cCI6MjA5MDUwNTI2OH0.-7nitlX1pzPGscGawlIF0vhwuD_w209FUU0PxDNGm0Y', - ); - getIt.registerSingleton(Supabase.instance.client); + await setupLocator(); runApp( MultiBlocProvider( @@ -37,12 +30,30 @@ void main() async { create: (context) => SessionBloc()..add(AppStarted()), ), BlocProvider(create: (context) => AuthBloc()), + BlocProvider(create: (context) => CompanyBloc()), ], child: const FluxApp(), ), ); } +Future setupLocator() async { + final GetIt getIt = GetIt.instance; + getIt.registerSingleton( + await SharedPreferences.getInstance(), + ); + + await Supabase.initialize( + url: 'https://pvqpjloswwvtfoxbkfbh.supabase.co', + anonKey: + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB2cXBqbG9zd3d2dGZveGJrZmJoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ5MjkyNjgsImV4cCI6MjA5MDUwNTI2OH0.-7nitlX1pzPGscGawlIF0vhwuD_w209FUU0PxDNGm0Y', + ); + getIt.registerSingleton(Supabase.instance.client); + getIt.registerLazySingleton(() => AppSettings()); + getIt.registerLazySingleton(() => CompanyRepository()); + getIt.registerLazySingleton(() => StoreRepository()); +} + class FluxApp extends StatelessWidget { const FluxApp({super.key}); diff --git a/lib/ui/home_screen.dart b/lib/ui/home_screen.dart index da1e328..b689616 100644 --- a/lib/ui/home_screen.dart +++ b/lib/ui/home_screen.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/ui/anagrafiche/anagrafiche_main_view.dart'; import 'package:flux/ui/dashboard/dashboard_view.dart'; -import 'package:flux/ui/settings/settings_view.dart'; +import 'package:flux/features/settings/settings_view.dart'; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); diff --git a/pubspec.yaml b/pubspec.yaml index a5f1933..554e4eb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -26,5 +26,6 @@ dev_dependencies: flutter: uses-material-design: true -assets: - - assets/images/ \ No newline at end of file + assets: + - assets/images/ + - assets/svg/ \ No newline at end of file