From b8caff76368dbfc4f246c6b8b82563b784d5cf49 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sun, 12 Apr 2026 19:21:54 +0200 Subject: [PATCH] products data --- .gitignore | 2 + .../products/blocs/product_cubit.dart | 104 ++++++++++++++++++ .../products/blocs/product_state.dart | 44 ++++++++ .../products/data/product_repository.dart | 86 +++++++++++++++ lib/features/products/models/brand_model.dart | 57 ++++++++++ lib/features/products/models/model_model.dart | 70 ++++++++++++ lib/main.dart | 72 +++++++----- pubspec.lock | 8 ++ pubspec.yaml | 4 +- 9 files changed, 419 insertions(+), 28 deletions(-) create mode 100644 lib/features/products/blocs/product_cubit.dart create mode 100644 lib/features/products/blocs/product_state.dart create mode 100644 lib/features/products/data/product_repository.dart create mode 100644 lib/features/products/models/brand_model.dart create mode 100644 lib/features/products/models/model_model.dart diff --git a/.gitignore b/.gitignore index 3820a95..11eb94d 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +.env diff --git a/lib/features/products/blocs/product_cubit.dart b/lib/features/products/blocs/product_cubit.dart new file mode 100644 index 0000000..2243953 --- /dev/null +++ b/lib/features/products/blocs/product_cubit.dart @@ -0,0 +1,104 @@ +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/products/data/product_repository.dart'; +import 'package:flux/features/products/models/brand_model.dart'; +import 'package:flux/features/products/models/model_model.dart'; +import 'package:get_it/get_it.dart'; + +part 'product_state.dart'; + +class ProductCubit extends Cubit { + final ProductRepository _repository = GetIt.I(); + final SessionBloc _sessionBloc; + + ProductCubit(this._sessionBloc) : super(const ProductState()); + + // Caricamento iniziale dei Brand + Future loadBrands() async { + emit(state.copyWith(status: ProductStatus.loading)); + try { + final brands = await _repository.getBrands( + _sessionBloc.state.company!.id, + ); + emit(state.copyWith(status: ProductStatus.success, brands: brands)); + } catch (e) { + emit( + state.copyWith(status: ProductStatus.error, errorMessage: e.toString()), + ); + } + } + + // Selezione Brand e caricamento Modelli + Future selectBrand(BrandModel? brand) async { + if (brand == null) { + emit(state.copyWith(selectedBrand: null, models: [])); + return; + } + + emit(state.copyWith(status: ProductStatus.loading, selectedBrand: brand)); + try { + final models = await _repository.getModelsByBrand(brand.id!); + emit(state.copyWith(status: ProductStatus.success, models: models)); + } catch (e) { + emit( + state.copyWith(status: ProductStatus.error, errorMessage: e.toString()), + ); + } + } + + // Aggiungi/Modifica Brand + Future saveBrand(String name, {String? id}) async { + try { + final brand = BrandModel( + id: id, + name: name, + companyId: _sessionBloc.state.company!.id, + ); + await _repository.upsertBrand(brand); + await loadBrands(); // Ricarichiamo la lista aggiornata + } catch (e) { + emit( + state.copyWith(status: ProductStatus.error, errorMessage: e.toString()), + ); + } + } + + // Aggiungi/Modifica Modello + Future saveModel(String name, {String? id}) async { + if (state.selectedBrand == null) return; + + try { + final model = ModelModel( + id: id, + name: name, + brandId: state.selectedBrand!.id!, + nameWithBrand: '', // Gestito dal trigger SQL + ); + await _repository.upsertModel(model); + await selectBrand( + state.selectedBrand, + ); // Ricarichiamo i modelli del brand + } catch (e) { + emit( + state.copyWith(status: ProductStatus.error, errorMessage: e.toString()), + ); + } + } + + // Disattivazione (Soft Delete) + Future toggleStatus(String table, String id, bool currentStatus) async { + try { + await _repository.toggleActiveStatus(table, id, !currentStatus); + if (table == 'brand') { + await loadBrands(); + } else { + await selectBrand(state.selectedBrand); + } + } catch (e) { + emit( + state.copyWith(status: ProductStatus.error, errorMessage: e.toString()), + ); + } + } +} diff --git a/lib/features/products/blocs/product_state.dart b/lib/features/products/blocs/product_state.dart new file mode 100644 index 0000000..8d5ab94 --- /dev/null +++ b/lib/features/products/blocs/product_state.dart @@ -0,0 +1,44 @@ +part of 'product_cubit.dart'; + +enum ProductStatus { initial, loading, success, error } + +class ProductState extends Equatable { + final ProductStatus status; + final List brands; + final List models; + final BrandModel? selectedBrand; // Il brand attualmente selezionato + final String? errorMessage; + + const ProductState({ + this.status = ProductStatus.initial, + this.brands = const [], + this.models = const [], + this.selectedBrand, + this.errorMessage, + }); + + ProductState copyWith({ + ProductStatus? status, + List? brands, + List? models, + BrandModel? selectedBrand, + String? errorMessage, + }) { + return ProductState( + status: status ?? this.status, + brands: brands ?? this.brands, + models: models ?? this.models, + selectedBrand: selectedBrand ?? this.selectedBrand, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [ + status, + brands, + models, + selectedBrand, + errorMessage, + ]; +} diff --git a/lib/features/products/data/product_repository.dart b/lib/features/products/data/product_repository.dart new file mode 100644 index 0000000..874854f --- /dev/null +++ b/lib/features/products/data/product_repository.dart @@ -0,0 +1,86 @@ +import 'package:get_it/get_it.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; +import '../models/brand_model.dart'; +import '../models/model_model.dart'; + +class ProductRepository { + final SupabaseClient _client = GetIt.I(); + + // --- BRAND --- + + /// Recupera tutti i brand dell'azienda + Future> getBrands(String companyId) async { + try { + final response = await _client + .from('brand') + .select() + .eq('company_id', companyId) + .eq('is_active', true) + .order('name'); + + return (response as List).map((b) => BrandModel.fromJson(b)).toList(); + } catch (e) { + throw 'Errore nel recupero dei Brand'; + } + } + + /// Crea o aggiorna un brand + Future upsertBrand(BrandModel brand) async { + try { + final response = await _client + .from('brand') + .upsert(brand.toJson()) + .select() + .single(); + + return BrandModel.fromJson(response); + } catch (e) { + throw 'Errore nel salvataggio del Brand'; + } + } + + // --- MODEL --- + + /// Recupera i modelli di un brand specifico + Future> getModelsByBrand(String brandId) async { + try { + final response = await _client + .from('model') + .select() + .eq('brand_id', brandId) + .eq('is_active', true) + .order('name'); + + return (response as List).map((m) => ModelModel.fromJson(m)).toList(); + } catch (e) { + throw 'Errore nel recupero dei modelli'; + } + } + + /// Crea o aggiorna un modello + /// NOTA: name_with_brand verrà gestito dal trigger SQL che hai lanciato! + Future upsertModel(ModelModel model) async { + try { + final response = await _client + .from('model') + .upsert(model.toJson()) + .select() + .single(); + + return ModelModel.fromJson(response); + } catch (e) { + throw 'Errore nel salvataggio del modello'; + } + } + + // --- DELETE (LOGICA) --- + + /// Disattiva un brand o un modello (Soft Delete per non rompere le FK delle operazioni passate) + Future toggleActiveStatus(String table, String id, bool status) async { + try { + await _client.from(table).update({'is_active': status}).eq('id', id); + } catch (e) { + throw 'Errore durante la modifica dello stato'; + } + } +} diff --git a/lib/features/products/models/brand_model.dart b/lib/features/products/models/brand_model.dart new file mode 100644 index 0000000..5124a05 --- /dev/null +++ b/lib/features/products/models/brand_model.dart @@ -0,0 +1,57 @@ +import 'package:equatable/equatable.dart'; + +class BrandModel extends Equatable { + final String? id; + final String name; + final String companyId; + final bool isActive; + final DateTime? createdAt; + + const BrandModel({ + this.id, + required this.name, + required this.companyId, + this.isActive = true, + this.createdAt, + }); + + factory BrandModel.fromJson(Map json) { + return BrandModel( + id: json['id'] as String, + name: json['name'] as String, + companyId: json['company_id'] as String, + isActive: json['is_active'] as bool? ?? true, + createdAt: json['created_at'] != null + ? DateTime.parse(json['created_at']) + : null, + ); + } + + Map toJson() { + return { + if (id != null) 'id': id, + 'name': name, + 'company_id': companyId, + 'is_active': isActive, + }; + } + + BrandModel copyWith({ + String? id, + String? name, + String? companyId, + bool? isActive, + DateTime? createdAt, + }) { + return BrandModel( + id: id ?? this.id, + name: name ?? this.name, + companyId: companyId ?? this.companyId, + isActive: isActive ?? this.isActive, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + List get props => [id, name, companyId, isActive, createdAt]; +} diff --git a/lib/features/products/models/model_model.dart b/lib/features/products/models/model_model.dart new file mode 100644 index 0000000..4260bfd --- /dev/null +++ b/lib/features/products/models/model_model.dart @@ -0,0 +1,70 @@ +import 'package:equatable/equatable.dart'; + +class ModelModel extends Equatable { + final String? id; + final String name; + final String nameWithBrand; + final String brandId; + final bool isActive; + final DateTime? createdAt; + + const ModelModel({ + this.id, + required this.name, + required this.nameWithBrand, + required this.brandId, + this.isActive = true, + this.createdAt, + }); + + factory ModelModel.fromJson(Map json) { + return ModelModel( + id: json['id'] as String, + name: json['name'] as String, + nameWithBrand: json['name_with_brand'] as String, + brandId: json['brand_id'] as String, + isActive: json['is_active'] as bool? ?? true, + createdAt: json['created_at'] != null + ? DateTime.parse(json['created_at']) + : null, + ); + } + + Map toJson() { + return { + if (id != null) 'id': id, + 'name': name, + 'name_with_brand': nameWithBrand, + 'brand_id': brandId, + 'is_active': isActive, + }; + } + + ModelModel copyWith({ + String? id, + String? name, + String? nameWithBrand, + String? brandId, + bool? isActive, + DateTime? createdAt, + }) { + return ModelModel( + id: id ?? this.id, + name: name ?? this.name, + nameWithBrand: nameWithBrand ?? this.nameWithBrand, + brandId: brandId ?? this.brandId, + isActive: isActive ?? this.isActive, + createdAt: createdAt ?? this.createdAt, + ); + } + + @override + List get props => [ + id, + name, + nameWithBrand, + brandId, + isActive, + createdAt, + ]; +} diff --git a/lib/main.dart b/lib/main.dart index ad9ddaf..4e34c5b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flux/core/blocs/session/session_bloc.dart'; import 'package:flux/core/routes/app_router.dart'; import 'package:flux/core/theme/theme.dart'; @@ -9,15 +10,19 @@ import 'package:flux/features/company/bloc/company_bloc.dart'; import 'package:flux/features/company/data/company_repository.dart'; import 'package:flux/features/customers/blocs/customer_bloc.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; +import 'package:flux/features/products/blocs/product_cubit.dart'; +import 'package:flux/features/products/data/product_repository.dart'; import 'package:flux/features/store/bloc/store_bloc.dart'; import 'package:flux/features/store/data/store_repository.dart'; import 'package:flux/features/settings/settings.dart'; import 'package:get_it/get_it.dart'; +import 'package:go_router/go_router.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); + await dotenv.load(fileName: ".env"); await setupLocator(); runApp( @@ -29,10 +34,6 @@ void main() async { BlocProvider( create: (context) => SessionBloc()..add(AppStarted()), ), - BlocProvider(create: (context) => AuthBloc()), - BlocProvider(create: (context) => CompanyBloc()), - BlocProvider(create: (context) => StoreBloc()), - BlocProvider(create: (context) => CustomerBloc()), ], child: const FluxApp(), ), @@ -46,41 +47,58 @@ Future setupLocator() async { ); await Supabase.initialize( - url: 'https://pvqpjloswwvtfoxbkfbh.supabase.co', - anonKey: - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InB2cXBqbG9zd3d2dGZveGJrZmJoIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ5MjkyNjgsImV4cCI6MjA5MDUwNTI2OH0.-7nitlX1pzPGscGawlIF0vhwuD_w209FUU0PxDNGm0Y', + url: dotenv.env['SUPABASE_URL'] ?? '', + anonKey: dotenv.env['SUPABASE_ANON_KEY'] ?? '', ); getIt.registerSingleton(Supabase.instance.client); getIt.registerLazySingleton(() => AppSettings()); getIt.registerLazySingleton(() => CompanyRepository()); getIt.registerLazySingleton(() => StoreRepository()); getIt.registerLazySingleton(() => CustomerRepository()); + getIt.registerLazySingleton(() => ProductRepository()); } -class FluxApp extends StatelessWidget { +class FluxApp extends StatefulWidget { const FluxApp({super.key}); @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - // Creiamo il router passando il SessionBloc che è già nell'albero grazie al MultiBlocProvider - final router = AppRouter.createRouter(context.read()); + State createState() => _FluxAppState(); +} - return BlocBuilder( - builder: (context, state) { - return MaterialApp.router( - // <--- Diventa .router - title: 'FLUX Gestionale', - debugShowCheckedModeBanner: false, - theme: fluxLightTheme, - darkTheme: fluxDarkTheme, - themeMode: state.currentTheme.themeMode, - routerConfig: router, // <--- Configurazione GoRouter - ); - }, - ); - }, +class _FluxAppState extends State { + late final GoRouter _router; + + @override + void initState() { + super.initState(); + // Lo creiamo una volta sola all'avvio dell'app + _router = AppRouter.createRouter(context.read()); + } + + @override + Widget build(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider(create: (_) => AuthBloc()), + BlocProvider(create: (_) => CompanyBloc()), + BlocProvider(create: (_) => StoreBloc()), + BlocProvider(create: (_) => CustomerBloc()), + BlocProvider( + create: (context) => ProductCubit(context.read()), + ), + ], + child: BlocBuilder( + builder: (context, state) { + return MaterialApp.router( + title: 'FLUX Gestionale', + debugShowCheckedModeBanner: false, + theme: fluxLightTheme, + darkTheme: fluxDarkTheme, + themeMode: state.currentTheme.themeMode, + routerConfig: _router, // Usa l'istanza mantenuta nello stato + ); + }, + ), ); } } diff --git a/pubspec.lock b/pubspec.lock index 4f159cf..e59aba5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -190,6 +190,14 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_dotenv: + dependency: "direct main" + description: + name: flutter_dotenv + sha256: d4130c4a43e0b13fefc593bc3961f2cb46e30cb79e253d4a526b1b5d24ae1ce4 + url: "https://pub.dev" + source: hosted + version: "6.0.0" flutter_lints: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index cc84a06..7c35a07 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: flutter: sdk: flutter flutter_bloc: ^9.1.1 + flutter_dotenv: ^6.0.0 flutter_svg: ^2.2.4 get_it: ^9.2.1 go_router: ^17.2.0 @@ -30,4 +31,5 @@ flutter: assets: - assets/images/ - - assets/svg/ \ No newline at end of file + - assets/svg/ + - .env \ No newline at end of file