From b1c1866b4b30d237b8fa169a4358035ed2b1d5b4 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Wed, 29 Apr 2026 12:34:05 +0200 Subject: [PATCH 01/18] latest store services --- .../bloc/latest_store_services_bloc.dart | 48 ++++++ .../bloc/latest_store_services_events.dart | 17 ++ .../bloc/latest_store_services_state.dart | 30 ++++ .../ui/latest_store_services_card.dart | 160 ++++++++++++++++++ .../home/ui/dashboard_action_card.dart | 44 ----- .../home/ui/dashboard_adaptive_grid.dart | 75 -------- lib/features/home/ui/dashboard_content.dart | 125 -------------- .../services/data/services_repository.dart | 16 ++ 8 files changed, 271 insertions(+), 244 deletions(-) create mode 100644 lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart create mode 100644 lib/features/home/latest_store_services/bloc/latest_store_services_events.dart create mode 100644 lib/features/home/latest_store_services/bloc/latest_store_services_state.dart create mode 100644 lib/features/home/latest_store_services/ui/latest_store_services_card.dart delete mode 100644 lib/features/home/ui/dashboard_action_card.dart delete mode 100644 lib/features/home/ui/dashboard_adaptive_grid.dart delete mode 100644 lib/features/home/ui/dashboard_content.dart diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart new file mode 100644 index 0000000..2593922 --- /dev/null +++ b/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart @@ -0,0 +1,48 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flux/features/services/data/services_repository.dart'; +import 'package:flux/features/services/models/service_model.dart'; +import 'package:get_it/get_it.dart'; + +part 'latest_store_services_events.dart'; +part 'latest_store_services_state.dart'; + +class LatestStoreServicesBloc + extends Bloc { + final _repository = GetIt.I.get(); + + LatestStoreServicesBloc() + : super( + const LatestStoreServicesState( + status: LatestStoreServicesStatus.initial, + ), + ) { + on((event, emit) async { + emit(state.copyWith(status: LatestStoreServicesStatus.loading)); + + try { + await emit.forEach( + _repository.getLastStoreServicesStream( + storeId: event.storeId, + limit: 5, + ), + onData: (List data) => state.copyWith( + status: LatestStoreServicesStatus.success, + services: data, + ), + onError: (error, stackTrace) => state.copyWith( + status: LatestStoreServicesStatus.failure, + error: error.toString(), + ), + ); + } catch (e) { + emit( + state.copyWith( + status: LatestStoreServicesStatus.failure, + error: e.toString(), + ), + ); + } + }); + } +} diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart new file mode 100644 index 0000000..9550022 --- /dev/null +++ b/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart @@ -0,0 +1,17 @@ +part of 'latest_store_services_bloc.dart'; + +sealed class LatestStoreServicesEvent extends Equatable { + const LatestStoreServicesEvent(); + + @override + List get props => []; +} + +class InitLastServicesEvent extends LatestStoreServicesEvent { + final String storeId; + + const InitLastServicesEvent(this.storeId); + + @override + List get props => [storeId]; +} diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart new file mode 100644 index 0000000..ede49cb --- /dev/null +++ b/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart @@ -0,0 +1,30 @@ +part of 'latest_store_services_bloc.dart'; + +enum LatestStoreServicesStatus { initial, loading, success, failure } + +class LatestStoreServicesState extends Equatable { + final LatestStoreServicesStatus status; + final String? error; + final List services; + + const LatestStoreServicesState({ + required this.status, + this.error, + this.services = const [], + }); + + @override + List get props => [status, error, services]; + + LatestStoreServicesState copyWith({ + LatestStoreServicesStatus? status, + String? error, + List? services, + }) { + return LatestStoreServicesState( + status: status ?? this.status, + error: error, + services: services ?? this.services, + ); + } +} diff --git a/lib/features/home/latest_store_services/ui/latest_store_services_card.dart b/lib/features/home/latest_store_services/ui/latest_store_services_card.dart new file mode 100644 index 0000000..3fe63b2 --- /dev/null +++ b/lib/features/home/latest_store_services/ui/latest_store_services_card.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/theme/theme.dart'; +import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.dart'; + +class LatestStoreServicesCard extends StatelessWidget { + const LatestStoreServicesCard({super.key}); + + @override + Widget build(BuildContext context) { + final currentStoreId = context.read().state.currentStore?.id; + + return BlocProvider( + // 1. Creiamo il Bloc e facciamo partire subito la query + create: (context) => + LatestStoreServicesBloc() + ..add(InitLastServicesEvent(currentStoreId ?? '')), + child: BlocListener( + // 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream! + listenWhen: (previous, current) => + previous.currentStore?.id != current.currentStore?.id, + listener: (context, state) { + if (state.currentStore?.id != null) { + context.read().add( + InitLastServicesEvent(state.currentStore!.id!), + ); + } + }, + child: _LatestServicesCardContent(), + ), + ); + } +} + +class _LatestServicesCardContent extends StatelessWidget { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + const color = Colors.blue; + + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // --- HEADER DELLA CARD --- + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.design_services_outlined, + color: color, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + "Ultimi Servizi", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: context.primaryText, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 12), + + // --- CORPO DELLA CARD (LA LISTA REAL-TIME) --- + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state.status == LatestStoreServicesStatus.loading || + state.status == LatestStoreServicesStatus.initial) { + return const Center(child: CircularProgressIndicator()); + } + + if (state.status == LatestStoreServicesStatus.failure) { + return Center( + child: Text( + "Errore di caricamento", + style: TextStyle(color: theme.colorScheme.error), + ), + ); + } + + if (state.services.isEmpty) { + return Center( + child: Text( + "Nessun servizio recente.", + style: TextStyle( + color: context.secondaryText, + fontStyle: FontStyle.italic, + ), + ), + ); + } + + return ListView.separated( + itemCount: state.services.length, + separatorBuilder: (context, index) => Divider( + height: 1, + color: theme.dividerColor.withValues(alpha: 0.3), + ), + itemBuilder: (context, index) { + final service = state.services[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + service.number, + style: TextStyle( + fontWeight: FontWeight.w600, + color: context.primaryText, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + // Se hai formattato la data, stampala qui (es. 12/04/2026) + Text( + "${service.createdAt?.day}/${service.createdAt?.month}", + style: TextStyle( + color: context.secondaryText, + fontSize: 12, + ), + ), + ], + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/ui/dashboard_action_card.dart b/lib/features/home/ui/dashboard_action_card.dart deleted file mode 100644 index 46446c2..0000000 --- a/lib/features/home/ui/dashboard_action_card.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flux/core/theme/theme.dart'; - -class DashboardActionCard extends StatelessWidget { - final String label; - final IconData icon; - final Color color; - final VoidCallback onTap; - - const DashboardActionCard({ - super.key, - required this.label, - required this.icon, - required this.color, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - return Card( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - // CAMBIA QUI: da Border.all a BorderSide - side: BorderSide( - color: context.accent.withValues(alpha: 0.1), - width: 1, - ), - ), - child: InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(16), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(icon, color: color, size: 32), - const SizedBox(height: 8), - Text(label, style: const TextStyle(fontWeight: FontWeight.bold)), - ], - ), - ), - ); - } -} diff --git a/lib/features/home/ui/dashboard_adaptive_grid.dart b/lib/features/home/ui/dashboard_adaptive_grid.dart deleted file mode 100644 index 191291d..0000000 --- a/lib/features/home/ui/dashboard_adaptive_grid.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flux/core/theme/theme.dart'; -import 'package:flux/features/home/ui/dashboard_action_card.dart'; -import 'package:flux/features/services/utils/service_actions.dart'; -import 'package:go_router/go_router.dart'; - -class DashboardAdaptiveGrid extends StatelessWidget { - final bool isLargeScreen; - final Function(int)? onTabRequested; - const DashboardAdaptiveGrid({ - super.key, - this.isLargeScreen = false, - this.onTabRequested, - }); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - // Logica Colonne: Mobile 2, Tablet 3, Desktop 4+ - int crossAxisCount = 2; - if (constraints.maxWidth > 1000) { - crossAxisCount = 5; - } else if (constraints.maxWidth > 700) { - crossAxisCount = 3; - } - - return GridView.count( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - crossAxisCount: crossAxisCount, - mainAxisSpacing: 16, - crossAxisSpacing: 16, - childAspectRatio: isLargeScreen ? 1.3 : 1.5, - children: [ - DashboardActionCard( - label: 'Nuova Op', - icon: Icons.add_task, - color: context.accent, - onTap: () => startNewService(context), - ), - DashboardActionCard( - label: 'Clienti', - icon: Icons.people, - color: Colors.orange, - onTap: () => onTabRequested?.call(1), - ), - DashboardActionCard( - label: 'Prodotti', - icon: Icons - .phone_android_outlined, // Icona "comoda" e professionale - color: context - .accent, // O un colore a tua scelta, magari Indigo o Blue - onTap: () => context.push( - '/products', - ), // Apre la schermata sopra la Dashboard - ), - DashboardActionCard( - label: 'Campagne', - icon: Icons.campaign, - color: Colors.purple, - onTap: () {}, - ), - DashboardActionCard( - label: 'Report', - icon: Icons.analytics, - color: Colors.teal, - onTap: () {}, - ), - ], - ); - }, - ); - } -} diff --git a/lib/features/home/ui/dashboard_content.dart b/lib/features/home/ui/dashboard_content.dart deleted file mode 100644 index d32b204..0000000 --- a/lib/features/home/ui/dashboard_content.dart +++ /dev/null @@ -1,125 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/theme/theme.dart'; -import 'package:flux/features/home/ui/dashboard_adaptive_grid.dart'; - -class DashboardContent extends StatelessWidget { - final bool isLargeScreen; - final Function(int)? onTabRequested; - - const DashboardContent({ - super.key, - this.isLargeScreen = false, - this.onTabRequested, - }); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final store = state.currentStore; - final company = state.company; - - return Scaffold( - backgroundColor: context.background, - body: CustomScrollView( - slivers: [ - SliverAppBar( - expandedHeight: 100.0, - floating: false, - pinned: true, - elevation: 0, - backgroundColor: context.background, - flexibleSpace: FlexibleSpaceBar( - titlePadding: const EdgeInsets.only(left: 24, bottom: 16), - title: Text( - store?.nome ?? 'Dashboard', - style: TextStyle( - color: context.primaryText, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - SliverToBoxAdapter( - child: Center( - child: Container( - constraints: const BoxConstraints(maxWidth: 1200), - padding: const EdgeInsets.all(24.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildWelcome(context, company?.ragioneSociale), - const SizedBox(height: 32), - const _SectionTitle(title: 'AZIONI RAPIDE'), - const SizedBox(height: 16), - DashboardAdaptiveGrid( - isLargeScreen: isLargeScreen, - onTabRequested: onTabRequested, - ), - const SizedBox(height: 40), - const _SectionTitle(title: 'INFO PUNTO VENDITA'), - const SizedBox(height: 16), - _buildStoreCard(context, store), - ], - ), - ), - ), - ), - ], - ), - ); - }, - ); - } - - Widget _buildWelcome(BuildContext context, String? name) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Benvenuto in', - style: TextStyle(color: context.secondaryText, fontSize: 16), - ), - Text( - name ?? 'Azienda', - style: const TextStyle(fontSize: 28, fontWeight: FontWeight.w900), - ), - ], - ); - } - - Widget _buildStoreCard(BuildContext context, dynamic store) { - return Container( - padding: const EdgeInsets.all(20), - decoration: BoxDecoration( - color: context.accent.withValues(alpha: 0.05), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: context.accent.withValues(alpha: 0.1)), - ), - child: Row( - children: [ - Icon(Icons.location_on, color: context.accent), - const SizedBox(width: 16), - Text('${store?.indirizzo}, ${store?.comune} (${store?.provincia})'), - ], - ), - ); - } -} - -class _SectionTitle extends StatelessWidget { - final String title; - const _SectionTitle({required this.title}); - @override - Widget build(BuildContext context) => Text( - title, - style: TextStyle( - color: context.accent, - fontWeight: FontWeight.bold, - fontSize: 12, - letterSpacing: 1.2, - ), - ); -} diff --git a/lib/features/services/data/services_repository.dart b/lib/features/services/data/services_repository.dart index abfb672..624c19d 100644 --- a/lib/features/services/data/services_repository.dart +++ b/lib/features/services/data/services_repository.dart @@ -84,6 +84,22 @@ class ServicesRepository { } } + Stream> getLastStoreServicesStream({ + required String storeId, + required int limit, + }) { + return _supabase + .from('service') + .stream(primaryKey: ['id']) + .eq('store_id', storeId) + .order('created_at', ascending: false) + .limit(limit) + .map( + (listOfMaps) => + listOfMaps.map((map) => ServiceModel.fromMap(map)).toList(), + ); + } + // --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) --- Future saveFullService(ServiceModel service) async { try { -- 2.43.0 From 8d3ca62304c9a90b1eb8915a83daf0717a42313b Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Wed, 29 Apr 2026 19:25:48 +0200 Subject: [PATCH 02/18] localization --- analysis_options.yaml | 15 ++ l10n.yaml | 3 + ...string_extensions.dart => extensions.dart} | 7 + .../customers/data/customer_repository.dart | 2 +- .../customers/models/customer_model.dart | 2 +- lib/features/home/ui/home_screen.dart | 9 +- .../products/models/brand_model.dart | 2 +- .../products/models/model_model.dart | 2 +- .../services/blocs/service_files_bloc.dart | 2 +- .../services/blocs/services_cubit.dart | 2 +- .../services/data/services_repository.dart | 2 +- .../services/models/service_model.dart | 2 +- lib/l10n/app_en.arb | 13 ++ lib/l10n/app_it.arb | 20 ++ lib/l10n/app_localizations.dart | 188 ++++++++++++++++++ lib/l10n/app_localizations_en.dart | 39 ++++ lib/l10n/app_localizations_it.dart | 39 ++++ lib/main.dart | 6 + pubspec.lock | 21 +- pubspec.yaml | 5 + 20 files changed, 361 insertions(+), 20 deletions(-) create mode 100644 l10n.yaml rename lib/core/utils/{string_extensions.dart => extensions.dart} (86%) create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_it.arb create mode 100644 lib/l10n/app_localizations.dart create mode 100644 lib/l10n/app_localizations_en.dart create mode 100644 lib/l10n/app_localizations_it.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index f9b3034..d78598d 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1 +1,16 @@ include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + # Escludiamo i file generati per le lingue, così il linter non ci entra proprio + - "lib/generated/**" + - "lib/l10n/*.dart" + - "**/*.g.dart" # Già che ci siamo escludiamo tutti i file generati (tipo quelli di JsonSerializable) + - "**/*.freezed.dart" + +linter: + rules: + diagnostic_describe_all_properties: false + public_member_api_docs: false + # Ti consiglio di aggiungere anche questa se usi molto i file generati + avoid_relative_lib_imports: true \ No newline at end of file diff --git a/l10n.yaml b/l10n.yaml new file mode 100644 index 0000000..aba5d99 --- /dev/null +++ b/l10n.yaml @@ -0,0 +1,3 @@ +arb-dir: lib/l10n +template-arb-file: app_it.arb +output-localization-file: app_localizations.dart \ No newline at end of file diff --git a/lib/core/utils/string_extensions.dart b/lib/core/utils/extensions.dart similarity index 86% rename from lib/core/utils/string_extensions.dart rename to lib/core/utils/extensions.dart index 060d8df..27b2e53 100644 --- a/lib/core/utils/string_extensions.dart +++ b/lib/core/utils/extensions.dart @@ -1,3 +1,6 @@ +import 'package:flutter/material.dart'; +import 'package:flux/l10n/app_localizations.dart'; + extension MyStringExtensions on String? { // Gestiamo anche il nullable per sicurezza String myFormat() { @@ -40,3 +43,7 @@ extension MyStringExtensions on String? { .join('.'); // Ritorna tutto tranne l'ultima parte } } + +extension LocalizationExtension on BuildContext { + AppLocalizations get l10n => AppLocalizations.of(this)!; +} diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index d2a8f7a..daf61c0 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -1,7 +1,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/utils/string_extensions.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; diff --git a/lib/features/customers/models/customer_model.dart b/lib/features/customers/models/customer_model.dart index fa55090..d1fe55e 100644 --- a/lib/features/customers/models/customer_model.dart +++ b/lib/features/customers/models/customer_model.dart @@ -1,5 +1,5 @@ import 'package:equatable/equatable.dart'; -import 'package:flux/core/utils/string_extensions.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/customers/models/customer_file_model.dart'; class CustomerModel extends Equatable { diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 9f9abac..e254aeb 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/home/ui/quick_actions_widget.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:go_router/go_router.dart'; @@ -57,25 +58,25 @@ class HomeScreen extends StatelessWidget { ), delegate: SliverChildListDelegate([ _buildDashboardWidget( - title: 'Contratti in Scadenza', + title: context.l10n.expiring_contracts, icon: Icons.assignment_late_outlined, color: Colors.orange, context: context, ), _buildDashboardWidget( - title: 'Sticky Notes', + title: context.l10n.sticky_notes, icon: Icons.sticky_note_2_outlined, color: Colors.yellow.shade700, context: context, ), _buildDashboardWidget( - title: 'I miei Task', + title: context.l10n.my_tasks, icon: Icons.check_box_outlined, color: Colors.green, context: context, ), _buildDashboardWidget( - title: 'Ultimi Servizi', + title: context.l10n.latestServices, icon: Icons.design_services_outlined, color: Colors.blue, context: context, diff --git a/lib/features/master_data/products/models/brand_model.dart b/lib/features/master_data/products/models/brand_model.dart index 72e02b1..f6c33c6 100644 --- a/lib/features/master_data/products/models/brand_model.dart +++ b/lib/features/master_data/products/models/brand_model.dart @@ -1,5 +1,5 @@ import 'package:equatable/equatable.dart'; -import 'package:flux/core/utils/string_extensions.dart'; +import 'package:flux/core/utils/extensions.dart'; class BrandModel extends Equatable { final String? id; diff --git a/lib/features/master_data/products/models/model_model.dart b/lib/features/master_data/products/models/model_model.dart index 4859356..aff7e36 100644 --- a/lib/features/master_data/products/models/model_model.dart +++ b/lib/features/master_data/products/models/model_model.dart @@ -1,5 +1,5 @@ import 'package:equatable/equatable.dart'; -import 'package:flux/core/utils/string_extensions.dart'; +import 'package:flux/core/utils/extensions.dart'; class ModelModel extends Equatable { final String? id; diff --git a/lib/features/services/blocs/service_files_bloc.dart b/lib/features/services/blocs/service_files_bloc.dart index 0298ef2..02f1922 100644 --- a/lib/features/services/blocs/service_files_bloc.dart +++ b/lib/features/services/blocs/service_files_bloc.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flux/core/utils/string_extensions.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/services/data/services_repository.dart'; import 'package:flux/features/services/models/service_file_model.dart'; import 'package:flux/features/services/models/service_model.dart'; diff --git a/lib/features/services/blocs/services_cubit.dart b/lib/features/services/blocs/services_cubit.dart index 3417802..a9fb3a6 100644 --- a/lib/features/services/blocs/services_cubit.dart +++ b/lib/features/services/blocs/services_cubit.dart @@ -3,7 +3,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/utils/string_extensions.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/services/data/services_repository.dart'; import 'package:flux/features/services/models/energy_service_model.dart'; import 'package:flux/features/services/models/entertainment_service_model.dart'; diff --git a/lib/features/services/data/services_repository.dart b/lib/features/services/data/services_repository.dart index 624c19d..9c91fc0 100644 --- a/lib/features/services/data/services_repository.dart +++ b/lib/features/services/data/services_repository.dart @@ -1,7 +1,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/utils/string_extensions.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:flux/features/services/models/service_file_model.dart'; diff --git a/lib/features/services/models/service_model.dart b/lib/features/services/models/service_model.dart index df58125..a4d15f6 100644 --- a/lib/features/services/models/service_model.dart +++ b/lib/features/services/models/service_model.dart @@ -1,5 +1,5 @@ import 'package:equatable/equatable.dart'; -import 'package:flux/core/utils/string_extensions.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/services/models/energy_service_model.dart'; import 'package:flux/features/services/models/entertainment_service_model.dart'; import 'package:flux/features/services/models/fin_service_model.dart'; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..84c90f5 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,13 @@ +{ + "@@locale": "en", + "welcomeBack": "Welcome back, {name}! 👋", + "latestServices": "Latest Services", + "masterData": "Master Data", + "settings": "Settings", + "newService": "Service", + "expiring_contracts": "Expiring Contracts", + "sticky_notes": "Sticky Notes", + "my_tasks": "My Tasks", + "latest_service_tickets": "Latest service tickets" + +} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb new file mode 100644 index 0000000..b6ab9cc --- /dev/null +++ b/lib/l10n/app_it.arb @@ -0,0 +1,20 @@ +{ + "@@locale": "it", + "welcomeBack": "Bentornato, {name}! 👋", + "@welcomeBack": { + "placeholders": { + "name": { + "type": "String" + } + } + }, + "latestServices": "Ultimi Servizi", + "masterData": "Anagrafiche", + "settings": "Impostazioni", + "newService": "Servizio", + "expiring_contracts": "Contratti in scadenza", + "sticky_notes": "Sticky Notes", + "my_tasks": "Mie Attività", + "latest_service_tickets": "Ultime assistenze" + +} \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart new file mode 100644 index 0000000..58547fa --- /dev/null +++ b/lib/l10n/app_localizations.dart @@ -0,0 +1,188 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:intl/intl.dart' as intl; + +import 'app_localizations_en.dart'; +import 'app_localizations_it.dart'; + +// ignore_for_file: type=lint + +/// Callers can lookup localized strings with an instance of AppLocalizations +/// returned by `AppLocalizations.of(context)`. +/// +/// Applications need to include `AppLocalizations.delegate()` in their app's +/// `localizationDelegates` list, and the locales they support in the app's +/// `supportedLocales` list. For example: +/// +/// ```dart +/// import 'l10n/app_localizations.dart'; +/// +/// return MaterialApp( +/// localizationsDelegates: AppLocalizations.localizationsDelegates, +/// supportedLocales: AppLocalizations.supportedLocales, +/// home: MyApplicationHome(), +/// ); +/// ``` +/// +/// ## Update pubspec.yaml +/// +/// Please make sure to update your pubspec.yaml to include the following +/// packages: +/// +/// ```yaml +/// dependencies: +/// # Internationalization support. +/// flutter_localizations: +/// sdk: flutter +/// intl: any # Use the pinned version from flutter_localizations +/// +/// # Rest of dependencies +/// ``` +/// +/// ## iOS Applications +/// +/// iOS applications define key application metadata, including supported +/// locales, in an Info.plist file that is built into the application bundle. +/// To configure the locales supported by your app, you’ll need to edit this +/// file. +/// +/// First, open your project’s ios/Runner.xcworkspace Xcode workspace file. +/// Then, in the Project Navigator, open the Info.plist file under the Runner +/// project’s Runner folder. +/// +/// Next, select the Information Property List item, select Add Item from the +/// Editor menu, then select Localizations from the pop-up menu. +/// +/// Select and expand the newly-created Localizations item then, for each +/// locale your application supports, add a new item and select the locale +/// you wish to add from the pop-up menu in the Value field. This list should +/// be consistent with the languages listed in the AppLocalizations.supportedLocales +/// property. +abstract class AppLocalizations { + AppLocalizations(String locale) + : localeName = intl.Intl.canonicalizedLocale(locale.toString()); + + final String localeName; + + static AppLocalizations? of(BuildContext context) { + return Localizations.of(context, AppLocalizations); + } + + static const LocalizationsDelegate delegate = + _AppLocalizationsDelegate(); + + /// A list of this localizations delegate along with the default localizations + /// delegates. + /// + /// Returns a list of localizations delegates containing this delegate along with + /// GlobalMaterialLocalizations.delegate, GlobalCupertinoLocalizations.delegate, + /// and GlobalWidgetsLocalizations.delegate. + /// + /// Additional delegates can be added by appending to this list in + /// MaterialApp. This list does not have to be used at all if a custom list + /// of delegates is preferred or required. + static const List> localizationsDelegates = + >[ + delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ]; + + /// A list of this localizations delegate's supported locales. + static const List supportedLocales = [ + Locale('en'), + Locale('it'), + ]; + + /// No description provided for @welcomeBack. + /// + /// In it, this message translates to: + /// **'Bentornato, {name}! 👋'** + String welcomeBack(String name); + + /// No description provided for @latestServices. + /// + /// In it, this message translates to: + /// **'Ultimi Servizi'** + String get latestServices; + + /// No description provided for @masterData. + /// + /// In it, this message translates to: + /// **'Anagrafiche'** + String get masterData; + + /// No description provided for @settings. + /// + /// In it, this message translates to: + /// **'Impostazioni'** + String get settings; + + /// No description provided for @newService. + /// + /// In it, this message translates to: + /// **'Servizio'** + String get newService; + + /// No description provided for @expiring_contracts. + /// + /// In it, this message translates to: + /// **'Contratti in scadenza'** + String get expiring_contracts; + + /// No description provided for @sticky_notes. + /// + /// In it, this message translates to: + /// **'Sticky Notes'** + String get sticky_notes; + + /// No description provided for @my_tasks. + /// + /// In it, this message translates to: + /// **'Mie Attività'** + String get my_tasks; + + /// No description provided for @latest_service_tickets. + /// + /// In it, this message translates to: + /// **'Ultime assistenze'** + String get latest_service_tickets; +} + +class _AppLocalizationsDelegate + extends LocalizationsDelegate { + const _AppLocalizationsDelegate(); + + @override + Future load(Locale locale) { + return SynchronousFuture(lookupAppLocalizations(locale)); + } + + @override + bool isSupported(Locale locale) => + ['en', 'it'].contains(locale.languageCode); + + @override + bool shouldReload(_AppLocalizationsDelegate old) => false; +} + +AppLocalizations lookupAppLocalizations(Locale locale) { + // Lookup logic when only language code is specified. + switch (locale.languageCode) { + case 'en': + return AppLocalizationsEn(); + case 'it': + return AppLocalizationsIt(); + } + + throw FlutterError( + 'AppLocalizations.delegate failed to load unsupported locale "$locale". This is likely ' + 'an issue with the localizations generation tool. Please file an issue ' + 'on GitHub with a reproducible sample app and the gen-l10n configuration ' + 'that was used.', + ); +} diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..f5dea85 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,39 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String welcomeBack(String name) { + return 'Welcome back, $name! 👋'; + } + + @override + String get latestServices => 'Latest Services'; + + @override + String get masterData => 'Master Data'; + + @override + String get settings => 'Settings'; + + @override + String get newService => 'Service'; + + @override + String get expiring_contracts => 'Expiring Contracts'; + + @override + String get sticky_notes => 'Sticky Notes'; + + @override + String get my_tasks => 'My Tasks'; + + @override + String get latest_service_tickets => 'Latest service tickets'; +} diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart new file mode 100644 index 0000000..434a39d --- /dev/null +++ b/lib/l10n/app_localizations_it.dart @@ -0,0 +1,39 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for Italian (`it`). +class AppLocalizationsIt extends AppLocalizations { + AppLocalizationsIt([String locale = 'it']) : super(locale); + + @override + String welcomeBack(String name) { + return 'Bentornato, $name! 👋'; + } + + @override + String get latestServices => 'Ultimi Servizi'; + + @override + String get masterData => 'Anagrafiche'; + + @override + String get settings => 'Impostazioni'; + + @override + String get newService => 'Servizio'; + + @override + String get expiring_contracts => 'Contratti in scadenza'; + + @override + String get sticky_notes => 'Sticky Notes'; + + @override + String get my_tasks => 'Mie Attività'; + + @override + String get latest_service_tickets => 'Ultime assistenze'; +} diff --git a/lib/main.dart b/lib/main.dart index 93b8891..3e64a34 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flux/features/auth/bloc/auth_cubit.dart'; +import 'package:flux/l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -152,6 +153,11 @@ class _FluxAppState extends State { darkTheme: fluxDarkTheme, themeMode: themeState.currentTheme.themeMode, routerConfig: _router, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + locale: const Locale( + 'it', + ), // Per ora forziamo l'italiano, poi lo renderemo dinamico! ); }, ); diff --git a/pubspec.lock b/pubspec.lock index b1c4635..30588e2 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -254,6 +254,11 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -316,10 +321,10 @@ packages: dependency: "direct main" description: name: google_fonts - sha256: db9df7a5898d894eeda4c78143f35c30a243558be439518972366880b80bf88e + sha256: "4e9391085e524954a51e3625b7c9c7e9851dc3f376603208bb45c24b9a66255d" url: "https://pub.dev" source: hosted - version: "8.0.2" + version: "8.1.0" gotrue: dependency: transitive description: @@ -364,10 +369,10 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" image_picker_android: dependency: transitive description: @@ -1041,10 +1046,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373" + sha256: "6409a25046024f0f8c5d8a59fec314081e81f9d436b66ca4015a8b49772bf445" url: "https://pub.dev" source: hosted - version: "1.1.21" + version: "1.2.0" vector_graphics_codec: dependency: transitive description: @@ -1073,10 +1078,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" url: "https://pub.dev" source: hosted - version: "15.1.0" + version: "15.2.0" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 732b141..1bef9ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,6 +12,8 @@ dependencies: file_picker: ^11.0.2 flutter: sdk: flutter + flutter_localizations: + sdk: flutter flutter_bloc: ^9.1.1 flutter_dotenv: ^6.0.0 flutter_svg: ^2.2.4 @@ -26,6 +28,8 @@ dependencies: qr_flutter: ^4.1.0 shared_preferences: ^2.5.5 supabase_flutter: ^2.12.2 + + dev_dependencies: flutter_test: @@ -34,6 +38,7 @@ dev_dependencies: flutter: uses-material-design: true + generate: true assets: - assets/images/ -- 2.43.0 From 11c1e28aaa97a9df3307083ddda53b439d8a6595 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Thu, 30 Apr 2026 10:25:52 +0200 Subject: [PATCH 03/18] basta localizzazioni per ora Co-authored-by: Copilot --- lib/core/data/core_repository.dart | 14 +- lib/core/layout/app_shell.dart | 17 +- lib/core/routes/app_router.dart | 17 +- lib/core/utils/app_message.dart | 20 + lib/core/widgets/image_viewer_widget.dart | 5 +- lib/core/widgets/pdf_viewer_widget.dart | 5 +- lib/core/widgets/qr_upload_dialog.dart | 3 +- lib/core/widgets/set_password_screen.dart | 37 +- lib/features/auth/bloc/auth_cubit.dart | 10 +- lib/features/auth/bloc/auth_state.dart | 4 +- lib/features/auth/ui/auth_screen.dart | 30 +- lib/features/company/bloc/company_state.dart | 2 +- .../company/data/company_repository.dart | 2 +- .../company/ui/create_company_screen.dart | 40 +- .../customers/data/customer_repository.dart | 20 +- lib/features/home/ui/home_screen.dart | 24 +- lib/l10n/app_en.arb | 13 - lib/l10n/app_it.arb | 92 ++++- lib/l10n/app_localizations.dart | 350 ++++++++++++++++-- lib/l10n/app_localizations_en.dart | 39 -- lib/l10n/app_localizations_it.dart | 181 ++++++++- lib/main.dart | 4 +- pubspec.yaml | 4 +- 23 files changed, 720 insertions(+), 213 deletions(-) create mode 100644 lib/core/utils/app_message.dart delete mode 100644 lib/l10n/app_en.arb delete mode 100644 lib/l10n/app_localizations_en.dart diff --git a/lib/core/data/core_repository.dart b/lib/core/data/core_repository.dart index 192e412..6aeac53 100644 --- a/lib/core/data/core_repository.dart +++ b/lib/core/data/core_repository.dart @@ -24,7 +24,7 @@ class CoreRepository { return CompanyModel.fromMap(response); } catch (e) { debugPrint('Errore recupero azienda: $e'); - throw Exception('Errore recupero azienda: $e'); + throw Exception('$e'); } } @@ -38,7 +38,7 @@ class CoreRepository { if (response == null) return null; return CompanyModel.fromMap(response); } catch (e) { - debugPrint('Errore recupero azienda per ID: $e'); + debugPrint('$e'); return null; } } @@ -55,7 +55,7 @@ class CoreRepository { return (response as List).map((s) => StoreModel.fromMap(s)).toList(); } catch (e) { debugPrint('Errore recupero negozi: $e'); - throw Exception('Errore recupero negozi: $e'); + throw Exception('$e'); } } @@ -71,7 +71,7 @@ class CoreRepository { return StaffMemberModel.fromMap(response); } catch (e) { debugPrint('Errore recupero profilo staff: $e'); - throw Exception('Errore recupero profilo staff: $e'); + throw Exception('$e'); } } @@ -87,7 +87,7 @@ class CoreRepository { return CompanyModel.fromMap(response); } catch (e) { debugPrint('Creazione azienda fallita: $e'); - throw Exception('Creazione azienda fallita: $e'); + throw Exception('$e'); } } @@ -101,7 +101,7 @@ class CoreRepository { return StoreModel.fromMap(response); } catch (e) { debugPrint('Creazione negozio fallita: $e'); - throw Exception('Creazione negozio fallita: $e'); + throw Exception('$e'); } } @@ -120,7 +120,7 @@ class CoreRepository { return StaffMemberModel.fromMap(response); } catch (e) { debugPrint('Creazione profilo staff fallita: $e'); - throw Exception('Creazione profilo staff fallita: $e'); + throw Exception('$e'); } } diff --git a/lib/core/layout/app_shell.dart b/lib/core/layout/app_shell.dart index d5eb3af..dcf2bdd 100644 --- a/lib/core/layout/app_shell.dart +++ b/lib/core/layout/app_shell.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:go_router/go_router.dart'; class AppShell extends StatelessWidget { @@ -43,21 +44,21 @@ class AppShell extends StatelessWidget { onDestinationSelected: (index) => _onItemTapped(index, context), labelType: NavigationRailLabelType.all, - destinations: const [ + destinations: [ NavigationRailDestination( icon: Icon(Icons.dashboard_outlined), selectedIcon: Icon(Icons.dashboard), - label: Text('Dashboard'), + label: Text(context.l10n.commonDashboard), ), NavigationRailDestination( icon: Icon(Icons.folder_special_outlined), selectedIcon: Icon(Icons.folder_special), - label: Text('Anagrafiche'), + label: Text(context.l10n.commonMasterData), ), NavigationRailDestination( icon: Icon(Icons.settings_outlined), selectedIcon: Icon(Icons.settings), - label: Text('Impostazioni'), + label: Text(context.l10n.commonSettings), ), ], ), @@ -73,21 +74,21 @@ class AppShell extends StatelessWidget { : NavigationBar( selectedIndex: currentIndex, onDestinationSelected: (index) => _onItemTapped(index, context), - destinations: const [ + destinations: [ NavigationDestination( icon: Icon(Icons.dashboard_outlined), selectedIcon: Icon(Icons.dashboard), - label: 'Dashboard', + label: context.l10n.commonDashboard, ), NavigationDestination( icon: Icon(Icons.folder_special_outlined), selectedIcon: Icon(Icons.folder_special), - label: 'Anagrafiche', + label: context.l10n.commonMasterData, ), NavigationDestination( icon: Icon(Icons.settings_outlined), selectedIcon: Icon(Icons.settings), - label: 'Impostazioni', + label: context.l10n.commonSettings, ), ], ), diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 89c892f..2008f95 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/data/core_repository.dart'; import 'package:flux/core/layout/app_shell.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/widgets/set_password_screen.dart'; import 'package:flux/features/auth/ui/auth_screen.dart'; import 'package:flux/features/customers/blocs/customer_files_bloc.dart'; @@ -14,6 +15,9 @@ import 'package:flux/features/customers/ui/customers_content.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/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'; import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; import 'package:flux/features/onboarding/ui/onboarding_screen.dart'; import 'package:flux/features/services/blocs/service_files_bloc.dart'; @@ -96,19 +100,16 @@ class AppRouter { ), GoRoute( path: 'staff', // Diventa /master-data/staff - builder: (context, state) => - const Scaffold(body: Center(child: Text("Lista Staff"))), + builder: (context, state) => const StaffScreen(), ), GoRoute( path: 'stores', // Diventa /master-data/stores - builder: (context, state) => - const Scaffold(body: Center(child: Text("Lista Negozi"))), + builder: (context, state) => const StoresScreen(), ), GoRoute( path: 'providers', // Diventa /master-data/providers - builder: (context, state) => const Scaffold( - body: Center(child: Text("Lista Fornitori")), - ), + builder: (context, state) => + const ProvidersMasterDataScreen(), ), ], ), @@ -117,7 +118,7 @@ class AppRouter { GoRoute( path: '/settings', builder: (context, state) => Scaffold( - appBar: AppBar(title: const Text("Impostazioni")), + appBar: AppBar(title: Text(context.l10n.commonSettings)), body: Center( child: ElevatedButton.icon( onPressed: () => context.read().signOut(), diff --git a/lib/core/utils/app_message.dart b/lib/core/utils/app_message.dart new file mode 100644 index 0000000..66ce076 --- /dev/null +++ b/lib/core/utils/app_message.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:flux/core/utils/extensions.dart'; + +class AppMessage { + final String key; + final String? argument; + + const AppMessage({required this.key, this.argument}); + + String translatedMessage(BuildContext context) { + switch (key) { + case 'authCubitCheckEmailToConfirmAccount': + return context.l10n.authCubitCheckEmailToConfirmAccount; + case 'authCubitResetPasswordEmailSentTo': + return context.l10n.authCubitResetPasswordEmailSentTo(argument!); + default: + return 'empty message'; + } + } +} diff --git a/lib/core/widgets/image_viewer_widget.dart b/lib/core/widgets/image_viewer_widget.dart index cfb90d0..ef5f1bb 100644 --- a/lib/core/widgets/image_viewer_widget.dart +++ b/lib/core/widgets/image_viewer_widget.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/utils/functions.dart'; class ImageViewerWidget extends StatelessWidget { @@ -36,8 +37,8 @@ class ImageViewerWidget extends StatelessWidget { return const CircularProgressIndicator(); } if (snapshot.hasError) { - return const Text( - "Errore caricamento immagine (Permessi negati?)", + return Text( + context.l10n.imageViewerWidgetErrorOpening, style: TextStyle(color: Colors.red), ); } diff --git a/lib/core/widgets/pdf_viewer_widget.dart b/lib/core/widgets/pdf_viewer_widget.dart index f83196a..6bcb85e 100644 --- a/lib/core/widgets/pdf_viewer_widget.dart +++ b/lib/core/widgets/pdf_viewer_widget.dart @@ -1,5 +1,6 @@ import 'dart:typed_data'; import 'package:flutter/material.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/utils/functions.dart'; import 'package:pdfx/pdfx.dart'; import 'package:internet_file/internet_file.dart'; @@ -74,13 +75,13 @@ class _PdfViewerWidgetState extends State { if (_errorMessage != null) { return Scaffold( appBar: AppBar(leading: const CloseButton()), - body: Center(child: Text("Errore: $_errorMessage")), + body: Center(child: Text(context.l10n.commonError(_errorMessage!))), ); } return Scaffold( appBar: AppBar( - title: const Text("Anteprima PDF"), + title: Text(context.l10n.pdfViewerAnteprimaPdf), leading: IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.pop(context), diff --git a/lib/core/widgets/qr_upload_dialog.dart b/lib/core/widgets/qr_upload_dialog.dart index 32ea978..65d37ee 100644 --- a/lib/core/widgets/qr_upload_dialog.dart +++ b/lib/core/widgets/qr_upload_dialog.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:qr_flutter/qr_flutter.dart'; class QrUploadDialog extends StatelessWidget { @@ -84,7 +85,7 @@ class QrUploadDialog extends StatelessWidget { actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text("CHIUDI"), + child: Text(context.l10n.commonClose), ), ], actionsAlignment: MainAxisAlignment.center, diff --git a/lib/core/widgets/set_password_screen.dart b/lib/core/widgets/set_password_screen.dart index 33e54b9..5f5d72d 100644 --- a/lib/core/widgets/set_password_screen.dart +++ b/lib/core/widgets/set_password_screen.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -25,9 +26,7 @@ class _SetPasswordScreenState extends State { final newPassword = _passwordCtrl.text.trim(); if (newPassword.length < 6) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("La password deve avere almeno 6 caratteri"), - ), + SnackBar(content: Text(context.l10n.setPasswordScreenAtLeast6Chars)), ); return; } @@ -43,23 +42,23 @@ class _SetPasswordScreenState extends State { // 2. Finito! Lo mandiamo alla home o facciamo ricaricare la sessione al SessionCubit if (mounted) { ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Password impostata! Benvenuto a bordo 🚀"), + SnackBar( + content: Text(context.l10n.setPasswordScreenPasswordSetWelcome), ), ); context.go('/'); // Rimandiamo al router principale } } on AuthException catch (e) { if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text("Errore Auth: ${e.message}"))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.authError(e.message))), + ); } } catch (e) { if (mounted) { - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text("Errore: $e"))); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.commonError(e.toString()))), + ); } } finally { if (mounted) setState(() => _isLoading = false); @@ -70,7 +69,7 @@ class _SetPasswordScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text("Benvenuto in FLUX!"), + title: Text(context.l10n.setPasswordScreenWelcomeInFlux), automaticallyImplyLeading: false, // Non può tornare indietro, deve mettere la password! ), @@ -82,21 +81,21 @@ class _SetPasswordScreenState extends State { children: [ const Icon(Icons.lock_reset, size: 80, color: Colors.blueAccent), const SizedBox(height: 24), - const Text( - "Imposta la tua Password", + Text( + context.l10n.setPasswordScreenSetPassword, textAlign: TextAlign.center, style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold), ), const SizedBox(height: 8), - const Text( - "Hai accettato l'invito. Scegli una password sicura per accedere in futuro.", + Text( + context.l10n.setPasswordInviteAcceptedChoosePassword, textAlign: TextAlign.center, style: TextStyle(color: Colors.grey), ), const SizedBox(height: 32), FluxTextField( controller: _passwordCtrl, - label: "Nuova Password", + label: context.l10n.commonNewPassword, icon: Icons.lock, isPassword: true, ), @@ -108,8 +107,8 @@ class _SetPasswordScreenState extends State { ), child: _isLoading ? const CircularProgressIndicator(color: Colors.white) - : const Text( - "SALVA E INIZIA", + : Text( + context.l10n.setPasswordScreenSaveAndStart, style: TextStyle(fontSize: 16), ), ), diff --git a/lib/features/auth/bloc/auth_cubit.dart b/lib/features/auth/bloc/auth_cubit.dart index 53411e0..b4faf4a 100644 --- a/lib/features/auth/bloc/auth_cubit.dart +++ b/lib/features/auth/bloc/auth_cubit.dart @@ -2,6 +2,7 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/data/constants.dart'; +import 'package:flux/core/utils/app_message.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; part 'auth_state.dart'; @@ -41,7 +42,9 @@ class AuthCubit extends Cubit { emit( state.copyWith( status: AuthStatus.initial, - infoMessage: "Controlla la tua email per confermare l'account!", + infoMessage: AppMessage( + key: 'authCubitCheckEmailToConfirmAccount', + ), ), ); } else { @@ -82,7 +85,10 @@ class AuthCubit extends Cubit { emit( state.copyWith( status: AuthStatus.pwResetSent, - infoMessage: "Email per reset password inviata a $email!", + infoMessage: AppMessage( + key: 'authCubitResetPasswordEmailSentTo', + argument: email, + ), ), ); } diff --git a/lib/features/auth/bloc/auth_state.dart b/lib/features/auth/bloc/auth_state.dart index f3c237e..5bb2dcf 100644 --- a/lib/features/auth/bloc/auth_state.dart +++ b/lib/features/auth/bloc/auth_state.dart @@ -6,7 +6,7 @@ class AuthState extends Equatable { final AuthStatus status; final bool isLoginMode; final String? errorMessage; - final String? infoMessage; + final AppMessage? infoMessage; const AuthState({ this.status = AuthStatus.initial, @@ -19,7 +19,7 @@ class AuthState extends Equatable { AuthStatus? status, bool? isLoginMode, String? errorMessage, - String? infoMessage, + AppMessage? infoMessage, }) { return AuthState( status: status ?? this.status, diff --git a/lib/features/auth/ui/auth_screen.dart b/lib/features/auth/ui/auth_screen.dart index f6327d8..428b6d7 100644 --- a/lib/features/auth/ui/auth_screen.dart +++ b/lib/features/auth/ui/auth_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/theme/theme.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/widgets/flux_logo.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/auth/bloc/auth_cubit.dart'; @@ -55,7 +56,7 @@ class _AuthScreenState extends State { if (state.infoMessage != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(state.infoMessage!), + content: Text(state.infoMessage!.translatedMessage(context)), backgroundColor: Colors.blueAccent, // O context.accent ), ); @@ -77,7 +78,9 @@ class _AuthScreenState extends State { // --- TITOLO DINAMICO --- Text( - state.isLoginMode ? 'BENTORNATO' : 'CREA ACCOUNT', + state.isLoginMode + ? context.l10n.authScreenWelcomeBack + : context.l10n.authScreenCreateAccount, style: TextStyle( color: context.primaryText, fontSize: 24, @@ -88,8 +91,10 @@ class _AuthScreenState extends State { const SizedBox(height: 8), Text( state.isLoginMode - ? 'Accedi per gestire il tuo business' - : 'Inizia oggi a digitalizzare il tuo negozio', + ? context.l10n.authScreenLoginToManageYourBusiness + : context + .l10n + .authScreenStartTodayToDigitalizeYourStore, textAlign: TextAlign.center, style: TextStyle(color: context.secondaryText), ), @@ -97,7 +102,7 @@ class _AuthScreenState extends State { // --- CAMPI INPUT --- FluxTextField( - label: 'Email Aziendale', + label: context.l10n.authScreenBusinessEmail, icon: Icons.email_outlined, controller: _emailController, keyboardType: TextInputType.emailAddress, @@ -130,7 +135,9 @@ class _AuthScreenState extends State { ), ) : Text( - state.isLoginMode ? 'ACCEDI' : 'REGISTRATI', + state.isLoginMode + ? context.l10n.authScreenLogin + : context.l10n.authScreenSignUp, style: const TextStyle( fontWeight: FontWeight.bold, ), @@ -147,12 +154,15 @@ class _AuthScreenState extends State { child: RichText( text: TextSpan( text: state.isLoginMode - ? "Non hai un account? " - : "Hai già un account? ", + ? context.l10n.authScreenDontHaveAccount + : context.l10n.authScreenAlreadyHaveAccount, style: TextStyle(color: context.secondaryText), children: [ TextSpan( - text: state.isLoginMode ? "Registrati" : "Accedi", + text: state.isLoginMode + ? context.l10n.authScreenSignUp + : context.l10n.authScreenLogin, + style: TextStyle( color: context.accent, fontWeight: FontWeight.bold, @@ -169,7 +179,7 @@ class _AuthScreenState extends State { .read() .requestPasswordReset(_emailController.text.trim()), child: Text( - 'Pw dimenticata/Invito scaduto?', + context.l10n.authScreenForgotPassword, style: TextStyle( color: context.accent, fontWeight: FontWeight.bold, diff --git a/lib/features/company/bloc/company_state.dart b/lib/features/company/bloc/company_state.dart index 5cef955..1460579 100644 --- a/lib/features/company/bloc/company_state.dart +++ b/lib/features/company/bloc/company_state.dart @@ -22,5 +22,5 @@ class CompanyState extends Equatable { } @override - List get props => [status, errorMessage]; + List get props => [status, errorMessage, company]; } diff --git a/lib/features/company/data/company_repository.dart b/lib/features/company/data/company_repository.dart index 2c70f4f..5a6bb17 100644 --- a/lib/features/company/data/company_repository.dart +++ b/lib/features/company/data/company_repository.dart @@ -17,7 +17,7 @@ class CompanyRepository { } on PostgrestException catch (e) { throw e.message; } catch (e) { - throw 'Errore imprevisto durante la creazione dell\'azienda'; + throw e.toString(); } } diff --git a/lib/features/company/ui/create_company_screen.dart b/lib/features/company/ui/create_company_screen.dart index e92e7d2..c5447b3 100644 --- a/lib/features/company/ui/create_company_screen.dart +++ b/lib/features/company/ui/create_company_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/company/bloc/company_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; @@ -69,7 +70,7 @@ class _CreateCompanyScreenState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: const Text('Configurazione Azienda'), + title: Text(context.l10n.createCompanyScreenCompanyConfiguration), actions: [ IconButton( icon: const Icon(Icons.logout_rounded), @@ -98,7 +99,7 @@ class _CreateCompanyScreenState extends State { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( - state.errorMessage ?? 'Errore durante il salvataggio', + state.errorMessage ?? context.l10n.commonSavingError, ), backgroundColor: Colors.redAccent, ), @@ -118,10 +119,12 @@ class _CreateCompanyScreenState extends State { const SizedBox(height: 32), // --- SEZIONE 1: IDENTITÀ FISCALE --- - _SectionTitle(title: 'DATI FISCALI'), + _SectionTitle( + title: context.l10n.createCompanyScreenFiscalData, + ), const SizedBox(height: 16), FluxTextField( - label: 'Ragione Sociale', + label: context.l10n.createCompanyScreenCompanyName, icon: Icons.business, controller: _ragioneSocialeController, ), @@ -130,7 +133,7 @@ class _CreateCompanyScreenState extends State { children: [ Expanded( child: FluxTextField( - label: 'Partita IVA', + label: context.l10n.createCompanyScreenVatId, icon: Icons.numbers, controller: _pIvaController, ), @@ -138,7 +141,7 @@ class _CreateCompanyScreenState extends State { const SizedBox(width: 12), Expanded( child: FluxTextField( - label: 'Codice Fiscale', + label: context.l10n.createCompanyScreenFiscalCode, icon: Icons.badge_outlined, controller: _cfController, ), @@ -147,7 +150,7 @@ class _CreateCompanyScreenState extends State { ), const SizedBox(height: 16), FluxTextField( - label: 'Codice Univoco (SDI) / PEC', + label: context.l10n.createCompanyScreenSdiPec, icon: Icons.send_and_archive_outlined, controller: _univocoController, ), @@ -155,10 +158,13 @@ class _CreateCompanyScreenState extends State { const SizedBox(height: 32), // --- SEZIONE 2: SEDE LEGALE --- - _SectionTitle(title: 'SEDE LEGALE'), + _SectionTitle( + title: + context.l10n.createCompanyScreenCompanyLegalAddress, + ), const SizedBox(height: 16), FluxTextField( - label: 'Indirizzo e n. civico', + label: context.l10n.commonAddress, icon: Icons.home_work_outlined, controller: _indirizzoController, ), @@ -168,7 +174,7 @@ class _CreateCompanyScreenState extends State { Expanded( flex: 2, child: FluxTextField( - label: 'Città', + label: context.l10n.commonCity, icon: Icons.location_city, controller: _cittaController, ), @@ -176,7 +182,7 @@ class _CreateCompanyScreenState extends State { const SizedBox(width: 12), Expanded( child: FluxTextField( - label: 'CAP', + label: context.l10n.commonZipCode, icon: Icons.map_outlined, controller: _capController, ), @@ -184,7 +190,7 @@ class _CreateCompanyScreenState extends State { const SizedBox(width: 12), Expanded( child: FluxTextField( - label: 'Prov', + label: context.l10n.commonProvince, icon: Icons.explore_outlined, controller: _provinciaController, ), @@ -232,7 +238,7 @@ class _CreateCompanyScreenState extends State { Icon(Icons.cloud_upload_outlined, color: context.accent, size: 32), const SizedBox(height: 12), Text( - 'Carica Logo Aziendale', + context.l10n.createCompanyScreenUploadLogo, style: TextStyle( color: context.primaryText, fontWeight: FontWeight.bold, @@ -240,7 +246,7 @@ class _CreateCompanyScreenState extends State { ), const SizedBox(height: 4), Text( - 'Verrà usato per le tue stampe e ricevute', + context.l10n.createCompanyScreenWillBeUsedForReceipts, textAlign: TextAlign.center, style: TextStyle(color: context.secondaryText, fontSize: 12), ), @@ -259,7 +265,7 @@ class _CreateCompanyScreenState extends State { : () => _onSave(), child: state.status == CompanyStatus.loading ? const CircularProgressIndicator() - : const Text('SALVA AZIENDA'), + : Text(context.l10n.createCompanyScreenSaveCompany), ), ); } @@ -282,7 +288,7 @@ class _CreateCompanyScreenState extends State { ), const SizedBox(height: 24), Text( - 'Configura la tua Azienda', + context.l10n.createCompanyScreenSetupYourCompany, style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, color: context.primaryText, @@ -290,7 +296,7 @@ class _CreateCompanyScreenState extends State { ), const SizedBox(height: 12), Text( - 'FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.', + context.l10n.createCompanyScreenFluxNeedsYourFiscalData, style: TextStyle( color: context.secondaryText, fontSize: 15, diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index daf61c0..0831685 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -21,7 +21,7 @@ class CustomerRepository { .single(); return CustomerModel.fromMap(response); } catch (e) { - throw 'Errore durante il salvataggio del cliente: $e'; + throw '$e'; } } @@ -35,7 +35,7 @@ class CustomerRepository { .single(); return CustomerModel.fromMap(response); } catch (e) { - throw 'Errore durante la modifica del cliente: $e'; + throw '$e'; } } @@ -54,7 +54,7 @@ class CustomerRepository { return (response as List).map((c) => CustomerModel.fromMap(c)).toList(); } catch (e) { - throw 'Errore nel recupero clienti'; + throw '$e'; } } @@ -102,7 +102,7 @@ class CustomerRepository { .map((f) => CustomerFileModel.fromMap(f)) .toList(); } catch (e) { - throw 'Errore recupero file: $e'; + throw '$e'; } } @@ -131,7 +131,7 @@ class CustomerRepository { try { // Usiamo bytes invece del path per massima compatibilità if (pickedFile.bytes == null && pickedFile.path == null) { - throw 'Impossibile leggere il contenuto del file'; + throw 'File read error'; } // Se siamo su desktop/mobile abbiamo il path, su web abbiamo i bytes @@ -153,7 +153,7 @@ class CustomerRepository { return CustomerFileModel.fromMap(response); } catch (e) { - throw 'Errore durante l\'upload: $e'; + throw '$e'; } } @@ -186,14 +186,10 @@ class CustomerRepository { // 3. Cancellazione MASSIVA dallo Storage await _supabase.storage.from('documents').remove(storagePaths); - - debugPrint("Eliminati con successo ${files.length} file."); } on PostgrestException catch (e) { - debugPrint("Errore DB: ${e.message}"); - throw 'Errore database: ${e.message}'; + throw e.message; } catch (e) { - debugPrint("Errore generico: $e"); - throw 'Errore durante l\'eliminazione dei file: $e'; + throw '$e'; } } } diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index e254aeb..0d73cfa 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -58,31 +58,31 @@ class HomeScreen extends StatelessWidget { ), delegate: SliverChildListDelegate([ _buildDashboardWidget( - title: context.l10n.expiring_contracts, + title: context.l10n.homeExpiringContracts, icon: Icons.assignment_late_outlined, color: Colors.orange, context: context, ), _buildDashboardWidget( - title: context.l10n.sticky_notes, + title: context.l10n.commonStickyNotes, icon: Icons.sticky_note_2_outlined, color: Colors.yellow.shade700, context: context, ), _buildDashboardWidget( - title: context.l10n.my_tasks, + title: context.l10n.homeMyTasks, icon: Icons.check_box_outlined, color: Colors.green, context: context, ), _buildDashboardWidget( - title: context.l10n.latestServices, + title: context.l10n.homeLatestServices, icon: Icons.design_services_outlined, color: Colors.blue, context: context, ), _buildDashboardWidget( - title: 'Ultime Assistenze', + title: context.l10n.homeLatestServiceTickets, icon: Icons.support_agent_outlined, color: Colors.purple, context: context, @@ -118,7 +118,7 @@ class HomeScreen extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Bentornato, ${user!.name}! 👋", + context.l10n.homeWelcomeBack(user?.name ?? "Utente"), style: theme.textTheme.headlineMedium?.copyWith( fontWeight: FontWeight.bold, letterSpacing: -0.5, @@ -152,7 +152,7 @@ class HomeScreen extends StatelessWidget { Icon(Icons.storefront, size: 16, color: context.primary), const SizedBox(width: 8), Text( - currentStore?.nome ?? "Nessun negozio", + currentStore?.nome ?? context.l10n.homeNoStoreFound, style: TextStyle( fontWeight: FontWeight.w600, color: context.primary, @@ -184,7 +184,7 @@ class HomeScreen extends StatelessWidget { children: [ QuickActionButton( icon: Icons.add, - label: "Servizio", + label: context.l10n.commonService, color: Colors.blue, onTap: () { // Entriamo nel form! Nessun parametro extra = Nuovo Servizio @@ -194,7 +194,7 @@ class HomeScreen extends StatelessWidget { const SizedBox(width: 12), QuickActionButton( icon: Icons.handyman, - label: "Assistenza", + label: context.l10n.homeNewServiceTicket, color: Colors.redAccent, onTap: () { // TODO: Quando avrai la rotta per la nuova assistenza @@ -204,7 +204,7 @@ class HomeScreen extends StatelessWidget { const SizedBox(width: 12), QuickActionButton( icon: Icons.note_add, - label: "Nota", + label: context.l10n.commonNote, color: Colors.amber, onTap: () { // TODO: Quando faremo il modale/pagina delle note @@ -213,7 +213,7 @@ class HomeScreen extends StatelessWidget { const SizedBox(width: 12), QuickActionButton( icon: Icons.task_alt, - label: "Task", + label: context.l10n.commonTask, color: Colors.teal, onTap: () { // TODO: Quando faremo i task @@ -281,7 +281,7 @@ class HomeScreen extends StatelessWidget { const Spacer(), Center( child: Text( - "(Coming Soon)", + context.l10n.commonComingSoon, style: TextStyle( color: context.secondaryText.withValues(alpha: 0.7), fontStyle: FontStyle.italic, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb deleted file mode 100644 index 84c90f5..0000000 --- a/lib/l10n/app_en.arb +++ /dev/null @@ -1,13 +0,0 @@ -{ - "@@locale": "en", - "welcomeBack": "Welcome back, {name}! 👋", - "latestServices": "Latest Services", - "masterData": "Master Data", - "settings": "Settings", - "newService": "Service", - "expiring_contracts": "Expiring Contracts", - "sticky_notes": "Sticky Notes", - "my_tasks": "My Tasks", - "latest_service_tickets": "Latest service tickets" - -} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index b6ab9cc..24055c9 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -1,20 +1,90 @@ { "@@locale": "it", - "welcomeBack": "Bentornato, {name}! 👋", - "@welcomeBack": { + "@authCubitResetPasswordEmailSentTo": { + "placeholders": { + "email": { + "type": "String" + } + } + }, + "@authError": { + "placeholders": { + "message": { + "type": "String" + } + } + }, + "@commonComingSoon": {}, + "@commonError": { + "placeholders": { + "error": { + "type": "String" + } + } + }, + "@homeWelcomeBack": { "placeholders": { "name": { "type": "String" } } }, - "latestServices": "Ultimi Servizi", - "masterData": "Anagrafiche", - "settings": "Impostazioni", - "newService": "Servizio", - "expiring_contracts": "Contratti in scadenza", - "sticky_notes": "Sticky Notes", - "my_tasks": "Mie Attività", - "latest_service_tickets": "Ultime assistenze" - + "authCubitCheckEmailToConfirmAccount": "Controlla la tua email per confermare l'account!", + "authCubitResetPasswordEmailSentTo": "Email per reset password inviata a {email}!", + "authError": "Errore di autenticazione: {message}", + "authScreenAlreadyHaveAccount": "Hai già un account?", + "authScreenBusinessEmail": "Email aziendale", + "authScreenCreateAccount": "CREA ACCOUNT", + "authScreenDontHaveAccount": "Non hai un account?", + "authScreenForgotPassword": "Password dimenticata/Invito scaduto?", + "authScreenLogin": "LOGIN", + "authScreenLoginToManageYourBusiness": "Accedi per gestire il tuo business", + "authScreenSignUp": "REGISTRATI", + "authScreenStartTodayToDigitalizeYourStore": "Inizia oggi a digitalizzare il tuo negozio", + "authScreenWelcomeBack": "BENTORNATO", + "commonClose": "Chiudi", + "commonComingSoon": "Coming soon", + "commonDashboard": "Panoramica", + "commonError": "Si è verificato un errore: {error}", + "commonMasterData": "Anagrafiche", + "commonNewPassword": "Nuova Password", + "commonNote": "Nota", + "commonSave": "Salva", + "commonService": "Servizio", + "commonSettings": "Impostazioni", + "commonStickyNotes": "Sticky Notes", + "commonTask": "Attività", + "homeExpiringContracts": "Contratti in scadenza", + "homeLatestServiceTickets": "Ultime assistenze", + "homeLatestServices": "Ultimi Servizi", + "homeMyTasks": "Mie Attività", + "homeNewServiceTicket": "Nuova assistenza", + "homeNoStoreFound": "Nessun negozio trovato", + "homeWelcomeBack": "Bentornato, {name}! 👋", + "imageViewerWidgetErrorOpening": "Errore durante l'apertura dell'immagine", + "pdfViewerAnteprimaPdf": "Anteprima PDF", + "setPasswordInviteAcceptedChoosePassword": "Hai accettato l'invito. Scegli una password sicura per accedere in futuro.", + "setPasswordScreenAtLeast6Chars": "La password deve avere almeno 6 caratteri", + "setPasswordScreenPasswordSetWelcome": "Password impostata! Benvenuto a bordo 🚀", + "setPasswordScreenSaveAndStart": "SALVA E INIZIA", + "setPasswordScreenSetPassword": "Imposta una nuova Password", + "setPasswordScreenWelcomeInFlux": "Benvenuto in FLUX!", + "createCompanyScreenCompanyConfiguration": "Configurazione Azienda", + "commonSavingError": "Errore durante il salvataggio", + "createCompanyScreenFiscalData": "DATI FISCALI", + "createCompanyScreenCompanyName": "Ragione Sociale", + "createCompanyScreenVatId": "Partita IVA", + "createCompanyScreenFiscalCode": "Codice Fiscale", + "createCompanyScreenSdiPec": "Codice Univoco (SDI) / PEC", + "createCompanyScreenCompanyLegalAddress": "SEDE LEGALE", + "commonAddress": "Indirizzo e n. civico", + "commonCity": "Città", + "commonZipCode": "CAP", + "commonProvince": "Prov", + "commonCountry": "Paese", + "createCompanyScreenUploadLogo": "Carica Logo Aziendale", + "createCompanyScreenWillBeUsedForReceipts": "Verrà utilizzato per le tue stampe e ricevute", + "createCompanyScreenSaveCompany": "SALVA AZIENDA", + "createCompanyScreenSetupYourCompany": "Configura la tua Azienda", + "createCompanyScreenFluxNeedsYourFiscalData": "FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi." } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 58547fa..c2b13f8 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -5,7 +5,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/intl.dart' as intl; -import 'app_localizations_en.dart'; import 'app_localizations_it.dart'; // ignore_for_file: type=lint @@ -93,64 +92,355 @@ abstract class AppLocalizations { ]; /// A list of this localizations delegate's supported locales. - static const List supportedLocales = [ - Locale('en'), - Locale('it'), - ]; + static const List supportedLocales = [Locale('it')]; - /// No description provided for @welcomeBack. + /// No description provided for @authCubitCheckEmailToConfirmAccount. /// /// In it, this message translates to: - /// **'Bentornato, {name}! 👋'** - String welcomeBack(String name); + /// **'Controlla la tua email per confermare l\'account!'** + String get authCubitCheckEmailToConfirmAccount; - /// No description provided for @latestServices. + /// No description provided for @authCubitResetPasswordEmailSentTo. /// /// In it, this message translates to: - /// **'Ultimi Servizi'** - String get latestServices; + /// **'Email per reset password inviata a {email}!'** + String authCubitResetPasswordEmailSentTo(String email); - /// No description provided for @masterData. + /// No description provided for @authError. + /// + /// In it, this message translates to: + /// **'Errore di autenticazione: {message}'** + String authError(String message); + + /// No description provided for @authScreenAlreadyHaveAccount. + /// + /// In it, this message translates to: + /// **'Hai già un account?'** + String get authScreenAlreadyHaveAccount; + + /// No description provided for @authScreenBusinessEmail. + /// + /// In it, this message translates to: + /// **'Email aziendale'** + String get authScreenBusinessEmail; + + /// No description provided for @authScreenCreateAccount. + /// + /// In it, this message translates to: + /// **'CREA ACCOUNT'** + String get authScreenCreateAccount; + + /// No description provided for @authScreenDontHaveAccount. + /// + /// In it, this message translates to: + /// **'Non hai un account?'** + String get authScreenDontHaveAccount; + + /// No description provided for @authScreenForgotPassword. + /// + /// In it, this message translates to: + /// **'Password dimenticata/Invito scaduto?'** + String get authScreenForgotPassword; + + /// No description provided for @authScreenLogin. + /// + /// In it, this message translates to: + /// **'LOGIN'** + String get authScreenLogin; + + /// No description provided for @authScreenLoginToManageYourBusiness. + /// + /// In it, this message translates to: + /// **'Accedi per gestire il tuo business'** + String get authScreenLoginToManageYourBusiness; + + /// No description provided for @authScreenSignUp. + /// + /// In it, this message translates to: + /// **'REGISTRATI'** + String get authScreenSignUp; + + /// No description provided for @authScreenStartTodayToDigitalizeYourStore. + /// + /// In it, this message translates to: + /// **'Inizia oggi a digitalizzare il tuo negozio'** + String get authScreenStartTodayToDigitalizeYourStore; + + /// No description provided for @authScreenWelcomeBack. + /// + /// In it, this message translates to: + /// **'BENTORNATO'** + String get authScreenWelcomeBack; + + /// No description provided for @commonClose. + /// + /// In it, this message translates to: + /// **'Chiudi'** + String get commonClose; + + /// No description provided for @commonComingSoon. + /// + /// In it, this message translates to: + /// **'Coming soon'** + String get commonComingSoon; + + /// No description provided for @commonDashboard. + /// + /// In it, this message translates to: + /// **'Panoramica'** + String get commonDashboard; + + /// No description provided for @commonError. + /// + /// In it, this message translates to: + /// **'Si è verificato un errore: {error}'** + String commonError(String error); + + /// No description provided for @commonMasterData. /// /// In it, this message translates to: /// **'Anagrafiche'** - String get masterData; + String get commonMasterData; - /// No description provided for @settings. + /// No description provided for @commonNewPassword. /// /// In it, this message translates to: - /// **'Impostazioni'** - String get settings; + /// **'Nuova Password'** + String get commonNewPassword; - /// No description provided for @newService. + /// No description provided for @commonNote. + /// + /// In it, this message translates to: + /// **'Nota'** + String get commonNote; + + /// No description provided for @commonSave. + /// + /// In it, this message translates to: + /// **'Salva'** + String get commonSave; + + /// No description provided for @commonService. /// /// In it, this message translates to: /// **'Servizio'** - String get newService; + String get commonService; - /// No description provided for @expiring_contracts. + /// No description provided for @commonSettings. /// /// In it, this message translates to: - /// **'Contratti in scadenza'** - String get expiring_contracts; + /// **'Impostazioni'** + String get commonSettings; - /// No description provided for @sticky_notes. + /// No description provided for @commonStickyNotes. /// /// In it, this message translates to: /// **'Sticky Notes'** - String get sticky_notes; + String get commonStickyNotes; - /// No description provided for @my_tasks. + /// No description provided for @commonTask. /// /// In it, this message translates to: - /// **'Mie Attività'** - String get my_tasks; + /// **'Attività'** + String get commonTask; - /// No description provided for @latest_service_tickets. + /// No description provided for @homeExpiringContracts. + /// + /// In it, this message translates to: + /// **'Contratti in scadenza'** + String get homeExpiringContracts; + + /// No description provided for @homeLatestServiceTickets. /// /// In it, this message translates to: /// **'Ultime assistenze'** - String get latest_service_tickets; + String get homeLatestServiceTickets; + + /// No description provided for @homeLatestServices. + /// + /// In it, this message translates to: + /// **'Ultimi Servizi'** + String get homeLatestServices; + + /// No description provided for @homeMyTasks. + /// + /// In it, this message translates to: + /// **'Mie Attività'** + String get homeMyTasks; + + /// No description provided for @homeNewServiceTicket. + /// + /// In it, this message translates to: + /// **'Nuova assistenza'** + String get homeNewServiceTicket; + + /// No description provided for @homeNoStoreFound. + /// + /// In it, this message translates to: + /// **'Nessun negozio trovato'** + String get homeNoStoreFound; + + /// No description provided for @homeWelcomeBack. + /// + /// In it, this message translates to: + /// **'Bentornato, {name}! 👋'** + String homeWelcomeBack(String name); + + /// No description provided for @imageViewerWidgetErrorOpening. + /// + /// In it, this message translates to: + /// **'Errore durante l\'apertura dell\'immagine'** + String get imageViewerWidgetErrorOpening; + + /// No description provided for @pdfViewerAnteprimaPdf. + /// + /// In it, this message translates to: + /// **'Anteprima PDF'** + String get pdfViewerAnteprimaPdf; + + /// No description provided for @setPasswordInviteAcceptedChoosePassword. + /// + /// In it, this message translates to: + /// **'Hai accettato l\'invito. Scegli una password sicura per accedere in futuro.'** + String get setPasswordInviteAcceptedChoosePassword; + + /// No description provided for @setPasswordScreenAtLeast6Chars. + /// + /// In it, this message translates to: + /// **'La password deve avere almeno 6 caratteri'** + String get setPasswordScreenAtLeast6Chars; + + /// No description provided for @setPasswordScreenPasswordSetWelcome. + /// + /// In it, this message translates to: + /// **'Password impostata! Benvenuto a bordo 🚀'** + String get setPasswordScreenPasswordSetWelcome; + + /// No description provided for @setPasswordScreenSaveAndStart. + /// + /// In it, this message translates to: + /// **'SALVA E INIZIA'** + String get setPasswordScreenSaveAndStart; + + /// No description provided for @setPasswordScreenSetPassword. + /// + /// In it, this message translates to: + /// **'Imposta una nuova Password'** + String get setPasswordScreenSetPassword; + + /// No description provided for @setPasswordScreenWelcomeInFlux. + /// + /// In it, this message translates to: + /// **'Benvenuto in FLUX!'** + String get setPasswordScreenWelcomeInFlux; + + /// No description provided for @createCompanyScreenCompanyConfiguration. + /// + /// In it, this message translates to: + /// **'Configurazione Azienda'** + String get createCompanyScreenCompanyConfiguration; + + /// No description provided for @commonSavingError. + /// + /// In it, this message translates to: + /// **'Errore durante il salvataggio'** + String get commonSavingError; + + /// No description provided for @createCompanyScreenFiscalData. + /// + /// In it, this message translates to: + /// **'DATI FISCALI'** + String get createCompanyScreenFiscalData; + + /// No description provided for @createCompanyScreenCompanyName. + /// + /// In it, this message translates to: + /// **'Ragione Sociale'** + String get createCompanyScreenCompanyName; + + /// No description provided for @createCompanyScreenVatId. + /// + /// In it, this message translates to: + /// **'Partita IVA'** + String get createCompanyScreenVatId; + + /// No description provided for @createCompanyScreenFiscalCode. + /// + /// In it, this message translates to: + /// **'Codice Fiscale'** + String get createCompanyScreenFiscalCode; + + /// No description provided for @createCompanyScreenSdiPec. + /// + /// In it, this message translates to: + /// **'Codice Univoco (SDI) / PEC'** + String get createCompanyScreenSdiPec; + + /// No description provided for @createCompanyScreenCompanyLegalAddress. + /// + /// In it, this message translates to: + /// **'SEDE LEGALE'** + String get createCompanyScreenCompanyLegalAddress; + + /// No description provided for @commonAddress. + /// + /// In it, this message translates to: + /// **'Indirizzo e n. civico'** + String get commonAddress; + + /// No description provided for @commonCity. + /// + /// In it, this message translates to: + /// **'Città'** + String get commonCity; + + /// No description provided for @commonZipCode. + /// + /// In it, this message translates to: + /// **'CAP'** + String get commonZipCode; + + /// No description provided for @commonProvince. + /// + /// In it, this message translates to: + /// **'Prov'** + String get commonProvince; + + /// No description provided for @commonCountry. + /// + /// In it, this message translates to: + /// **'Paese'** + String get commonCountry; + + /// No description provided for @createCompanyScreenUploadLogo. + /// + /// In it, this message translates to: + /// **'Carica Logo Aziendale'** + String get createCompanyScreenUploadLogo; + + /// No description provided for @createCompanyScreenWillBeUsedForReceipts. + /// + /// In it, this message translates to: + /// **'Verrà utilizzato per le tue stampe e ricevute'** + String get createCompanyScreenWillBeUsedForReceipts; + + /// No description provided for @createCompanyScreenSaveCompany. + /// + /// In it, this message translates to: + /// **'SALVA AZIENDA'** + String get createCompanyScreenSaveCompany; + + /// No description provided for @createCompanyScreenSetupYourCompany. + /// + /// In it, this message translates to: + /// **'Configura la tua Azienda'** + String get createCompanyScreenSetupYourCompany; + + /// No description provided for @createCompanyScreenFluxNeedsYourFiscalData. + /// + /// In it, this message translates to: + /// **'FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.'** + String get createCompanyScreenFluxNeedsYourFiscalData; } class _AppLocalizationsDelegate @@ -164,7 +454,7 @@ class _AppLocalizationsDelegate @override bool isSupported(Locale locale) => - ['en', 'it'].contains(locale.languageCode); + ['it'].contains(locale.languageCode); @override bool shouldReload(_AppLocalizationsDelegate old) => false; @@ -173,8 +463,6 @@ class _AppLocalizationsDelegate AppLocalizations lookupAppLocalizations(Locale locale) { // Lookup logic when only language code is specified. switch (locale.languageCode) { - case 'en': - return AppLocalizationsEn(); case 'it': return AppLocalizationsIt(); } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart deleted file mode 100644 index f5dea85..0000000 --- a/lib/l10n/app_localizations_en.dart +++ /dev/null @@ -1,39 +0,0 @@ -// ignore: unused_import -import 'package:intl/intl.dart' as intl; -import 'app_localizations.dart'; - -// ignore_for_file: type=lint - -/// The translations for English (`en`). -class AppLocalizationsEn extends AppLocalizations { - AppLocalizationsEn([String locale = 'en']) : super(locale); - - @override - String welcomeBack(String name) { - return 'Welcome back, $name! 👋'; - } - - @override - String get latestServices => 'Latest Services'; - - @override - String get masterData => 'Master Data'; - - @override - String get settings => 'Settings'; - - @override - String get newService => 'Service'; - - @override - String get expiring_contracts => 'Expiring Contracts'; - - @override - String get sticky_notes => 'Sticky Notes'; - - @override - String get my_tasks => 'My Tasks'; - - @override - String get latest_service_tickets => 'Latest service tickets'; -} diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 434a39d..5ca322c 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -9,31 +9,194 @@ class AppLocalizationsIt extends AppLocalizations { AppLocalizationsIt([String locale = 'it']) : super(locale); @override - String welcomeBack(String name) { + String get authCubitCheckEmailToConfirmAccount => + 'Controlla la tua email per confermare l\'account!'; + + @override + String authCubitResetPasswordEmailSentTo(String email) { + return 'Email per reset password inviata a $email!'; + } + + @override + String authError(String message) { + return 'Errore di autenticazione: $message'; + } + + @override + String get authScreenAlreadyHaveAccount => 'Hai già un account?'; + + @override + String get authScreenBusinessEmail => 'Email aziendale'; + + @override + String get authScreenCreateAccount => 'CREA ACCOUNT'; + + @override + String get authScreenDontHaveAccount => 'Non hai un account?'; + + @override + String get authScreenForgotPassword => 'Password dimenticata/Invito scaduto?'; + + @override + String get authScreenLogin => 'LOGIN'; + + @override + String get authScreenLoginToManageYourBusiness => + 'Accedi per gestire il tuo business'; + + @override + String get authScreenSignUp => 'REGISTRATI'; + + @override + String get authScreenStartTodayToDigitalizeYourStore => + 'Inizia oggi a digitalizzare il tuo negozio'; + + @override + String get authScreenWelcomeBack => 'BENTORNATO'; + + @override + String get commonClose => 'Chiudi'; + + @override + String get commonComingSoon => 'Coming soon'; + + @override + String get commonDashboard => 'Panoramica'; + + @override + String commonError(String error) { + return 'Si è verificato un errore: $error'; + } + + @override + String get commonMasterData => 'Anagrafiche'; + + @override + String get commonNewPassword => 'Nuova Password'; + + @override + String get commonNote => 'Nota'; + + @override + String get commonSave => 'Salva'; + + @override + String get commonService => 'Servizio'; + + @override + String get commonSettings => 'Impostazioni'; + + @override + String get commonStickyNotes => 'Sticky Notes'; + + @override + String get commonTask => 'Attività'; + + @override + String get homeExpiringContracts => 'Contratti in scadenza'; + + @override + String get homeLatestServiceTickets => 'Ultime assistenze'; + + @override + String get homeLatestServices => 'Ultimi Servizi'; + + @override + String get homeMyTasks => 'Mie Attività'; + + @override + String get homeNewServiceTicket => 'Nuova assistenza'; + + @override + String get homeNoStoreFound => 'Nessun negozio trovato'; + + @override + String homeWelcomeBack(String name) { return 'Bentornato, $name! 👋'; } @override - String get latestServices => 'Ultimi Servizi'; + String get imageViewerWidgetErrorOpening => + 'Errore durante l\'apertura dell\'immagine'; @override - String get masterData => 'Anagrafiche'; + String get pdfViewerAnteprimaPdf => 'Anteprima PDF'; @override - String get settings => 'Impostazioni'; + String get setPasswordInviteAcceptedChoosePassword => + 'Hai accettato l\'invito. Scegli una password sicura per accedere in futuro.'; @override - String get newService => 'Servizio'; + String get setPasswordScreenAtLeast6Chars => + 'La password deve avere almeno 6 caratteri'; @override - String get expiring_contracts => 'Contratti in scadenza'; + String get setPasswordScreenPasswordSetWelcome => + 'Password impostata! Benvenuto a bordo 🚀'; @override - String get sticky_notes => 'Sticky Notes'; + String get setPasswordScreenSaveAndStart => 'SALVA E INIZIA'; @override - String get my_tasks => 'Mie Attività'; + String get setPasswordScreenSetPassword => 'Imposta una nuova Password'; @override - String get latest_service_tickets => 'Ultime assistenze'; + String get setPasswordScreenWelcomeInFlux => 'Benvenuto in FLUX!'; + + @override + String get createCompanyScreenCompanyConfiguration => + 'Configurazione Azienda'; + + @override + String get commonSavingError => 'Errore durante il salvataggio'; + + @override + String get createCompanyScreenFiscalData => 'DATI FISCALI'; + + @override + String get createCompanyScreenCompanyName => 'Ragione Sociale'; + + @override + String get createCompanyScreenVatId => 'Partita IVA'; + + @override + String get createCompanyScreenFiscalCode => 'Codice Fiscale'; + + @override + String get createCompanyScreenSdiPec => 'Codice Univoco (SDI) / PEC'; + + @override + String get createCompanyScreenCompanyLegalAddress => 'SEDE LEGALE'; + + @override + String get commonAddress => 'Indirizzo e n. civico'; + + @override + String get commonCity => 'Città'; + + @override + String get commonZipCode => 'CAP'; + + @override + String get commonProvince => 'Prov'; + + @override + String get commonCountry => 'Paese'; + + @override + String get createCompanyScreenUploadLogo => 'Carica Logo Aziendale'; + + @override + String get createCompanyScreenWillBeUsedForReceipts => + 'Verrà utilizzato per le tue stampe e ricevute'; + + @override + String get createCompanyScreenSaveCompany => 'SALVA AZIENDA'; + + @override + String get createCompanyScreenSetupYourCompany => 'Configura la tua Azienda'; + + @override + String get createCompanyScreenFluxNeedsYourFiscalData => + 'FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.'; } diff --git a/lib/main.dart b/lib/main.dart index 3e64a34..d8b2451 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -155,9 +155,7 @@ class _FluxAppState extends State { routerConfig: _router, localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, - locale: const Locale( - 'it', - ), // Per ora forziamo l'italiano, poi lo renderemo dinamico! + locale: const Locale('it'), ); }, ); diff --git a/pubspec.yaml b/pubspec.yaml index 1bef9ab..f511d7a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,8 +28,6 @@ dependencies: qr_flutter: ^4.1.0 shared_preferences: ^2.5.5 supabase_flutter: ^2.12.2 - - dev_dependencies: flutter_test: @@ -43,4 +41,4 @@ flutter: assets: - assets/images/ - assets/svg/ - - .env \ No newline at end of file + - .env -- 2.43.0 From a562606613bece1edd7600fb7b4f209d46abfa59 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Thu, 30 Apr 2026 12:39:25 +0200 Subject: [PATCH 04/18] latest services ok Co-authored-by: Copilot --- lib/core/routes/app_router.dart | 16 ++-- .../bloc/latest_store_services_bloc.dart | 41 ++++++--- .../bloc/latest_store_services_events.dart | 4 +- .../ui/latest_store_services_card.dart | 84 ++++++++++++------- lib/features/home/ui/home_screen.dart | 9 +- lib/l10n/app_en.arb | 13 +++ lib/l10n/app_localizations_en.dart | 57 +++++++++++++ lib/l10n/intl_en.arb | 1 + lib/main.dart | 1 + 9 files changed, 171 insertions(+), 55 deletions(-) create mode 100644 lib/l10n/app_en.arb create mode 100644 lib/l10n/app_localizations_en.dart create mode 100644 lib/l10n/intl_en.arb diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 2008f95..2a942e8 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -12,6 +12,7 @@ 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_mobile_upload_screen.dart'; import 'package:flux/features/customers/ui/customers_content.dart'; +import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.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/products/ui/products_screen.dart'; @@ -24,6 +25,7 @@ import 'package:flux/features/services/blocs/service_files_bloc.dart'; import 'package:flux/features/services/models/service_model.dart'; import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart'; import 'package:flux/features/services/ui/service_form_screen/service_mobile_upload_screen.dart'; +import 'package:flux/features/services/ui/services_screen.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; @@ -128,15 +130,19 @@ class AppRouter { ), ), ), + GoRoute( + path: '/services', + builder: (context, state) => const ServicesScreen(), + ), + GoRoute( + path: '/customers', + builder: (context, state) => + const CustomersContent(), // O come si chiama il tuo widget della lista! + ), ], ), // --- DETTAGLI E OPERATIVITÀ (FUORI DALLA SHELL - TUTTO SCHERMO) --- - GoRoute( - path: '/customers', - builder: (context, state) => - const CustomersContent(), // O come si chiama il tuo widget della lista! - ), GoRoute( path: '/customer/:id', builder: (context, state) { diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart index 2593922..9cd5b94 100644 --- a/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart +++ b/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flux/features/services/data/services_repository.dart'; @@ -17,19 +19,38 @@ class LatestStoreServicesBloc status: LatestStoreServicesStatus.initial, ), ) { - on((event, emit) async { + on((event, emit) async { emit(state.copyWith(status: LatestStoreServicesStatus.loading)); - try { + // 1. Creiamo uno stream "intermedio" che idrata i dati + final hydratedStream = _repository + .getLastStoreServicesStream(storeId: event.storeId, limit: 5) + .asyncMap((List rawServices) async { + // Questo gira ad ogni "scatto" dello stream di Supabase + List fullyHydratedServices = []; + + for (ServiceModel service in rawServices) { + // Peschiamo i dati completi (incluso il cliente) + ServiceModel fullService = await _repository.fetchServiceById( + service.id!, + ); + fullyHydratedServices.add(fullService); + } + + // Passiamo la lista completa allo step successivo + return fullyHydratedServices; + }); + + // 2. Ora passiamo lo stream idratato all'emit.forEach await emit.forEach( - _repository.getLastStoreServicesStream( - storeId: event.storeId, - limit: 5, - ), - onData: (List data) => state.copyWith( - status: LatestStoreServicesStatus.success, - services: data, - ), + hydratedStream, // Usiamo lo stream modificato! + onData: (List fullyHydratedServices) { + // Qui ora è tutto sincrono e bellissimo + return state.copyWith( + services: fullyHydratedServices, + status: LatestStoreServicesStatus.success, + ); + }, onError: (error, stackTrace) => state.copyWith( status: LatestStoreServicesStatus.failure, error: error.toString(), diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart index 9550022..b66128d 100644 --- a/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart +++ b/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart @@ -7,10 +7,10 @@ sealed class LatestStoreServicesEvent extends Equatable { List get props => []; } -class InitLastServicesEvent extends LatestStoreServicesEvent { +class InitLastStoreServicesEvent extends LatestStoreServicesEvent { final String storeId; - const InitLastServicesEvent(this.storeId); + const InitLastStoreServicesEvent(this.storeId); @override List get props => [storeId]; diff --git a/lib/features/home/latest_store_services/ui/latest_store_services_card.dart b/lib/features/home/latest_store_services/ui/latest_store_services_card.dart index 3fe63b2..368b7dc 100644 --- a/lib/features/home/latest_store_services/ui/latest_store_services_card.dart +++ b/lib/features/home/latest_store_services/ui/latest_store_services_card.dart @@ -2,7 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.dart'; +import 'package:go_router/go_router.dart'; class LatestStoreServicesCard extends StatelessWidget { const LatestStoreServicesCard({super.key}); @@ -15,7 +17,7 @@ class LatestStoreServicesCard extends StatelessWidget { // 1. Creiamo il Bloc e facciamo partire subito la query create: (context) => LatestStoreServicesBloc() - ..add(InitLastServicesEvent(currentStoreId ?? '')), + ..add(InitLastStoreServicesEvent(currentStoreId ?? '')), child: BlocListener( // 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream! listenWhen: (previous, current) => @@ -23,7 +25,7 @@ class LatestStoreServicesCard extends StatelessWidget { listener: (context, state) { if (state.currentStore?.id != null) { context.read().add( - InitLastServicesEvent(state.currentStore!.id!), + InitLastStoreServicesEvent(state.currentStore!.id!), ); } }, @@ -67,15 +69,18 @@ class _LatestServicesCardContent extends StatelessWidget { ), const SizedBox(width: 12), Expanded( - child: Text( - "Ultimi Servizi", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: context.primaryText, + child: TextButton( + onPressed: () => context.push('/services'), + child: Text( + context.l10n.homeLatestServices, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: context.primaryText, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), ), ], @@ -120,31 +125,46 @@ class _LatestServicesCardContent extends StatelessWidget { ), itemBuilder: (context, index) { final service = state.services[index]; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - service.number, - style: TextStyle( - fontWeight: FontWeight.w600, - color: context.primaryText, + return InkWell( + onTap: () => + context.push('/service-form', extra: service), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 5, + child: Text( + service.customerDisplayName ?? + 'Cliente sconosciuto', + style: TextStyle( + fontWeight: FontWeight.w700, + color: context.primaryText, + ), ), - maxLines: 1, - overflow: TextOverflow.ellipsis, ), - ), - // Se hai formattato la data, stampala qui (es. 12/04/2026) - Text( - "${service.createdAt?.day}/${service.createdAt?.month}", - style: TextStyle( - color: context.secondaryText, - fontSize: 12, + Expanded( + flex: 5, + child: Text( + service.number, + style: TextStyle( + fontWeight: FontWeight.w600, + color: context.primaryText, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - ), - ], + Text( + "${service.createdAt?.day}/${service.createdAt?.month}", + style: TextStyle( + color: context.secondaryText, + fontSize: 12, + ), + ), + ], + ), ), ); }, diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 0d73cfa..91ab08c 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/utils/extensions.dart'; +import 'package:flux/features/home/latest_store_services/ui/latest_store_services_card.dart'; import 'package:flux/features/home/ui/quick_actions_widget.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:go_router/go_router.dart'; @@ -75,12 +76,8 @@ class HomeScreen extends StatelessWidget { color: Colors.green, context: context, ), - _buildDashboardWidget( - title: context.l10n.homeLatestServices, - icon: Icons.design_services_outlined, - color: Colors.blue, - context: context, - ), + LatestStoreServicesCard(), + _buildDashboardWidget( title: context.l10n.homeLatestServiceTickets, icon: Icons.support_agent_outlined, diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb new file mode 100644 index 0000000..84c90f5 --- /dev/null +++ b/lib/l10n/app_en.arb @@ -0,0 +1,13 @@ +{ + "@@locale": "en", + "welcomeBack": "Welcome back, {name}! 👋", + "latestServices": "Latest Services", + "masterData": "Master Data", + "settings": "Settings", + "newService": "Service", + "expiring_contracts": "Expiring Contracts", + "sticky_notes": "Sticky Notes", + "my_tasks": "My Tasks", + "latest_service_tickets": "Latest service tickets" + +} \ No newline at end of file diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart new file mode 100644 index 0000000..eeebff4 --- /dev/null +++ b/lib/l10n/app_localizations_en.dart @@ -0,0 +1,57 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'app_localizations.dart'; + +// ignore_for_file: type=lint + +/// The translations for English (`en`). +class AppLocalizationsEn extends AppLocalizations { + AppLocalizationsEn([String locale = 'en']) : super(locale); + + @override + String get homeExpiringContracts => 'Contratti in scadenza'; + + @override + String get homeLatestServiceTickets => 'Ultime assistenze'; + + @override + String get homeLatestServices => 'Ultimi Servizi'; + + @override + String get homeMasterData => 'Anagrafiche'; + + @override + String get homeMyTasks => 'Mie Attività'; + + @override + String get homeNewService => 'Servizio'; + + @override + String get homeSettings => 'Impostazioni'; + + @override + String get homeStickyNotes => 'Sticky Notes'; + + @override + String homeWelcomeBack(String name) { + return 'Bentornato, $name! 👋'; + } + + @override + String get homeNoStoreFound => 'Nessun negozio trovato'; + + @override + String get homeNewServiceTicket => 'Nuova assistenza'; + + @override + String get homeNewNote => 'Nota'; + + @override + String get homeNewTask => 'Attività'; + + @override + String get commonComingSoon => 'Coming soon'; + + @override + String get commonDashboard => 'Panoramica'; +} diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/lib/l10n/intl_en.arb @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index d8b2451..4130cd2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flux/features/auth/bloc/auth_cubit.dart'; +import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.dart'; import 'package:flux/l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; -- 2.43.0 From 87b4661d33bad592c21b164666c953ffec32a6c9 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Fri, 1 May 2026 09:41:48 +0200 Subject: [PATCH 05/18] renamed services folder to operations --- lib/core/routes/app_router.dart | 10 +++++----- .../customers/ui/customer_search_sheet.dart | 2 +- .../bloc/latest_store_services_bloc.dart | 4 ++-- .../blocs/service_files_bloc.dart | 6 +++--- .../blocs/service_files_events.dart | 0 .../blocs/service_files_state.dart | 0 .../blocs/services_cubit.dart | 12 +++++------ .../blocs/services_state.dart | 0 .../data/services_repository.dart | 2 +- .../models/energy_service_model.dart | 0 .../models/entertainment_service_model.dart | 0 .../models/fin_service_model.dart | 0 .../models/service_file_model.dart | 0 .../models/service_model.dart | 8 ++++---- .../ui/service_action_card.dart | 0 .../ui/service_form_screen/action_card.dart | 0 .../attachment_section.dart | 6 +++--- .../service_form_screen/customer_section.dart | 2 +- .../energy_service_dialog.dart | 2 +- .../entertainment_service_card.dart | 4 ++-- .../finance_service_dialog.dart | 2 +- .../general_info_section.dart | 4 ++-- .../ui/service_form_screen/int_dialogs.dart | 0 .../service_form_screen.dart | 12 +++++------ .../service_mobile_upload_screen.dart | 2 +- .../ui/service_form_screen/services_grid.dart | 20 +++++++++---------- .../ui/services_screen.dart | 6 +++--- .../utils/service_actions.dart | 4 ++-- lib/main.dart | 4 ++-- 29 files changed, 56 insertions(+), 56 deletions(-) rename lib/features/{services => operations}/blocs/service_files_bloc.dart (97%) rename lib/features/{services => operations}/blocs/service_files_events.dart (100%) rename lib/features/{services => operations}/blocs/service_files_state.dart (100%) rename lib/features/{services => operations}/blocs/services_cubit.dart (95%) rename lib/features/{services => operations}/blocs/services_state.dart (100%) rename lib/features/{services => operations}/data/services_repository.dart (99%) rename lib/features/{services => operations}/models/energy_service_model.dart (100%) rename lib/features/{services => operations}/models/entertainment_service_model.dart (100%) rename lib/features/{services => operations}/models/fin_service_model.dart (100%) rename lib/features/{services => operations}/models/service_file_model.dart (100%) rename lib/features/{services => operations}/models/service_model.dart (94%) rename lib/features/{services => operations}/ui/service_action_card.dart (100%) rename lib/features/{services => operations}/ui/service_form_screen/action_card.dart (100%) rename lib/features/{services => operations}/ui/service_form_screen/attachment_section.dart (98%) rename lib/features/{services => operations}/ui/service_form_screen/customer_section.dart (97%) rename lib/features/{services => operations}/ui/service_form_screen/energy_service_dialog.dart (99%) rename lib/features/{services => operations}/ui/service_form_screen/entertainment_service_card.dart (98%) rename lib/features/{services => operations}/ui/service_form_screen/finance_service_dialog.dart (99%) rename lib/features/{services => operations}/ui/service_form_screen/general_info_section.dart (96%) rename lib/features/{services => operations}/ui/service_form_screen/int_dialogs.dart (100%) rename lib/features/{services => operations}/ui/service_form_screen/service_form_screen.dart (92%) rename lib/features/{services => operations}/ui/service_form_screen/service_mobile_upload_screen.dart (99%) rename lib/features/{services => operations}/ui/service_form_screen/services_grid.dart (89%) rename lib/features/{services => operations}/ui/services_screen.dart (96%) rename lib/features/{services => operations}/utils/service_actions.dart (95%) diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 2a942e8..be1f6ed 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -21,11 +21,11 @@ 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/onboarding/blocs/onboarding_cubit.dart'; import 'package:flux/features/onboarding/ui/onboarding_screen.dart'; -import 'package:flux/features/services/blocs/service_files_bloc.dart'; -import 'package:flux/features/services/models/service_model.dart'; -import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart'; -import 'package:flux/features/services/ui/service_form_screen/service_mobile_upload_screen.dart'; -import 'package:flux/features/services/ui/services_screen.dart'; +import 'package:flux/features/operations/blocs/service_files_bloc.dart'; +import 'package:flux/features/operations/models/service_model.dart'; +import 'package:flux/features/operations/ui/service_form_screen/service_form_screen.dart'; +import 'package:flux/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart'; +import 'package:flux/features/operations/ui/services_screen.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/features/customers/ui/customer_search_sheet.dart b/lib/features/customers/ui/customer_search_sheet.dart index 4cc3ca9..c12609c 100644 --- a/lib/features/customers/ui/customer_search_sheet.dart +++ b/lib/features/customers/ui/customer_search_sheet.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/customers/blocs/customer_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/quick_customer_dialog.dart'; -import 'package:flux/features/services/blocs/services_cubit.dart'; +import 'package:flux/features/operations/blocs/services_cubit.dart'; class CustomerSearchSheet extends StatefulWidget { const CustomerSearchSheet({super.key}); diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart index 9cd5b94..a85c6c9 100644 --- a/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart +++ b/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart @@ -2,8 +2,8 @@ import 'dart:developer'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; -import 'package:flux/features/services/data/services_repository.dart'; -import 'package:flux/features/services/models/service_model.dart'; +import 'package:flux/features/operations/data/services_repository.dart'; +import 'package:flux/features/operations/models/service_model.dart'; import 'package:get_it/get_it.dart'; part 'latest_store_services_events.dart'; diff --git a/lib/features/services/blocs/service_files_bloc.dart b/lib/features/operations/blocs/service_files_bloc.dart similarity index 97% rename from lib/features/services/blocs/service_files_bloc.dart rename to lib/features/operations/blocs/service_files_bloc.dart index 02f1922..f34f86d 100644 --- a/lib/features/services/blocs/service_files_bloc.dart +++ b/lib/features/operations/blocs/service_files_bloc.dart @@ -5,9 +5,9 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/services/data/services_repository.dart'; -import 'package:flux/features/services/models/service_file_model.dart'; -import 'package:flux/features/services/models/service_model.dart'; +import 'package:flux/features/operations/data/services_repository.dart'; +import 'package:flux/features/operations/models/service_file_model.dart'; +import 'package:flux/features/operations/models/service_model.dart'; import 'package:get_it/get_it.dart'; part 'service_files_events.dart'; diff --git a/lib/features/services/blocs/service_files_events.dart b/lib/features/operations/blocs/service_files_events.dart similarity index 100% rename from lib/features/services/blocs/service_files_events.dart rename to lib/features/operations/blocs/service_files_events.dart diff --git a/lib/features/services/blocs/service_files_state.dart b/lib/features/operations/blocs/service_files_state.dart similarity index 100% rename from lib/features/services/blocs/service_files_state.dart rename to lib/features/operations/blocs/service_files_state.dart diff --git a/lib/features/services/blocs/services_cubit.dart b/lib/features/operations/blocs/services_cubit.dart similarity index 95% rename from lib/features/services/blocs/services_cubit.dart rename to lib/features/operations/blocs/services_cubit.dart index a9fb3a6..036a32f 100644 --- a/lib/features/services/blocs/services_cubit.dart +++ b/lib/features/operations/blocs/services_cubit.dart @@ -4,12 +4,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/services/data/services_repository.dart'; -import 'package:flux/features/services/models/energy_service_model.dart'; -import 'package:flux/features/services/models/entertainment_service_model.dart'; -import 'package:flux/features/services/models/fin_service_model.dart'; -import 'package:flux/features/services/models/service_file_model.dart'; -import 'package:flux/features/services/models/service_model.dart'; +import 'package:flux/features/operations/data/services_repository.dart'; +import 'package:flux/features/operations/models/energy_service_model.dart'; +import 'package:flux/features/operations/models/entertainment_service_model.dart'; +import 'package:flux/features/operations/models/fin_service_model.dart'; +import 'package:flux/features/operations/models/service_file_model.dart'; +import 'package:flux/features/operations/models/service_model.dart'; import 'package:get_it/get_it.dart'; import 'package:collection/collection.dart'; part 'services_state.dart'; diff --git a/lib/features/services/blocs/services_state.dart b/lib/features/operations/blocs/services_state.dart similarity index 100% rename from lib/features/services/blocs/services_state.dart rename to lib/features/operations/blocs/services_state.dart diff --git a/lib/features/services/data/services_repository.dart b/lib/features/operations/data/services_repository.dart similarity index 99% rename from lib/features/services/data/services_repository.dart rename to lib/features/operations/data/services_repository.dart index 9c91fc0..ead6a04 100644 --- a/lib/features/services/data/services_repository.dart +++ b/lib/features/operations/data/services_repository.dart @@ -4,7 +4,7 @@ import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/customers/models/customer_file_model.dart'; -import 'package:flux/features/services/models/service_file_model.dart'; +import 'package:flux/features/operations/models/service_file_model.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/service_model.dart'; diff --git a/lib/features/services/models/energy_service_model.dart b/lib/features/operations/models/energy_service_model.dart similarity index 100% rename from lib/features/services/models/energy_service_model.dart rename to lib/features/operations/models/energy_service_model.dart diff --git a/lib/features/services/models/entertainment_service_model.dart b/lib/features/operations/models/entertainment_service_model.dart similarity index 100% rename from lib/features/services/models/entertainment_service_model.dart rename to lib/features/operations/models/entertainment_service_model.dart diff --git a/lib/features/services/models/fin_service_model.dart b/lib/features/operations/models/fin_service_model.dart similarity index 100% rename from lib/features/services/models/fin_service_model.dart rename to lib/features/operations/models/fin_service_model.dart diff --git a/lib/features/services/models/service_file_model.dart b/lib/features/operations/models/service_file_model.dart similarity index 100% rename from lib/features/services/models/service_file_model.dart rename to lib/features/operations/models/service_file_model.dart diff --git a/lib/features/services/models/service_model.dart b/lib/features/operations/models/service_model.dart similarity index 94% rename from lib/features/services/models/service_model.dart rename to lib/features/operations/models/service_model.dart index a4d15f6..86c0bf4 100644 --- a/lib/features/services/models/service_model.dart +++ b/lib/features/operations/models/service_model.dart @@ -1,9 +1,9 @@ import 'package:equatable/equatable.dart'; import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/services/models/energy_service_model.dart'; -import 'package:flux/features/services/models/entertainment_service_model.dart'; -import 'package:flux/features/services/models/fin_service_model.dart'; -import 'package:flux/features/services/models/service_file_model.dart'; // <-- Aggiunto Import +import 'package:flux/features/operations/models/energy_service_model.dart'; +import 'package:flux/features/operations/models/entertainment_service_model.dart'; +import 'package:flux/features/operations/models/fin_service_model.dart'; +import 'package:flux/features/operations/models/service_file_model.dart'; // <-- Aggiunto Import class ServiceModel extends Equatable { final String? id; diff --git a/lib/features/services/ui/service_action_card.dart b/lib/features/operations/ui/service_action_card.dart similarity index 100% rename from lib/features/services/ui/service_action_card.dart rename to lib/features/operations/ui/service_action_card.dart diff --git a/lib/features/services/ui/service_form_screen/action_card.dart b/lib/features/operations/ui/service_form_screen/action_card.dart similarity index 100% rename from lib/features/services/ui/service_form_screen/action_card.dart rename to lib/features/operations/ui/service_form_screen/action_card.dart diff --git a/lib/features/services/ui/service_form_screen/attachment_section.dart b/lib/features/operations/ui/service_form_screen/attachment_section.dart similarity index 98% rename from lib/features/services/ui/service_form_screen/attachment_section.dart rename to lib/features/operations/ui/service_form_screen/attachment_section.dart index 6883f65..2211186 100644 --- a/lib/features/services/ui/service_form_screen/attachment_section.dart +++ b/lib/features/operations/ui/service_form_screen/attachment_section.dart @@ -5,9 +5,9 @@ import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/widgets/image_viewer_widget.dart'; import 'package:flux/core/widgets/pdf_viewer_widget.dart'; import 'package:flux/core/widgets/qr_upload_dialog.dart'; -import 'package:flux/features/services/blocs/service_files_bloc.dart'; -import 'package:flux/features/services/blocs/services_cubit.dart'; -import 'package:flux/features/services/models/service_file_model.dart'; +import 'package:flux/features/operations/blocs/service_files_bloc.dart'; +import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/models/service_file_model.dart'; class AttachmentsSection extends StatelessWidget { const AttachmentsSection({super.key}); diff --git a/lib/features/services/ui/service_form_screen/customer_section.dart b/lib/features/operations/ui/service_form_screen/customer_section.dart similarity index 97% rename from lib/features/services/ui/service_form_screen/customer_section.dart rename to lib/features/operations/ui/service_form_screen/customer_section.dart index dd1e752..3a5c42c 100644 --- a/lib/features/services/ui/service_form_screen/customer_section.dart +++ b/lib/features/operations/ui/service_form_screen/customer_section.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flux/features/customers/ui/customer_search_sheet.dart'; -import 'package:flux/features/services/models/service_model.dart'; +import 'package:flux/features/operations/models/service_model.dart'; class CustomerSection extends StatelessWidget { final ServiceModel service; diff --git a/lib/features/services/ui/service_form_screen/energy_service_dialog.dart b/lib/features/operations/ui/service_form_screen/energy_service_dialog.dart similarity index 99% rename from lib/features/services/ui/service_form_screen/energy_service_dialog.dart rename to lib/features/operations/ui/service_form_screen/energy_service_dialog.dart index 58da992..67bd0b4 100644 --- a/lib/features/services/ui/service_form_screen/energy_service_dialog.dart +++ b/lib/features/operations/ui/service_form_screen/energy_service_dialog.dart @@ -2,7 +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/services/models/energy_service_model.dart'; // Assicurati degli import +import 'package:flux/features/operations/models/energy_service_model.dart'; // Assicurati degli import class EnergyServiceDialog extends StatefulWidget { final List initialServices; diff --git a/lib/features/services/ui/service_form_screen/entertainment_service_card.dart b/lib/features/operations/ui/service_form_screen/entertainment_service_card.dart similarity index 98% rename from lib/features/services/ui/service_form_screen/entertainment_service_card.dart rename to lib/features/operations/ui/service_form_screen/entertainment_service_card.dart index 4e07001..e1899f9 100644 --- a/lib/features/services/ui/service_form_screen/entertainment_service_card.dart +++ b/lib/features/operations/ui/service_form_screen/entertainment_service_card.dart @@ -3,8 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.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/services/data/services_repository.dart'; -import 'package:flux/features/services/models/entertainment_service_model.dart'; +import 'package:flux/features/operations/data/services_repository.dart'; +import 'package:flux/features/operations/models/entertainment_service_model.dart'; import 'package:get_it/get_it.dart'; class EntertainmentServiceDialog extends StatefulWidget { diff --git a/lib/features/services/ui/service_form_screen/finance_service_dialog.dart b/lib/features/operations/ui/service_form_screen/finance_service_dialog.dart similarity index 99% rename from lib/features/services/ui/service_form_screen/finance_service_dialog.dart rename to lib/features/operations/ui/service_form_screen/finance_service_dialog.dart index b03f4d7..0b0248d 100644 --- a/lib/features/services/ui/service_form_screen/finance_service_dialog.dart +++ b/lib/features/operations/ui/service_form_screen/finance_service_dialog.dart @@ -5,7 +5,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; import 'package:flux/features/master_data/products/models/model_model.dart'; import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; -import 'package:flux/features/services/models/fin_service_model.dart'; +import 'package:flux/features/operations/models/fin_service_model.dart'; import 'package:flux/features/master_data/providers/models/provider_model.dart'; // =========================================================================== diff --git a/lib/features/services/ui/service_form_screen/general_info_section.dart b/lib/features/operations/ui/service_form_screen/general_info_section.dart similarity index 96% rename from lib/features/services/ui/service_form_screen/general_info_section.dart rename to lib/features/operations/ui/service_form_screen/general_info_section.dart index e2330bf..cf1c3ca 100644 --- a/lib/features/services/ui/service_form_screen/general_info_section.dart +++ b/lib/features/operations/ui/service_form_screen/general_info_section.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/services/blocs/services_cubit.dart'; -import 'package:flux/features/services/models/service_model.dart'; +import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/models/service_model.dart'; class GeneralInfoSection extends StatelessWidget { final ServiceModel service; diff --git a/lib/features/services/ui/service_form_screen/int_dialogs.dart b/lib/features/operations/ui/service_form_screen/int_dialogs.dart similarity index 100% rename from lib/features/services/ui/service_form_screen/int_dialogs.dart rename to lib/features/operations/ui/service_form_screen/int_dialogs.dart diff --git a/lib/features/services/ui/service_form_screen/service_form_screen.dart b/lib/features/operations/ui/service_form_screen/service_form_screen.dart similarity index 92% rename from lib/features/services/ui/service_form_screen/service_form_screen.dart rename to lib/features/operations/ui/service_form_screen/service_form_screen.dart index 6574c5b..4022f10 100644 --- a/lib/features/services/ui/service_form_screen/service_form_screen.dart +++ b/lib/features/operations/ui/service_form_screen/service_form_screen.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/services/blocs/services_cubit.dart'; -import 'package:flux/features/services/models/service_model.dart'; -import 'package:flux/features/services/ui/service_form_screen/attachment_section.dart'; -import 'package:flux/features/services/ui/service_form_screen/customer_section.dart'; -import 'package:flux/features/services/ui/service_form_screen/general_info_section.dart'; -import 'package:flux/features/services/ui/service_form_screen/services_grid.dart'; +import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/models/service_model.dart'; +import 'package:flux/features/operations/ui/service_form_screen/attachment_section.dart'; +import 'package:flux/features/operations/ui/service_form_screen/customer_section.dart'; +import 'package:flux/features/operations/ui/service_form_screen/general_info_section.dart'; +import 'package:flux/features/operations/ui/service_form_screen/services_grid.dart'; class ServiceFormScreen extends StatefulWidget { final String? serviceId; diff --git a/lib/features/services/ui/service_form_screen/service_mobile_upload_screen.dart b/lib/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart similarity index 99% rename from lib/features/services/ui/service_form_screen/service_mobile_upload_screen.dart rename to lib/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart index 08e306a..bd64cf5 100644 --- a/lib/features/services/ui/service_form_screen/service_mobile_upload_screen.dart +++ b/lib/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:flux/features/services/blocs/service_files_bloc.dart'; +import 'package:flux/features/operations/blocs/service_files_bloc.dart'; class ServiceMobileUploadScreen extends StatefulWidget { final String serviceId; diff --git a/lib/features/services/ui/service_form_screen/services_grid.dart b/lib/features/operations/ui/service_form_screen/services_grid.dart similarity index 89% rename from lib/features/services/ui/service_form_screen/services_grid.dart rename to lib/features/operations/ui/service_form_screen/services_grid.dart index 28e282a..f9d5874 100644 --- a/lib/features/services/ui/service_form_screen/services_grid.dart +++ b/lib/features/operations/ui/service_form_screen/services_grid.dart @@ -1,16 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; -import 'package:flux/features/services/blocs/services_cubit.dart'; -import 'package:flux/features/services/models/energy_service_model.dart'; -import 'package:flux/features/services/models/entertainment_service_model.dart'; -import 'package:flux/features/services/models/fin_service_model.dart'; -import 'package:flux/features/services/models/service_model.dart'; -import 'package:flux/features/services/ui/service_form_screen/action_card.dart'; -import 'package:flux/features/services/ui/service_form_screen/energy_service_dialog.dart'; -import 'package:flux/features/services/ui/service_form_screen/entertainment_service_card.dart'; -import 'package:flux/features/services/ui/service_form_screen/finance_service_dialog.dart'; -import 'package:flux/features/services/ui/service_form_screen/int_dialogs.dart'; // Assicurati di importare il modello +import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/models/energy_service_model.dart'; +import 'package:flux/features/operations/models/entertainment_service_model.dart'; +import 'package:flux/features/operations/models/fin_service_model.dart'; +import 'package:flux/features/operations/models/service_model.dart'; +import 'package:flux/features/operations/ui/service_form_screen/action_card.dart'; +import 'package:flux/features/operations/ui/service_form_screen/energy_service_dialog.dart'; +import 'package:flux/features/operations/ui/service_form_screen/entertainment_service_card.dart'; +import 'package:flux/features/operations/ui/service_form_screen/finance_service_dialog.dart'; +import 'package:flux/features/operations/ui/service_form_screen/int_dialogs.dart'; // Assicurati di importare il modello class ServicesGrid extends StatelessWidget { final ServiceModel service; diff --git a/lib/features/services/ui/services_screen.dart b/lib/features/operations/ui/services_screen.dart similarity index 96% rename from lib/features/services/ui/services_screen.dart rename to lib/features/operations/ui/services_screen.dart index dfb53f9..2c1ec32 100644 --- a/lib/features/services/ui/services_screen.dart +++ b/lib/features/operations/ui/services_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/services/blocs/services_cubit.dart'; -import 'package:flux/features/services/models/service_model.dart'; -import 'package:flux/features/services/utils/service_actions.dart'; +import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/models/service_model.dart'; +import 'package:flux/features/operations/utils/service_actions.dart'; import 'package:go_router/go_router.dart'; // Importa i tuoi modelli e cubit diff --git a/lib/features/services/utils/service_actions.dart b/lib/features/operations/utils/service_actions.dart similarity index 95% rename from lib/features/services/utils/service_actions.dart rename to lib/features/operations/utils/service_actions.dart index 3159591..ca8565f 100644 --- a/lib/features/services/utils/service_actions.dart +++ b/lib/features/operations/utils/service_actions.dart @@ -2,8 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; -import 'package:flux/features/services/blocs/services_cubit.dart'; -import 'package:flux/features/services/models/service_model.dart'; +import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/models/service_model.dart'; import 'package:go_router/go_router.dart'; /// Avvia la creazione di un nuovo servizio partendo dalla selezione dell'operatore. diff --git a/lib/main.dart b/lib/main.dart index 4130cd2..532039d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,8 +26,8 @@ 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'; import 'package:flux/features/master_data/store/data/store_repository.dart'; -import 'package:flux/features/services/blocs/services_cubit.dart'; -import 'package:flux/features/services/data/services_repository.dart'; +import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/data/services_repository.dart'; import 'package:flux/features/settings/settings.dart'; void main() async { -- 2.43.0 From 9c8576ada523f211a35e48f304281e13c2dec33b Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Fri, 1 May 2026 09:51:42 +0200 Subject: [PATCH 06/18] autoreplace service operation --- .../gradle/wrapper/gradle-wrapper.properties | 2 +- lib/core/routes/app_router.dart | 8 ++-- lib/core/widgets/flux_text_field.dart | 2 +- .../customers/ui/customer_search_sheet.dart | 2 +- .../bloc/latest_store_services_bloc.dart | 6 +-- .../bloc/latest_store_services_state.dart | 10 ++-- .../ui/latest_store_services_card.dart | 16 +++---- lib/features/home/ui/home_screen.dart | 2 +- .../onboarding/ui/store_onboarding_form.dart | 2 +- ...vices_cubit.dart => operations_cubit.dart} | 0 .../blocs/service_files_events.dart | 6 +-- .../operations/blocs/services_state.dart | 2 +- .../operations/data/services_repository.dart | 42 ++++++++--------- .../attachment_section.dart | 4 +- .../service_form_screen/customer_section.dart | 10 ++-- .../energy_service_dialog.dart | 12 ++--- .../entertainment_service_card.dart | 12 ++--- .../finance_service_dialog.dart | 12 ++--- .../general_info_section.dart | 14 +++--- .../service_form_screen.dart | 14 +++--- .../ui/service_form_screen/services_grid.dart | 46 +++++++++---------- .../operations/ui/services_screen.dart | 30 ++++++------ .../operations/utils/service_actions.dart | 4 +- lib/l10n/app_en.arb | 6 +-- lib/main.dart | 2 +- macos/Runner/Base.lproj/MainMenu.xib | 4 +- 26 files changed, 136 insertions(+), 134 deletions(-) rename lib/features/operations/blocs/{services_cubit.dart => operations_cubit.dart} (100%) diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index e4ef43f..87b6bad 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip +distributionUrl=https\://operations.gradle.org/distributions/gradle-8.14-all.zip diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index be1f6ed..aaed4d9 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -131,7 +131,7 @@ class AppRouter { ), ), GoRoute( - path: '/services', + path: '/operations', builder: (context, state) => const ServicesScreen(), ), GoRoute( @@ -168,8 +168,8 @@ class AppRouter { }, ), GoRoute( - path: '/service-form', - name: 'service-form', + path: '/operation-form', + name: 'operation-form', builder: (context, state) { final existingService = state.extra as ServiceModel?; final serviceId = state.uri.queryParameters['serviceId']; @@ -184,7 +184,7 @@ class AppRouter { }, ), GoRoute( - path: '/service/:id/upload', + path: '/operation/:id/upload', builder: (context, state) { final serviceId = state.pathParameters['id']!; final serviceName = state.uri.queryParameters['name'] ?? 'Pratica'; diff --git a/lib/core/widgets/flux_text_field.dart b/lib/core/widgets/flux_text_field.dart index f2cbf24..39fcf8c 100644 --- a/lib/core/widgets/flux_text_field.dart +++ b/lib/core/widgets/flux_text_field.dart @@ -1,6 +1,6 @@ // lib/ui/common/flux_text_field.dart import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter/operations.dart'; import 'package:flux/core/theme/theme.dart'; class FluxTextField extends StatefulWidget { diff --git a/lib/features/customers/ui/customer_search_sheet.dart b/lib/features/customers/ui/customer_search_sheet.dart index c12609c..0030995 100644 --- a/lib/features/customers/ui/customer_search_sheet.dart +++ b/lib/features/customers/ui/customer_search_sheet.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/customers/blocs/customer_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/quick_customer_dialog.dart'; -import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/blocs/operations_cubit.dart'; class CustomerSearchSheet extends StatefulWidget { const CustomerSearchSheet({super.key}); diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart index a85c6c9..b80cfd7 100644 --- a/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart +++ b/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart @@ -29,10 +29,10 @@ class LatestStoreServicesBloc // Questo gira ad ogni "scatto" dello stream di Supabase List fullyHydratedServices = []; - for (ServiceModel service in rawServices) { + for (ServiceModel operation in rawServices) { // Peschiamo i dati completi (incluso il cliente) ServiceModel fullService = await _repository.fetchServiceById( - service.id!, + operation.id!, ); fullyHydratedServices.add(fullService); } @@ -47,7 +47,7 @@ class LatestStoreServicesBloc onData: (List fullyHydratedServices) { // Qui ora è tutto sincrono e bellissimo return state.copyWith( - services: fullyHydratedServices, + operations: fullyHydratedServices, status: LatestStoreServicesStatus.success, ); }, diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart index ede49cb..8c9907d 100644 --- a/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart +++ b/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart @@ -5,26 +5,26 @@ enum LatestStoreServicesStatus { initial, loading, success, failure } class LatestStoreServicesState extends Equatable { final LatestStoreServicesStatus status; final String? error; - final List services; + final List operations; const LatestStoreServicesState({ required this.status, this.error, - this.services = const [], + this.operations = const [], }); @override - List get props => [status, error, services]; + List get props => [status, error, operations]; LatestStoreServicesState copyWith({ LatestStoreServicesStatus? status, String? error, - List? services, + List? operations, }) { return LatestStoreServicesState( status: status ?? this.status, error: error, - services: services ?? this.services, + operations: operations ?? this.operations, ); } } diff --git a/lib/features/home/latest_store_services/ui/latest_store_services_card.dart b/lib/features/home/latest_store_services/ui/latest_store_services_card.dart index 368b7dc..bf8556c 100644 --- a/lib/features/home/latest_store_services/ui/latest_store_services_card.dart +++ b/lib/features/home/latest_store_services/ui/latest_store_services_card.dart @@ -70,7 +70,7 @@ class _LatestServicesCardContent extends StatelessWidget { const SizedBox(width: 12), Expanded( child: TextButton( - onPressed: () => context.push('/services'), + onPressed: () => context.push('/operations'), child: Text( context.l10n.homeLatestServices, style: TextStyle( @@ -105,7 +105,7 @@ class _LatestServicesCardContent extends StatelessWidget { ); } - if (state.services.isEmpty) { + if (state.operations.isEmpty) { return Center( child: Text( "Nessun servizio recente.", @@ -118,16 +118,16 @@ class _LatestServicesCardContent extends StatelessWidget { } return ListView.separated( - itemCount: state.services.length, + itemCount: state.operations.length, separatorBuilder: (context, index) => Divider( height: 1, color: theme.dividerColor.withValues(alpha: 0.3), ), itemBuilder: (context, index) { - final service = state.services[index]; + final operation = state.operations[index]; return InkWell( onTap: () => - context.push('/service-form', extra: service), + context.push('/operation-form', extra: operation), child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Row( @@ -136,7 +136,7 @@ class _LatestServicesCardContent extends StatelessWidget { Expanded( flex: 5, child: Text( - service.customerDisplayName ?? + operation.customerDisplayName ?? 'Cliente sconosciuto', style: TextStyle( fontWeight: FontWeight.w700, @@ -147,7 +147,7 @@ class _LatestServicesCardContent extends StatelessWidget { Expanded( flex: 5, child: Text( - service.number, + operation.number, style: TextStyle( fontWeight: FontWeight.w600, color: context.primaryText, @@ -157,7 +157,7 @@ class _LatestServicesCardContent extends StatelessWidget { ), ), Text( - "${service.createdAt?.day}/${service.createdAt?.month}", + "${operation.createdAt?.day}/${operation.createdAt?.month}", style: TextStyle( color: context.secondaryText, fontSize: 12, diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 91ab08c..69786ee 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -185,7 +185,7 @@ class HomeScreen extends StatelessWidget { color: Colors.blue, onTap: () { // Entriamo nel form! Nessun parametro extra = Nuovo Servizio - context.push('/service-form'); + context.push('/operation-form'); }, ), const SizedBox(width: 12), diff --git a/lib/features/onboarding/ui/store_onboarding_form.dart b/lib/features/onboarding/ui/store_onboarding_form.dart index 0f26732..26f5a6e 100644 --- a/lib/features/onboarding/ui/store_onboarding_form.dart +++ b/lib/features/onboarding/ui/store_onboarding_form.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; // <-- IMPORTANTE per i formatter +import 'package:flutter/operations.dart'; // <-- IMPORTANTE per i formatter import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; diff --git a/lib/features/operations/blocs/services_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart similarity index 100% rename from lib/features/operations/blocs/services_cubit.dart rename to lib/features/operations/blocs/operations_cubit.dart diff --git a/lib/features/operations/blocs/service_files_events.dart b/lib/features/operations/blocs/service_files_events.dart index 141d5ac..cb02eb2 100644 --- a/lib/features/operations/blocs/service_files_events.dart +++ b/lib/features/operations/blocs/service_files_events.dart @@ -17,11 +17,11 @@ class ServiceSavedEvent extends ServiceFilesEvent { class LoadServiceFilesEvent extends ServiceFilesEvent { final String? serviceId; - final ServiceModel? service; - const LoadServiceFilesEvent({this.serviceId, this.service}); + final ServiceModel? operation; + const LoadServiceFilesEvent({this.serviceId, this.operation}); @override - List get props => [serviceId, service]; + List get props => [serviceId, operation]; } class AddServiceFilesEvent extends ServiceFilesEvent { diff --git a/lib/features/operations/blocs/services_state.dart b/lib/features/operations/blocs/services_state.dart index 9d5a15a..5af7130 100644 --- a/lib/features/operations/blocs/services_state.dart +++ b/lib/features/operations/blocs/services_state.dart @@ -1,4 +1,4 @@ -part of 'services_cubit.dart'; +part of 'operations_cubit.dart'; enum ServicesStatus { initial, diff --git a/lib/features/operations/data/services_repository.dart b/lib/features/operations/data/services_repository.dart index ead6a04..f0f8233 100644 --- a/lib/features/operations/data/services_repository.dart +++ b/lib/features/operations/data/services_repository.dart @@ -18,7 +18,7 @@ class ServicesRepository { Future fetchServiceById(String id) async { try { final response = await _supabase - .from('service') + .from('operation') .select(''' *, customer(nome), @@ -47,7 +47,7 @@ class ServicesRepository { try { // Nota: 'customer(name, surname)' serve per il display name nella card var query = _supabase - .from('service') + .from('operation') .select(''' *, customer(nome), @@ -89,7 +89,7 @@ class ServicesRepository { required int limit, }) { return _supabase - .from('service') + .from('operation') .stream(primaryKey: ['id']) .eq('store_id', storeId) .order('created_at', ascending: false) @@ -101,12 +101,12 @@ class ServicesRepository { } // --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) --- - Future saveFullService(ServiceModel service) async { + Future saveFullService(ServiceModel operation) async { try { // 1. Upsert del record principale final serviceData = await _supabase - .from('service') - .upsert(service.toMap()) + .from('operation') + .upsert(operation.toMap()) .select() .single(); @@ -114,7 +114,7 @@ class ServicesRepository { // 2. MODIFICA: Pulizia atomica dei figli // Se stiamo modificando (id != null), resettiamo le tabelle collegate - if (service.id != null) { + if (operation.id != null) { await Future.wait([ _supabase.from('energy_service').delete().eq('service_id', newId), _supabase.from('fin_service').delete().eq('service_id', newId), @@ -129,36 +129,36 @@ class ServicesRepository { // 3. Inserimento dei moduli in parallelo per velocità final List insertTasks = []; - if (service.energyServices.isNotEmpty) { + if (operation.energyServices.isNotEmpty) { insertTasks.add( _supabase .from('energy_service') .insert( - service.energyServices + operation.energyServices .map((item) => item.copyWith(serviceId: newId).toMap()) .toList(), ), ); } - if (service.finServices.isNotEmpty) { + if (operation.finServices.isNotEmpty) { insertTasks.add( _supabase .from('fin_service') .insert( - service.finServices + operation.finServices .map((item) => item.copyWith(serviceId: newId).toMap()) .toList(), ), ); } - if (service.entertainmentServices.isNotEmpty) { + if (operation.entertainmentServices.isNotEmpty) { insertTasks.add( _supabase .from('entertainment_service') .insert( - service.entertainmentServices + operation.entertainmentServices .map((item) => item.copyWith(serviceId: newId).toMap()) .toList(), ), @@ -171,7 +171,7 @@ class ServicesRepository { // 4. UPLOAD DEI FILE LOCALI (Nuovi) // Filtriamo solo i file che non hanno ancora un ID (quindi sono locali) - final localFilesToUpload = service.files + final localFilesToUpload = operation.files .where((f) => f.id == null) .toList(); @@ -180,7 +180,7 @@ class ServicesRepository { for (var file in localFilesToUpload) { final storagePath = - '$companyId/services/$newId/${DateTime.now().millisecondsSinceEpoch}_${file.name}.${file.extension}'; + '$companyId/operations/$newId/${DateTime.now().millisecondsSinceEpoch}_${file.name}.${file.extension}'; final String mimeType = file.extension.toLowerCase() == 'pdf' ? 'application/pdf' : 'image/${file.extension}'; @@ -216,7 +216,7 @@ class ServicesRepository { // Interroghiamo Supabase per farci restituire la pratica con TUTTI gli ID generati // (inclusi quelli della tabella service_file appena inseriti) final updatedServiceData = await _supabase - .from('service') + .from('operation') .select(''' *, energy_service(*), @@ -237,7 +237,7 @@ class ServicesRepository { // --- ELIMINAZIONE --- Future deleteService(String id) async { try { - await _supabase.from('service').delete().eq('id', id); + await _supabase.from('operation').delete().eq('id', id); } catch (e) { throw Exception('Errore durante l\'eliminazione: $e'); } @@ -247,11 +247,11 @@ class ServicesRepository { Future> fetchTopEntertainmentTypes(String companyId) async { try { // Cerchiamo i tipi più frequenti associati ai servizi di questa company - // Nota: dobbiamo passare attraverso la tabella 'service' per filtrare per company_id + // Nota: dobbiamo passare attraverso la tabella 'operation' per filtrare per company_id final response = await _supabase .from('entertainment_service') - .select('type, service!inner(store!inner(company_id))') - .eq('service.store.company_id', companyId) + .select('type, operation!inner(store!inner(company_id))') + .eq('operation.store.company_id', companyId) .limit(100); // Prendiamo un campione // Logica rapida per contare le occorrenze e prendere i primi 5 @@ -297,7 +297,7 @@ class ServicesRepository { '_', ); final storagePath = - '$companyId/services/$serviceId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; + '$companyId/operations/$serviceId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; final int fileSize = pickedFile.size; final fileToSave = ServiceFileModel( serviceId: serviceId, diff --git a/lib/features/operations/ui/service_form_screen/attachment_section.dart b/lib/features/operations/ui/service_form_screen/attachment_section.dart index 2211186..1a5e960 100644 --- a/lib/features/operations/ui/service_form_screen/attachment_section.dart +++ b/lib/features/operations/ui/service_form_screen/attachment_section.dart @@ -6,7 +6,7 @@ import 'package:flux/core/widgets/image_viewer_widget.dart'; import 'package:flux/core/widgets/pdf_viewer_widget.dart'; import 'package:flux/core/widgets/qr_upload_dialog.dart'; import 'package:flux/features/operations/blocs/service_files_bloc.dart'; -import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/service_file_model.dart'; class AttachmentsSection extends StatelessWidget { @@ -310,7 +310,7 @@ class AttachmentsSection extends StatelessWidget { }, child: QrUploadDialog( deepLinkUrl: - 'fluxapp:///service/${currentService!.id}/upload?name=${Uri.encodeComponent(nomePratica)}', + 'fluxapp:///operation/${currentService!.id}/upload?name=${Uri.encodeComponent(nomePratica)}', title: 'Scatta per\n$nomePratica', ), ), diff --git a/lib/features/operations/ui/service_form_screen/customer_section.dart b/lib/features/operations/ui/service_form_screen/customer_section.dart index 3a5c42c..bb0a7e8 100644 --- a/lib/features/operations/ui/service_form_screen/customer_section.dart +++ b/lib/features/operations/ui/service_form_screen/customer_section.dart @@ -3,9 +3,9 @@ import 'package:flux/features/customers/ui/customer_search_sheet.dart'; import 'package:flux/features/operations/models/service_model.dart'; class CustomerSection extends StatelessWidget { - final ServiceModel service; + final ServiceModel operation; - const CustomerSection({super.key, required this.service}); + const CustomerSection({super.key, required this.operation}); void _openCustomerSearch(BuildContext context) { showModalBottomSheet( @@ -28,8 +28,8 @@ class CustomerSection extends StatelessWidget { @override Widget build(BuildContext context) { - // Niente BlocBuilder qui! Leggiamo solo la variabile 'service' - final hasCustomer = service.customerId != null; + // Niente BlocBuilder qui! Leggiamo solo la variabile 'operation' + final hasCustomer = operation.customerId != null; return Card( elevation: 2, @@ -74,7 +74,7 @@ class CustomerSection extends StatelessWidget { children: [ Expanded( child: Text( - service.customerDisplayName ?? "Cliente Selezionato", + operation.customerDisplayName ?? "Cliente Selezionato", style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, diff --git a/lib/features/operations/ui/service_form_screen/energy_service_dialog.dart b/lib/features/operations/ui/service_form_screen/energy_service_dialog.dart index 67bd0b4..04c0ef4 100644 --- a/lib/features/operations/ui/service_form_screen/energy_service_dialog.dart +++ b/lib/features/operations/ui/service_form_screen/energy_service_dialog.dart @@ -63,7 +63,7 @@ class _EnergyServiceDialogState extends State { }, ) : _EnergyList( - services: _tempList, + operations: _tempList, onDelete: (index) { setState(() => _tempList.removeAt(index)); }, @@ -101,14 +101,14 @@ class _EnergyServiceDialogState extends State { // VISTA 1: LA LISTA DEI CONTRATTI // ========================================== class _EnergyList extends StatelessWidget { - final List services; + final List operations; final List activeProviders; // <--- NUOVO: La lista vera dal Cubit final Function(int) onDelete; final VoidCallback onAddTap; const _EnergyList({ - required this.services, + required this.operations, required this.activeProviders, // <--- Richiesto required this.onDelete, required this.onAddTap, @@ -120,7 +120,7 @@ class _EnergyList extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (services.isEmpty) + if (operations.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 32.0), child: Text( @@ -133,10 +133,10 @@ class _EnergyList extends StatelessWidget { Flexible( child: ListView.separated( shrinkWrap: true, - itemCount: services.length, + itemCount: operations.length, separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { - final s = services[index]; + final s = operations[index]; final isLuce = s.type == EnergyType.luce; // LA MAGIA: Troviamo il nome partendo dall'ID salvato nel servizio diff --git a/lib/features/operations/ui/service_form_screen/entertainment_service_card.dart b/lib/features/operations/ui/service_form_screen/entertainment_service_card.dart index e1899f9..d7d08a5 100644 --- a/lib/features/operations/ui/service_form_screen/entertainment_service_card.dart +++ b/lib/features/operations/ui/service_form_screen/entertainment_service_card.dart @@ -67,7 +67,7 @@ class _EntertainmentServiceDialogState builder: (context, state) { // Passiamo allProviders per garantire la visione dello storico return _EntertainmentList( - services: _tempList, + operations: _tempList, allProviders: state.allProviders, onDelete: (index) => setState(() => _tempList.removeAt(index)), @@ -94,13 +94,13 @@ class _EntertainmentServiceDialogState } class _EntertainmentList extends StatelessWidget { - final List services; + final List operations; final List allProviders; final Function(int) onDelete; final VoidCallback onAddTap; const _EntertainmentList({ - required this.services, + required this.operations, required this.allProviders, required this.onDelete, required this.onAddTap, @@ -112,7 +112,7 @@ class _EntertainmentList extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (services.isEmpty) + if (operations.isEmpty) const Padding( padding: EdgeInsets.symmetric(vertical: 32.0), child: Text( @@ -125,10 +125,10 @@ class _EntertainmentList extends StatelessWidget { Flexible( child: ListView.separated( shrinkWrap: true, - itemCount: services.length, + itemCount: operations.length, separatorBuilder: (_, _) => const Divider(height: 1), itemBuilder: (context, index) { - final s = services[index]; + final s = operations[index]; final providerName = allProviders .firstWhere( diff --git a/lib/features/operations/ui/service_form_screen/finance_service_dialog.dart b/lib/features/operations/ui/service_form_screen/finance_service_dialog.dart index 0b0248d..a8196c9 100644 --- a/lib/features/operations/ui/service_form_screen/finance_service_dialog.dart +++ b/lib/features/operations/ui/service_form_screen/finance_service_dialog.dart @@ -74,7 +74,7 @@ class _FinanceServiceDialogState extends State { return BlocBuilder( builder: (context, prodState) { return _FinanceList( - services: _tempList, + operations: _tempList, allProviders: provState.allProviders, // Per vedere lo storico allModels: prodState.models, @@ -109,14 +109,14 @@ class _FinanceServiceDialogState extends State { // VISTA LISTA (STORICA) // =========================================================================== class _FinanceList extends StatelessWidget { - final List services; + final List operations; final List allProviders; final List allModels; final Function(int) onDelete; final VoidCallback onAddTap; const _FinanceList({ - required this.services, + required this.operations, required this.allProviders, required this.allModels, required this.onDelete, @@ -125,7 +125,7 @@ class _FinanceList extends StatelessWidget { @override Widget build(BuildContext context) { - if (services.isEmpty) { + if (operations.isEmpty) { return Column( mainAxisSize: MainAxisSize.min, children: [ @@ -151,10 +151,10 @@ class _FinanceList extends StatelessWidget { Flexible( child: ListView.separated( shrinkWrap: true, - itemCount: services.length, + itemCount: operations.length, separatorBuilder: (_, _) => const Divider(), itemBuilder: (context, index) { - final s = services[index]; + final s = operations[index]; // Cerchiamo il nome del provider in TUTTI quelli caricati (storico) final providerName = allProviders diff --git a/lib/features/operations/ui/service_form_screen/general_info_section.dart b/lib/features/operations/ui/service_form_screen/general_info_section.dart index cf1c3ca..f679b9f 100644 --- a/lib/features/operations/ui/service_form_screen/general_info_section.dart +++ b/lib/features/operations/ui/service_form_screen/general_info_section.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/service_model.dart'; class GeneralInfoSection extends StatelessWidget { - final ServiceModel service; - const GeneralInfoSection({super.key, required this.service}); + final ServiceModel operation; + const GeneralInfoSection({super.key, required this.operation}); @override Widget build(BuildContext context) { @@ -34,7 +34,7 @@ class GeneralInfoSection extends StatelessWidget { // Numero di Riferimento / Telefono TextFormField( - initialValue: service.number, + initialValue: operation.number, keyboardType: TextInputType .phone, // Fa aprire il tastierino numerico su mobile decoration: const InputDecoration( @@ -59,7 +59,7 @@ class GeneralInfoSection extends StatelessWidget { "Pratica in lavorazione", style: TextStyle(fontSize: 12), ), - value: service.isBozza, + value: operation.isBozza, activeThumbColor: Colors.orange, contentPadding: EdgeInsets.zero, onChanged: (val) { @@ -75,7 +75,7 @@ class GeneralInfoSection extends StatelessWidget { "Esito positivo", style: TextStyle(fontSize: 12), ), - value: service.resultOk, + value: operation.resultOk, activeThumbColor: Colors.green, contentPadding: EdgeInsets.zero, onChanged: (val) { @@ -89,7 +89,7 @@ class GeneralInfoSection extends StatelessWidget { // Campo Note TextFormField( - initialValue: service.note, + initialValue: operation.note, maxLines: 4, minLines: 2, decoration: const InputDecoration( diff --git a/lib/features/operations/ui/service_form_screen/service_form_screen.dart b/lib/features/operations/ui/service_form_screen/service_form_screen.dart index 4022f10..400dc41 100644 --- a/lib/features/operations/ui/service_form_screen/service_form_screen.dart +++ b/lib/features/operations/ui/service_form_screen/service_form_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/service_model.dart'; import 'package:flux/features/operations/ui/service_form_screen/attachment_section.dart'; import 'package:flux/features/operations/ui/service_form_screen/customer_section.dart'; @@ -70,7 +70,7 @@ class _ServiceFormScreenState extends State { } }, builder: (context, state) { - final service = state.currentService; + final operation = state.currentService; final isSaving = state.status == ServicesStatus.saving; final isEditMode = widget.serviceId != null; @@ -89,7 +89,7 @@ class _ServiceFormScreenState extends State { ), ), ) - else if (service != null) ...[ + else if (operation != null) ...[ IconButton( icon: const Icon(Icons.edit_note), tooltip: "Salva come Bozza", @@ -107,20 +107,20 @@ class _ServiceFormScreenState extends State { ], ], ), - body: (service == null) + body: (operation == null) ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - CustomerSection(service: service), + CustomerSection(operation: operation), const SizedBox(height: 24), - GeneralInfoSection(service: service), + GeneralInfoSection(operation: operation), const SizedBox(height: 24), - ServicesGrid(service: service), + ServicesGrid(operation: operation), const SizedBox(height: 32), AttachmentsSection(), diff --git a/lib/features/operations/ui/service_form_screen/services_grid.dart b/lib/features/operations/ui/service_form_screen/services_grid.dart index f9d5874..bc0ab95 100644 --- a/lib/features/operations/ui/service_form_screen/services_grid.dart +++ b/lib/features/operations/ui/service_form_screen/services_grid.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; -import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/energy_service_model.dart'; import 'package:flux/features/operations/models/entertainment_service_model.dart'; import 'package:flux/features/operations/models/fin_service_model.dart'; @@ -13,9 +13,9 @@ import 'package:flux/features/operations/ui/service_form_screen/finance_service_ import 'package:flux/features/operations/ui/service_form_screen/int_dialogs.dart'; // Assicurati di importare il modello class ServicesGrid extends StatelessWidget { - final ServiceModel service; + final ServiceModel operation; - const ServicesGrid({super.key, required this.service}); + const ServicesGrid({super.key, required this.operation}); @override Widget build(BuildContext context) { @@ -52,65 +52,65 @@ class ServicesGrid extends StatelessWidget { // --- CONTATORI SEMPLICI --- ActionCard( label: "AL", - count: service.al, + count: operation.al, icon: Icons.sim_card, color: Colors.blue, onTap: () => updateCountDialog( context, "AL", - service.al, + operation.al, (val) => context.read().updateField(al: val), ), ), ActionCard( label: "MNP", - count: service.mnp, + count: operation.mnp, icon: Icons.phone_android, color: Colors.indigo, onTap: () => updateCountDialog( context, "MNP", - service.mnp, + operation.mnp, (val) => context.read().updateField(mnp: val), ), ), ActionCard( label: "NIP", - count: service.nip, + count: operation.nip, icon: Icons.compare_arrows, color: Colors.cyan, onTap: () => updateCountDialog( context, "NIP", - service.nip, + operation.nip, (val) => context.read().updateField(nip: val), ), ), ActionCard( label: "Unica", - count: service.unica, + count: operation.unica, icon: Icons.all_inclusive, color: Colors.purple, onTap: () => updateCountDialog( context, "Unica", - service.unica, + operation.unica, (val) => context.read().updateField(unica: val), ), ), ActionCard( label: "Telepass", - count: service.telepass, + count: operation.telepass, icon: Icons.directions_car, color: Colors.amber.shade700, onTap: () => updateCountDialog( context, "Telepass", - service.telepass, + operation.telepass, (val) => context.read().updateField( telepass: val, ), @@ -120,7 +120,7 @@ class ServicesGrid extends StatelessWidget { // --- MODULI COMPLESSI (Le liste) --- ActionCard( label: "Energia", - count: service.energyServices.length, + count: operation.energyServices.length, icon: Icons.bolt, color: Colors.green, onTap: () async { @@ -128,8 +128,8 @@ class ServicesGrid extends StatelessWidget { final result = await showDialog>( context: context, builder: (context) => EnergyServiceDialog( - currentStoreId: service.storeId, - initialServices: service + currentStoreId: operation.storeId, + initialServices: operation .energyServices, // Passiamo la lista attuale ), ); @@ -144,7 +144,7 @@ class ServicesGrid extends StatelessWidget { ), ActionCard( label: "Finanziam.", - count: service.finServices.length, + count: operation.finServices.length, icon: Icons.euro_symbol, color: Colors.teal, onTap: () async { @@ -152,9 +152,9 @@ class ServicesGrid extends StatelessWidget { context: context, builder: (context) => FinanceServiceDialog( productCubit: context.read(), - currentStoreId: service.storeId, - initialServices: - service.finServices, // Passiamo la lista attuale + currentStoreId: operation.storeId, + initialServices: operation + .finServices, // Passiamo la lista attuale ), ); @@ -165,7 +165,7 @@ class ServicesGrid extends StatelessWidget { ), ActionCard( label: "Intratten.", - count: service.entertainmentServices.length, + count: operation.entertainmentServices.length, icon: Icons.movie_filter_outlined, color: Colors.purple, onTap: () async { @@ -173,8 +173,8 @@ class ServicesGrid extends StatelessWidget { await showDialog>( context: context, builder: (context) => EntertainmentServiceDialog( - initialServices: service.entertainmentServices, - currentStoreId: service.storeId, + initialServices: operation.entertainmentServices, + currentStoreId: operation.storeId, ), ); diff --git a/lib/features/operations/ui/services_screen.dart b/lib/features/operations/ui/services_screen.dart index 2c1ec32..4935529 100644 --- a/lib/features/operations/ui/services_screen.dart +++ b/lib/features/operations/ui/services_screen.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/service_model.dart'; import 'package:flux/features/operations/utils/service_actions.dart'; import 'package:go_router/go_router.dart'; @@ -107,8 +107,8 @@ class _ServicesScreenState extends State { ); } - final service = state.allServices[index]; - return _buildServiceCard(context, service); + final operation = state.allServices[index]; + return _buildServiceCard(context, operation); }, ), ); @@ -121,7 +121,7 @@ class _ServicesScreenState extends State { ); } - Widget _buildServiceCard(BuildContext context, ServiceModel service) { + Widget _buildServiceCard(BuildContext context, ServiceModel operation) { return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), elevation: 2, @@ -132,14 +132,14 @@ class _ServicesScreenState extends State { children: [ Expanded( child: Text( - service.customerDisplayName ?? "Cliente sconosciuto", + operation.customerDisplayName ?? "Cliente sconosciuto", style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), ), - if (service.isBozza) + if (operation.isBozza) const Chip( label: Text( "BOZZA", @@ -155,20 +155,20 @@ class _ServicesScreenState extends State { children: [ const SizedBox(height: 4), Text( - "Pratica: ${service.number} • ${service.createdAt?.day}/${service.createdAt?.month}/${service.createdAt?.year}", + "Pratica: ${operation.number} • ${operation.createdAt?.day}/${operation.createdAt?.month}/${operation.createdAt?.year}", ), const SizedBox(height: 8), // I nostri mini-chip per i servizi attivati Wrap( spacing: 6, children: [ - if (service.al > 0 || service.mnp > 0) + if (operation.al > 0 || operation.mnp > 0) _miniBadge("📞 Tel", Colors.blue), - if (service.energyServices.isNotEmpty) + if (operation.energyServices.isNotEmpty) _miniBadge("⚡ Energy", Colors.green), - if (service.finServices.isNotEmpty) + if (operation.finServices.isNotEmpty) _miniBadge("💰 Fin", Colors.purple), - if (service.entertainmentServices.isNotEmpty) + if (operation.entertainmentServices.isNotEmpty) _miniBadge("📺 Ent", Colors.red), ], ), @@ -176,10 +176,12 @@ class _ServicesScreenState extends State { ), trailing: const Icon(Icons.chevron_right), onTap: () => context.pushNamed( - 'service-form', - extra: service, // <-- LA MAGIA È QUI: Passa l'oggetto intero! + 'operation-form', + extra: operation, // <-- LA MAGIA È QUI: Passa l'oggetto intero! // Teniamo anche il parametro URL per coerenza di routing - queryParameters: service.id != null ? {'serviceId': service.id!} : {}, + queryParameters: operation.id != null + ? {'serviceId': operation.id!} + : {}, ), ), ); diff --git a/lib/features/operations/utils/service_actions.dart b/lib/features/operations/utils/service_actions.dart index ca8565f..c0bdf38 100644 --- a/lib/features/operations/utils/service_actions.dart +++ b/lib/features/operations/utils/service_actions.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; -import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/service_model.dart'; import 'package:go_router/go_router.dart'; @@ -67,7 +67,7 @@ void startNewService(BuildContext context) { Navigator.pop(modalContext); // 3. Naviga verso il form - context.pushNamed('service-form'); + context.pushNamed('operation-form'); }, ), ), diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 84c90f5..a0fe5df 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,13 +1,13 @@ { "@@locale": "en", "welcomeBack": "Welcome back, {name}! 👋", - "latestServices": "Latest Services", + "latestServices": "Latest Operations", "masterData": "Master Data", "settings": "Settings", - "newService": "Service", + "newService": "Operation", "expiring_contracts": "Expiring Contracts", "sticky_notes": "Sticky Notes", "my_tasks": "My Tasks", - "latest_service_tickets": "Latest service tickets" + "latest_service_tickets": "Latest operation tickets" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 532039d..a6515df 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -26,7 +26,7 @@ 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'; import 'package:flux/features/master_data/store/data/store_repository.dart'; -import 'package:flux/features/operations/blocs/services_cubit.dart'; +import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/data/services_repository.dart'; import 'package:flux/features/settings/settings.dart'; diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib index 80e867a..02538c9 100644 --- a/macos/Runner/Base.lproj/MainMenu.xib +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -35,9 +35,9 @@ - + - + -- 2.43.0 From f8bcac51e169edaac182b471543464e5021245aa Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Fri, 1 May 2026 10:11:44 +0200 Subject: [PATCH 07/18] asd --- lib/core/routes/app_router.dart | 42 +-- lib/core/widgets/flux_text_field.dart | 2 +- .../company/models/company_model.dart | 2 +- .../customers/ui/customer_search_sheet.dart | 6 +- .../bloc/latest_store_operations_bloc.dart | 66 +++++ .../bloc/latest_store_operations_events.dart | 17 ++ .../bloc/latest_store_operations_state.dart | 30 +++ .../ui/latest_store_operations_card.dart | 189 ++++++++++++++ .../bloc/latest_store_services_bloc.dart | 69 ----- .../bloc/latest_store_services_events.dart | 17 -- .../bloc/latest_store_services_state.dart | 30 --- .../ui/latest_store_services_card.dart | 180 ------------- lib/features/home/ui/home_screen.dart | 10 +- .../onboarding/ui/store_onboarding_form.dart | 3 +- .../blocs/operation_files_bloc.dart | 242 ++++++++++++++++++ .../blocs/operation_files_events.dart | 56 ++++ .../blocs/operation_files_state.dart | 52 ++++ .../operations/blocs/operations_cubit.dart | 175 +++++++------ ...vices_state.dart => operations_state.dart} | 34 +-- .../operations/blocs/service_files_bloc.dart | 232 ----------------- .../blocs/service_files_events.dart | 56 ---- .../operations/blocs/service_files_state.dart | 52 ---- ...sitory.dart => operations_repository.dart} | 125 ++++----- ...model.dart => energy_operation_model.dart} | 26 +- ...art => entertainment_operation_model.dart} | 26 +- ...ce_model.dart => fin_operation_model.dart} | 26 +- ...e_model.dart => operation_file_model.dart} | 26 +- ...ervice_model.dart => operation_model.dart} | 78 +++--- ...n_card.dart => operation_action_card.dart} | 4 +- .../action_card.dart | 0 .../attachment_section.dart | 62 ++--- .../customer_section.dart | 4 +- .../energy_operation_dialog.dart} | 30 +-- .../entertainment_operation_card.dart} | 36 +-- .../finance_operation_dialog.dart} | 24 +- .../general_info_section.dart | 14 +- .../int_dialogs.dart | 0 .../operation_form_screen.dart} | 50 ++-- .../operation_mobile_upload_screen.dart} | 33 +-- .../operations_grid.dart} | 85 +++--- ...ces_screen.dart => operations_screen.dart} | 54 ++-- ...ce_actions.dart => operation_actions.dart} | 8 +- lib/l10n/app_en.arb | 6 +- lib/l10n/app_it.arb | 8 +- lib/l10n/app_localizations.dart | 16 +- lib/l10n/app_localizations_en.dart | 8 +- lib/l10n/app_localizations_it.dart | 8 +- lib/main.dart | 9 +- 48 files changed, 1187 insertions(+), 1141 deletions(-) create mode 100644 lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart create mode 100644 lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart create mode 100644 lib/features/home/latest_store_operations/bloc/latest_store_operations_state.dart create mode 100644 lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart delete mode 100644 lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart delete mode 100644 lib/features/home/latest_store_services/bloc/latest_store_services_events.dart delete mode 100644 lib/features/home/latest_store_services/bloc/latest_store_services_state.dart delete mode 100644 lib/features/home/latest_store_services/ui/latest_store_services_card.dart create mode 100644 lib/features/operations/blocs/operation_files_bloc.dart create mode 100644 lib/features/operations/blocs/operation_files_events.dart create mode 100644 lib/features/operations/blocs/operation_files_state.dart rename lib/features/operations/blocs/{services_state.dart => operations_state.dart} (60%) delete mode 100644 lib/features/operations/blocs/service_files_bloc.dart delete mode 100644 lib/features/operations/blocs/service_files_events.dart delete mode 100644 lib/features/operations/blocs/service_files_state.dart rename lib/features/operations/data/{services_repository.dart => operations_repository.dart} (75%) rename lib/features/operations/models/{energy_service_model.dart => energy_operation_model.dart} (74%) rename lib/features/operations/models/{entertainment_service_model.dart => entertainment_operation_model.dart} (75%) rename lib/features/operations/models/{fin_service_model.dart => fin_operation_model.dart} (69%) rename lib/features/operations/models/{service_file_model.dart => operation_file_model.dart} (81%) rename lib/features/operations/models/{service_model.dart => operation_model.dart} (67%) rename lib/features/operations/ui/{service_action_card.dart => operation_action_card.dart} (96%) rename lib/features/operations/ui/{service_form_screen => operation_form_screen}/action_card.dart (100%) rename lib/features/operations/ui/{service_form_screen => operation_form_screen}/attachment_section.dart (87%) rename lib/features/operations/ui/{service_form_screen => operation_form_screen}/customer_section.dart (96%) rename lib/features/operations/ui/{service_form_screen/energy_service_dialog.dart => operation_form_screen/energy_operation_dialog.dart} (94%) rename lib/features/operations/ui/{service_form_screen/entertainment_service_card.dart => operation_form_screen/entertainment_operation_card.dart} (92%) rename lib/features/operations/ui/{service_form_screen/finance_service_dialog.dart => operation_form_screen/finance_operation_dialog.dart} (96%) rename lib/features/operations/ui/{service_form_screen => operation_form_screen}/general_info_section.dart (88%) rename lib/features/operations/ui/{service_form_screen => operation_form_screen}/int_dialogs.dart (100%) rename lib/features/operations/ui/{service_form_screen/service_form_screen.dart => operation_form_screen/operation_form_screen.dart} (76%) rename lib/features/operations/ui/{service_form_screen/service_mobile_upload_screen.dart => operation_form_screen/operation_mobile_upload_screen.dart} (92%) rename lib/features/operations/ui/{service_form_screen/services_grid.dart => operation_form_screen/operations_grid.dart} (63%) rename lib/features/operations/ui/{services_screen.dart => operations_screen.dart} (77%) rename lib/features/operations/utils/{service_actions.dart => operation_actions.dart} (92%) diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index aaed4d9..087125a 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -12,7 +12,7 @@ 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_mobile_upload_screen.dart'; import 'package:flux/features/customers/ui/customers_content.dart'; -import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.dart'; +import 'package:flux/features/home/latest_store_operations/bloc/latest_store_operations_bloc.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/products/ui/products_screen.dart'; @@ -21,11 +21,11 @@ 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/onboarding/blocs/onboarding_cubit.dart'; import 'package:flux/features/onboarding/ui/onboarding_screen.dart'; -import 'package:flux/features/operations/blocs/service_files_bloc.dart'; -import 'package:flux/features/operations/models/service_model.dart'; -import 'package:flux/features/operations/ui/service_form_screen/service_form_screen.dart'; -import 'package:flux/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart'; -import 'package:flux/features/operations/ui/services_screen.dart'; +import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/operation_form_screen.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart'; +import 'package:flux/features/operations/ui/operations_screen.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; @@ -132,7 +132,7 @@ class AppRouter { ), GoRoute( path: '/operations', - builder: (context, state) => const ServicesScreen(), + builder: (context, state) => const OperationsScreen(), ), GoRoute( path: '/customers', @@ -171,14 +171,15 @@ class AppRouter { path: '/operation-form', name: 'operation-form', builder: (context, state) { - final existingService = state.extra as ServiceModel?; - final serviceId = state.uri.queryParameters['serviceId']; + final existingOperation = state.extra as OperationModel?; + final operationId = state.uri.queryParameters['operationId']; return BlocProvider( - create: (context) => - ServiceFilesBloc(serviceId: serviceId ?? existingService?.id), - child: ServiceFormScreen( - serviceId: serviceId ?? existingService?.id, - existingService: existingService, + create: (context) => OperationFilesBloc( + operationId: operationId ?? existingOperation?.id, + ), + child: OperationFormScreen( + operationId: operationId ?? existingOperation?.id, + existingOperation: existingOperation, ), ); }, @@ -186,13 +187,14 @@ class AppRouter { GoRoute( path: '/operation/:id/upload', builder: (context, state) { - final serviceId = state.pathParameters['id']!; - final serviceName = state.uri.queryParameters['name'] ?? 'Pratica'; + final operationId = state.pathParameters['id']!; + final operationName = + state.uri.queryParameters['name'] ?? 'Pratica'; return BlocProvider( - create: (context) => ServiceFilesBloc(serviceId: serviceId), - child: ServiceMobileUploadScreen( - serviceId: serviceId, - serviceName: serviceName, + create: (context) => OperationFilesBloc(operationId: operationId), + child: OperationMobileUploadScreen( + operationId: operationId, + operationName: operationName, ), ); }, diff --git a/lib/core/widgets/flux_text_field.dart b/lib/core/widgets/flux_text_field.dart index 39fcf8c..f2cbf24 100644 --- a/lib/core/widgets/flux_text_field.dart +++ b/lib/core/widgets/flux_text_field.dart @@ -1,6 +1,6 @@ // lib/ui/common/flux_text_field.dart import 'package:flutter/material.dart'; -import 'package:flutter/operations.dart'; +import 'package:flutter/services.dart'; import 'package:flux/core/theme/theme.dart'; class FluxTextField extends StatefulWidget { diff --git a/lib/features/company/models/company_model.dart b/lib/features/company/models/company_model.dart index 4c5c4aa..97e542a 100644 --- a/lib/features/company/models/company_model.dart +++ b/lib/features/company/models/company_model.dart @@ -263,7 +263,7 @@ extension CompanyLimits on CompanyModel { } } - int get maxServicesPerMonth { + int get maxOperationsPerMonth { switch (subscriptionTier) { case SubscriptionTier.free: return 50; diff --git a/lib/features/customers/ui/customer_search_sheet.dart b/lib/features/customers/ui/customer_search_sheet.dart index 0030995..e93d980 100644 --- a/lib/features/customers/ui/customer_search_sheet.dart +++ b/lib/features/customers/ui/customer_search_sheet.dart @@ -91,7 +91,7 @@ class _CustomerSearchSheetState extends State { child: IconButton( icon: const Icon(Icons.person_add), onPressed: () async { - final servicesCubit = context.read(); + final operationsCubit = context.read(); // Apriamo la dialog passando la query attuale final CustomerModel? nuovoCliente = await showDialog( context: context, @@ -101,7 +101,7 @@ class _CustomerSearchSheetState extends State { ); if (nuovoCliente != null) { - servicesCubit.updateField( + operationsCubit.updateField( customerId: nuovoCliente.id, customerDisplayName: nuovoCliente.nome, ); @@ -180,7 +180,7 @@ class _CustomerSearchSheetState extends State { ), onTap: () { // Salviamo l'ID e il nome formattato nel form dei servizi - context.read().updateField( + context.read().updateField( customerId: customer.id, customerDisplayName: displayName, ); diff --git a/lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart b/lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart new file mode 100644 index 0000000..5d52d3d --- /dev/null +++ b/lib/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart @@ -0,0 +1,66 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flux/features/operations/data/operations_repository.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:get_it/get_it.dart'; + +part '../../latest_store_operations/bloc/latest_store_operations_events.dart'; +part '../../latest_store_operations/bloc/latest_store_operations_state.dart'; + +class LatestStoreOperationsBloc + extends Bloc { + final _repository = GetIt.I.get(); + + LatestStoreOperationsBloc() + : super( + const LatestStoreOperationsState( + status: LatestStoreOperationsStatus.initial, + ), + ) { + on((event, emit) async { + emit(state.copyWith(status: LatestStoreOperationsStatus.loading)); + try { + // 1. Creiamo uno stream "intermedio" che idrata i dati + final hydratedStream = _repository + .getLastStoreOperationsStream(storeId: event.storeId, limit: 5) + .asyncMap((List rawOperations) async { + // Questo gira ad ogni "scatto" dello stream di Supabase + List fullyHydratedOperations = []; + + for (OperationModel operation in rawOperations) { + // Peschiamo i dati completi (incluso il cliente) + OperationModel fullOperation = await _repository + .fetchOperationById(operation.id!); + fullyHydratedOperations.add(fullOperation); + } + + // Passiamo la lista completa allo step successivo + return fullyHydratedOperations; + }); + + // 2. Ora passiamo lo stream idratato all'emit.forEach + await emit.forEach( + hydratedStream, // Usiamo lo stream modificato! + onData: (List fullyHydratedOperations) { + // Qui ora è tutto sincrono e bellissimo + return state.copyWith( + operations: fullyHydratedOperations, + status: LatestStoreOperationsStatus.success, + ); + }, + onError: (error, stackTrace) => state.copyWith( + status: LatestStoreOperationsStatus.failure, + error: error.toString(), + ), + ); + } catch (e) { + emit( + state.copyWith( + status: LatestStoreOperationsStatus.failure, + error: e.toString(), + ), + ); + } + }); + } +} diff --git a/lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart b/lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart new file mode 100644 index 0000000..c15c0f8 --- /dev/null +++ b/lib/features/home/latest_store_operations/bloc/latest_store_operations_events.dart @@ -0,0 +1,17 @@ +part of 'latest_store_operations_bloc.dart'; + +sealed class LatestStoreOperationsEvent extends Equatable { + const LatestStoreOperationsEvent(); + + @override + List get props => []; +} + +class InitLastStoreOperationsEvent extends LatestStoreOperationsEvent { + final String storeId; + + const InitLastStoreOperationsEvent(this.storeId); + + @override + List get props => [storeId]; +} diff --git a/lib/features/home/latest_store_operations/bloc/latest_store_operations_state.dart b/lib/features/home/latest_store_operations/bloc/latest_store_operations_state.dart new file mode 100644 index 0000000..d373848 --- /dev/null +++ b/lib/features/home/latest_store_operations/bloc/latest_store_operations_state.dart @@ -0,0 +1,30 @@ +part of 'latest_store_operations_bloc.dart'; + +enum LatestStoreOperationsStatus { initial, loading, success, failure } + +class LatestStoreOperationsState extends Equatable { + final LatestStoreOperationsStatus status; + final String? error; + final List operations; + + const LatestStoreOperationsState({ + required this.status, + this.error, + this.operations = const [], + }); + + @override + List get props => [status, error, operations]; + + LatestStoreOperationsState copyWith({ + LatestStoreOperationsStatus? status, + String? error, + List? operations, + }) { + return LatestStoreOperationsState( + status: status ?? this.status, + error: error, + operations: operations ?? this.operations, + ); + } +} diff --git a/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart b/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart new file mode 100644 index 0000000..10123e0 --- /dev/null +++ b/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart @@ -0,0 +1,189 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/theme/theme.dart'; +import 'package:flux/core/utils/extensions.dart'; +import 'package:flux/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart'; +import 'package:go_router/go_router.dart'; + +class LatestStoreOperationsCard extends StatelessWidget { + const LatestStoreOperationsCard({super.key}); + + @override + Widget build(BuildContext context) { + final currentStoreId = context.read().state.currentStore?.id; + + return BlocProvider( + // 1. Creiamo il Bloc e facciamo partire subito la query + create: (context) => + LatestStoreOperationsBloc() + ..add(InitLastStoreOperationsEvent(currentStoreId ?? '')), + child: BlocListener( + // 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream! + listenWhen: (previous, current) => + previous.currentStore?.id != current.currentStore?.id, + listener: (context, state) { + if (state.currentStore?.id != null) { + context.read().add( + InitLastStoreOperationsEvent(state.currentStore!.id!), + ); + } + }, + child: _LatestOperationsCardContent(), + ), + ); + } +} + +class _LatestOperationsCardContent extends StatelessWidget { + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + const color = Colors.blue; + + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // --- HEADER DELLA CARD --- + Row( + children: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.design_services_outlined, + color: color, + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextButton( + onPressed: () => context.push('/operations'), + child: Text( + context.l10n.homeLatestOperations, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16, + color: context.primaryText, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + + // --- CORPO DELLA CARD (LA LISTA REAL-TIME) --- + Expanded( + child: + BlocBuilder< + LatestStoreOperationsBloc, + LatestStoreOperationsState + >( + builder: (context, state) { + if (state.status == LatestStoreOperationsStatus.loading || + state.status == LatestStoreOperationsStatus.initial) { + return const Center(child: CircularProgressIndicator()); + } + + if (state.status == LatestStoreOperationsStatus.failure) { + return Center( + child: Text( + "Errore di caricamento", + style: TextStyle(color: theme.colorScheme.error), + ), + ); + } + + if (state.operations.isEmpty) { + return Center( + child: Text( + "Nessun servizio recente.", + style: TextStyle( + color: context.secondaryText, + fontStyle: FontStyle.italic, + ), + ), + ); + } + + return ListView.separated( + itemCount: state.operations.length, + separatorBuilder: (context, index) => Divider( + height: 1, + color: theme.dividerColor.withValues(alpha: 0.3), + ), + itemBuilder: (context, index) { + final operation = state.operations[index]; + return InkWell( + onTap: () => context.push( + '/operation-form', + extra: operation, + ), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8.0, + ), + child: Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Expanded( + flex: 5, + child: Text( + operation.customerDisplayName ?? + 'Cliente sconosciuto', + style: TextStyle( + fontWeight: FontWeight.w700, + color: context.primaryText, + ), + ), + ), + Expanded( + flex: 5, + child: Text( + operation.number, + style: TextStyle( + fontWeight: FontWeight.w600, + color: context.primaryText, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + Text( + "${operation.createdAt?.day}/${operation.createdAt?.month}", + style: TextStyle( + color: context.secondaryText, + fontSize: 12, + ), + ), + ], + ), + ), + ); + }, + ); + }, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart deleted file mode 100644 index b80cfd7..0000000 --- a/lib/features/home/latest_store_services/bloc/latest_store_services_bloc.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'dart:developer'; - -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flux/features/operations/data/services_repository.dart'; -import 'package:flux/features/operations/models/service_model.dart'; -import 'package:get_it/get_it.dart'; - -part 'latest_store_services_events.dart'; -part 'latest_store_services_state.dart'; - -class LatestStoreServicesBloc - extends Bloc { - final _repository = GetIt.I.get(); - - LatestStoreServicesBloc() - : super( - const LatestStoreServicesState( - status: LatestStoreServicesStatus.initial, - ), - ) { - on((event, emit) async { - emit(state.copyWith(status: LatestStoreServicesStatus.loading)); - try { - // 1. Creiamo uno stream "intermedio" che idrata i dati - final hydratedStream = _repository - .getLastStoreServicesStream(storeId: event.storeId, limit: 5) - .asyncMap((List rawServices) async { - // Questo gira ad ogni "scatto" dello stream di Supabase - List fullyHydratedServices = []; - - for (ServiceModel operation in rawServices) { - // Peschiamo i dati completi (incluso il cliente) - ServiceModel fullService = await _repository.fetchServiceById( - operation.id!, - ); - fullyHydratedServices.add(fullService); - } - - // Passiamo la lista completa allo step successivo - return fullyHydratedServices; - }); - - // 2. Ora passiamo lo stream idratato all'emit.forEach - await emit.forEach( - hydratedStream, // Usiamo lo stream modificato! - onData: (List fullyHydratedServices) { - // Qui ora è tutto sincrono e bellissimo - return state.copyWith( - operations: fullyHydratedServices, - status: LatestStoreServicesStatus.success, - ); - }, - onError: (error, stackTrace) => state.copyWith( - status: LatestStoreServicesStatus.failure, - error: error.toString(), - ), - ); - } catch (e) { - emit( - state.copyWith( - status: LatestStoreServicesStatus.failure, - error: e.toString(), - ), - ); - } - }); - } -} diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart deleted file mode 100644 index b66128d..0000000 --- a/lib/features/home/latest_store_services/bloc/latest_store_services_events.dart +++ /dev/null @@ -1,17 +0,0 @@ -part of 'latest_store_services_bloc.dart'; - -sealed class LatestStoreServicesEvent extends Equatable { - const LatestStoreServicesEvent(); - - @override - List get props => []; -} - -class InitLastStoreServicesEvent extends LatestStoreServicesEvent { - final String storeId; - - const InitLastStoreServicesEvent(this.storeId); - - @override - List get props => [storeId]; -} diff --git a/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart b/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart deleted file mode 100644 index 8c9907d..0000000 --- a/lib/features/home/latest_store_services/bloc/latest_store_services_state.dart +++ /dev/null @@ -1,30 +0,0 @@ -part of 'latest_store_services_bloc.dart'; - -enum LatestStoreServicesStatus { initial, loading, success, failure } - -class LatestStoreServicesState extends Equatable { - final LatestStoreServicesStatus status; - final String? error; - final List operations; - - const LatestStoreServicesState({ - required this.status, - this.error, - this.operations = const [], - }); - - @override - List get props => [status, error, operations]; - - LatestStoreServicesState copyWith({ - LatestStoreServicesStatus? status, - String? error, - List? operations, - }) { - return LatestStoreServicesState( - status: status ?? this.status, - error: error, - operations: operations ?? this.operations, - ); - } -} diff --git a/lib/features/home/latest_store_services/ui/latest_store_services_card.dart b/lib/features/home/latest_store_services/ui/latest_store_services_card.dart deleted file mode 100644 index bf8556c..0000000 --- a/lib/features/home/latest_store_services/ui/latest_store_services_card.dart +++ /dev/null @@ -1,180 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/theme/theme.dart'; -import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.dart'; -import 'package:go_router/go_router.dart'; - -class LatestStoreServicesCard extends StatelessWidget { - const LatestStoreServicesCard({super.key}); - - @override - Widget build(BuildContext context) { - final currentStoreId = context.read().state.currentStore?.id; - - return BlocProvider( - // 1. Creiamo il Bloc e facciamo partire subito la query - create: (context) => - LatestStoreServicesBloc() - ..add(InitLastStoreServicesEvent(currentStoreId ?? '')), - child: BlocListener( - // 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream! - listenWhen: (previous, current) => - previous.currentStore?.id != current.currentStore?.id, - listener: (context, state) { - if (state.currentStore?.id != null) { - context.read().add( - InitLastStoreServicesEvent(state.currentStore!.id!), - ); - } - }, - child: _LatestServicesCardContent(), - ), - ); - } -} - -class _LatestServicesCardContent extends StatelessWidget { - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - const color = Colors.blue; - - return Card( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)), - ), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // --- HEADER DELLA CARD --- - Row( - children: [ - Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(8), - ), - child: const Icon( - Icons.design_services_outlined, - color: color, - size: 20, - ), - ), - const SizedBox(width: 12), - Expanded( - child: TextButton( - onPressed: () => context.push('/operations'), - child: Text( - context.l10n.homeLatestServices, - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - color: context.primaryText, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ), - const SizedBox(height: 12), - - // --- CORPO DELLA CARD (LA LISTA REAL-TIME) --- - Expanded( - child: BlocBuilder( - builder: (context, state) { - if (state.status == LatestStoreServicesStatus.loading || - state.status == LatestStoreServicesStatus.initial) { - return const Center(child: CircularProgressIndicator()); - } - - if (state.status == LatestStoreServicesStatus.failure) { - return Center( - child: Text( - "Errore di caricamento", - style: TextStyle(color: theme.colorScheme.error), - ), - ); - } - - if (state.operations.isEmpty) { - return Center( - child: Text( - "Nessun servizio recente.", - style: TextStyle( - color: context.secondaryText, - fontStyle: FontStyle.italic, - ), - ), - ); - } - - return ListView.separated( - itemCount: state.operations.length, - separatorBuilder: (context, index) => Divider( - height: 1, - color: theme.dividerColor.withValues(alpha: 0.3), - ), - itemBuilder: (context, index) { - final operation = state.operations[index]; - return InkWell( - onTap: () => - context.push('/operation-form', extra: operation), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - flex: 5, - child: Text( - operation.customerDisplayName ?? - 'Cliente sconosciuto', - style: TextStyle( - fontWeight: FontWeight.w700, - color: context.primaryText, - ), - ), - ), - Expanded( - flex: 5, - child: Text( - operation.number, - style: TextStyle( - fontWeight: FontWeight.w600, - color: context.primaryText, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - Text( - "${operation.createdAt?.day}/${operation.createdAt?.month}", - style: TextStyle( - color: context.secondaryText, - fontSize: 12, - ), - ), - ], - ), - ), - ); - }, - ); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 69786ee..1c498b2 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/home/latest_store_services/ui/latest_store_services_card.dart'; +import 'package:flux/features/home/latest_store_operations/ui/latest_store_operations_card.dart'; import 'package:flux/features/home/ui/quick_actions_widget.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:go_router/go_router.dart'; @@ -76,10 +76,10 @@ class HomeScreen extends StatelessWidget { color: Colors.green, context: context, ), - LatestStoreServicesCard(), + LatestStoreOperationsCard(), _buildDashboardWidget( - title: context.l10n.homeLatestServiceTickets, + title: context.l10n.homeLatestOperationTickets, icon: Icons.support_agent_outlined, color: Colors.purple, context: context, @@ -181,7 +181,7 @@ class HomeScreen extends StatelessWidget { children: [ QuickActionButton( icon: Icons.add, - label: context.l10n.commonService, + label: context.l10n.commonOperation, color: Colors.blue, onTap: () { // Entriamo nel form! Nessun parametro extra = Nuovo Servizio @@ -191,7 +191,7 @@ class HomeScreen extends StatelessWidget { const SizedBox(width: 12), QuickActionButton( icon: Icons.handyman, - label: context.l10n.homeNewServiceTicket, + label: context.l10n.homeNewOperationTicket, color: Colors.redAccent, onTap: () { // TODO: Quando avrai la rotta per la nuova assistenza diff --git a/lib/features/onboarding/ui/store_onboarding_form.dart b/lib/features/onboarding/ui/store_onboarding_form.dart index 26f5a6e..2408abf 100644 --- a/lib/features/onboarding/ui/store_onboarding_form.dart +++ b/lib/features/onboarding/ui/store_onboarding_form.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/operations.dart'; // <-- IMPORTANTE per i formatter +import 'package:flutter/services.dart'; + import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/features/master_data/store/models/store_model.dart'; diff --git a/lib/features/operations/blocs/operation_files_bloc.dart b/lib/features/operations/blocs/operation_files_bloc.dart new file mode 100644 index 0000000..6407d5a --- /dev/null +++ b/lib/features/operations/blocs/operation_files_bloc.dart @@ -0,0 +1,242 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flux/core/utils/extensions.dart'; +import 'package:flux/features/operations/data/operations_repository.dart'; +import 'package:flux/features/operations/models/operation_file_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:get_it/get_it.dart'; + +part 'operation_files_events.dart'; +part 'operation_files_state.dart'; + +class OperationFilesBloc + extends Bloc { + final _repository = GetIt.I.get(); + final String? operationId; + + OperationFilesBloc({this.operationId}) + : super( + OperationFilesState( + status: OperationFilesStatus.initial, + operationId: operationId, + ), + ) { + on(_onOperationsaved); + on(_onLoadOperationFiles); + on(_onAddOperationFiles); + on(_onUploadOperationFiles); + on(_onUploadMultipleOperationFiles); + on(_onDeleteOperationFiles); + on(_onToggleOperationFileSelection); + // Se il BLoC nasce con un ID, accendiamo subito lo stream! + if (operationId != null) { + add(LoadOperationFilesEvent(operationId: operationId)); + } + } + + FutureOr _onOperationsaved( + OperationsavedEvent event, + Emitter emit, + ) { + // 1. Aggiorniamo l'ID nello stato + // 2. PIALLIAMO i file locali: ormai sono partiti per Supabase! + // Così la UI si pulisce all'istante e aspetta quelli remoti. + emit( + state.copyWith( + operationId: event.operationId, + localFiles: [], // <-- LA MAGIA ANTI-DUPLICATI + ), + ); + + // Lanciamo il caricamento + add(LoadOperationFilesEvent(operationId: event.operationId)); + } + + FutureOr _onLoadOperationFiles( + LoadOperationFilesEvent event, + Emitter emit, + ) async { + // Usiamo l'ID dell'evento, e se non c'è usiamo quello dello stato + final currentId = event.operationId ?? state.operationId; + + if (currentId != null) { + emit(state.copyWith(status: OperationFilesStatus.loading)); + + await emit.forEach( + _repository.getOperationFilesStream( + currentId, + ), // <-- Usiamo l'ID corretto! + onData: (data) => state.copyWith( + status: OperationFilesStatus.success, + remoteFiles: data, + ), + onError: (error, stackTrace) => state.copyWith( + status: OperationFilesStatus.failure, + error: error.toString(), + ), + ); + } + } + + void _onAddOperationFiles( + AddOperationFilesEvent event, + Emitter emit, + ) async { + final currentId = state.operationId; + // BIVIO 1: PRATICA NUOVA (Nessun ID) + if (currentId == null) { + // Mettiamo i file nel "parcheggio" locale dello State + final newLocalFiles = event.files.map((file) { + return OperationFileModel( + id: null, + operationId: operationId ?? '', + name: file.name.fileNameWithoutExtension(), + extension: file.name.fileExtension(), + storagePath: '', + fileSize: file.size, + localBytes: file.bytes, + ); + }).toList(); + final List updatedLocalFiles = [ + ...state.localFiles, + ...newLocalFiles, + ]; + emit( + state.copyWith( + localFiles: updatedLocalFiles, + status: OperationFilesStatus.success, + ), + ); + return; + } + + // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID) + emit(state.copyWith(status: OperationFilesStatus.uploading)); + try { + // Logica identica a quella che abbiamo fatto per i clienti + for (var file in event.files) { + await _repository.uploadAndRegisterOperationFile( + operationId: operationId!, + pickedFile: file, + ); + } + emit(state.copyWith(status: OperationFilesStatus.success)); + } catch (e) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: e.toString(), + ), + ); + } + } + + FutureOr _onUploadOperationFiles( + UploadOperationFilesEvent event, + Emitter emit, + ) async { + if (event.pickedFiles == null && event.photos == null) return; + if (event.pickedFiles!.isEmpty && event.photos!.isEmpty) return; + + // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID + emit(state.copyWith(status: OperationFilesStatus.uploading)); + try { + // Logica identica a quella che abbiamo fatto per i clienti + if (event.pickedFiles != null && event.pickedFiles!.isNotEmpty) { + for (var file in event.pickedFiles!) { + await _repository.uploadAndRegisterOperationFile( + operationId: state.operationId!, + pickedFile: file, + ); + } + } + emit(state.copyWith(status: OperationFilesStatus.success)); + } catch (e) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: e.toString(), + ), + ); + } + } + + FutureOr _onUploadMultipleOperationFiles( + UploadMultipleOperationFilesEvent event, + Emitter emit, + ) async { + if (event.files.isEmpty) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: "Nessun file selezionato", + ), + ); + return; + } + emit(state.copyWith(status: OperationFilesStatus.uploading, error: null)); + try { + // 2. Creiamo una lista di "Promesse" (Futures) per il repository + final List> uploadTasks = []; + for (var file in event.files) { + // Aggiungiamo il task alla lista, ma NON usiamo await qui dentro! + uploadTasks.add( + _repository.uploadAndRegisterOperationFile( + operationId: state.operationId!, + pickedFile: file, + ), + ); + } + // 3. ESECUZIONE PARALLELA! + // Aspettiamo che tutti i file siano caricati contemporaneamente. + await Future.wait(uploadTasks); + // 4. GRAN FINALE: Tutto caricato, emettiamo il success! + emit(state.copyWith(status: OperationFilesStatus.success)); + } catch (e) { + // Se anche un solo file fallisce, catturiamo l'errore + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: "Errore durante l'upload multiplo: $e", + ), + ); + } + } + + FutureOr _onDeleteOperationFiles( + DeleteOperationFilesEvent event, + Emitter emit, + ) async { + emit(state.copyWith(status: OperationFilesStatus.loading)); + try { + await _repository.deleteOperationFiles(state.selectedFiles); + emit( + state.copyWith(status: OperationFilesStatus.success, selectedFiles: []), + ); + } catch (e) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: e.toString(), + ), + ); + } + } + + FutureOr _onToggleOperationFileSelection( + ToggleOperationFileSelectionEvent event, + Emitter emit, + ) { + List selectedFiles = List.from(state.selectedFiles); + if (selectedFiles.contains(event.file)) { + selectedFiles.remove(event.file); + } else { + selectedFiles.add(event.file); + } + emit(state.copyWith(selectedFiles: selectedFiles)); + } +} diff --git a/lib/features/operations/blocs/operation_files_events.dart b/lib/features/operations/blocs/operation_files_events.dart new file mode 100644 index 0000000..4542902 --- /dev/null +++ b/lib/features/operations/blocs/operation_files_events.dart @@ -0,0 +1,56 @@ +part of 'operation_files_bloc.dart'; + +abstract class OperationFilesEvent extends Equatable { + const OperationFilesEvent(); + + @override + List get props => []; +} + +class OperationsavedEvent extends OperationFilesEvent { + final String operationId; + const OperationsavedEvent(this.operationId); + + @override + List get props => [operationId]; +} + +class LoadOperationFilesEvent extends OperationFilesEvent { + final String? operationId; + final OperationModel? operation; + const LoadOperationFilesEvent({this.operationId, this.operation}); + + @override + List get props => [operationId, operation]; +} + +class AddOperationFilesEvent extends OperationFilesEvent { + final List files; + const AddOperationFilesEvent(this.files); + + @override + List get props => [files]; +} + +class UploadOperationFilesEvent extends OperationFilesEvent { + final List? pickedFiles; + final List? photos; + const UploadOperationFilesEvent({this.pickedFiles, this.photos}); + + @override + List get props => [pickedFiles, photos]; +} + +class UploadMultipleOperationFilesEvent extends OperationFilesEvent { + final List files; + const UploadMultipleOperationFilesEvent(this.files); + @override + List get props => [files]; +} + +class DeleteOperationFilesEvent extends OperationFilesEvent {} + +class ToggleOperationFileSelectionEvent extends OperationFilesEvent { + final OperationFileModel file; + const ToggleOperationFileSelectionEvent(this.file); +} diff --git a/lib/features/operations/blocs/operation_files_state.dart b/lib/features/operations/blocs/operation_files_state.dart new file mode 100644 index 0000000..8ea5eb3 --- /dev/null +++ b/lib/features/operations/blocs/operation_files_state.dart @@ -0,0 +1,52 @@ +part of 'operation_files_bloc.dart'; + +enum OperationFilesStatus { initial, loading, uploading, success, failure } + +class OperationFilesState extends Equatable { + const OperationFilesState({ + this.operationId, + required this.status, + this.error, + this.localFiles = const [], + this.remoteFiles = const [], + this.selectedFiles = const [], + }); + + final String? operationId; + final OperationFilesStatus status; + final String? error; + final List localFiles; + final List remoteFiles; + + final List selectedFiles; + + @override + List get props => [ + operationId, + status, + error, + localFiles, + remoteFiles, + selectedFiles, + ]; + + List get allFiles => [...remoteFiles, ...localFiles]; + + OperationFilesState copyWith({ + String? operationId, + OperationFilesStatus? status, + String? error, + List? localFiles, + List? remoteFiles, + List? selectedFiles, + }) { + return OperationFilesState( + operationId: operationId ?? this.operationId, + status: status ?? this.status, + error: error, + localFiles: localFiles ?? this.localFiles, + remoteFiles: remoteFiles ?? this.remoteFiles, + selectedFiles: selectedFiles ?? this.selectedFiles, + ); + } +} diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index 036a32f..f66e1b0 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -4,50 +4,51 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/operations/data/services_repository.dart'; -import 'package:flux/features/operations/models/energy_service_model.dart'; -import 'package:flux/features/operations/models/entertainment_service_model.dart'; -import 'package:flux/features/operations/models/fin_service_model.dart'; -import 'package:flux/features/operations/models/service_file_model.dart'; -import 'package:flux/features/operations/models/service_model.dart'; +import 'package:flux/features/operations/data/operations_repository.dart'; +import 'package:flux/features/operations/models/energy_operation_model.dart'; +import 'package:flux/features/operations/models/entertainment_operation_model.dart'; +import 'package:flux/features/operations/models/fin_operation_model.dart'; +import 'package:flux/features/operations/models/operation_file_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; import 'package:get_it/get_it.dart'; import 'package:collection/collection.dart'; -part 'services_state.dart'; +part 'operations_state.dart'; -class ServicesCubit extends Cubit { - final ServicesRepository _repository = GetIt.I(); +class OperationsCubit extends Cubit { + final OperationsRepository _repository = GetIt.I(); final SessionCubit _sessionCubit = GetIt.I(); - ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial)); + OperationsCubit() + : super(const OperationsState(status: OperationsStatus.initial)); // --- CARICAMENTO E PAGINAZIONE --- - Future loadServices({bool refresh = false}) async { + Future loadOperations({bool refresh = false}) async { // Se stiamo già caricando, evitiamo chiamate doppie - if (state.status == ServicesStatus.loading) return; + if (state.status == OperationsStatus.loading) return; // Se non è un refresh e abbiamo già raggiunto la fine dei dati, ci fermiamo if (!refresh && state.hasReachedMax) return; emit( state.copyWith( - status: ServicesStatus.loading, + status: OperationsStatus.loading, errorMessage: null, // Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading - allServices: refresh ? [] : state.allServices, + allOperations: refresh ? [] : state.allOperations, hasReachedMax: refresh ? false : state.hasReachedMax, ), ); try { - final currentOffset = refresh ? 0 : state.allServices.length; + final currentOffset = refresh ? 0 : state.allOperations.length; final companyId = _sessionCubit.state.company?.id; if (companyId == null) { throw Exception("Company ID non trovato nella sessione"); } - final newServices = await _repository.fetchServices( + final newOperations = await _repository.fetchOperations( companyId: companyId, offset: currentOffset, limit: 50, @@ -56,21 +57,21 @@ class ServicesCubit extends Cubit { ); // Se ricevi meno record del limite, significa che non ce ne sono altri sul DB - final bool reachedMax = newServices.length < 50; + final bool reachedMax = newOperations.length < 50; emit( state.copyWith( - status: ServicesStatus.ready, - allServices: refresh - ? newServices - : [...state.allServices, ...newServices], + status: OperationsStatus.ready, + allOperations: refresh + ? newOperations + : [...state.allOperations, ...newOperations], hasReachedMax: reachedMax, ), ); } catch (e) { emit( state.copyWith( - status: ServicesStatus.failure, + status: OperationsStatus.failure, errorMessage: "Errore nel caricamento servizi: $e", ), ); @@ -87,51 +88,51 @@ class ServicesCubit extends Cubit { dateRange: range ?? state.dateRange, ), ); - loadServices(refresh: true); + loadOperations(refresh: true); } /// Pulisce tutti i filtri void clearFilters() { emit(state.copyWith(query: '', dateRange: null)); - loadServices(refresh: true); + loadOperations(refresh: true); } // --- GESTIONE BOZZA (DRAFT) --- /// Inizializza un nuovo servizio o ne carica uno esistente per la modifica - void initServiceForm({ - ServiceModel? existingService, - String? serviceId, + void initOperationForm({ + OperationModel? existingOperation, + String? operationId, }) async { - if (existingService != null) { + if (existingOperation != null) { emit( state.copyWith( - currentService: existingService, - status: ServicesStatus.ready, + currentOperation: existingOperation, + status: OperationsStatus.ready, ), ); - } else if (serviceId != null) { - ServiceModel? serviceModel = state.allServices.firstWhereOrNull( - (s) => s.id == serviceId, + } else if (operationId != null) { + OperationModel? operationModel = state.allOperations.firstWhereOrNull( + (s) => s.id == operationId, ); - serviceModel ??= await _repository.fetchServiceById(serviceId); + operationModel ??= await _repository.fetchOperationById(operationId); emit( state.copyWith( - currentService: serviceModel, - status: ServicesStatus.ready, + currentOperation: operationModel, + status: OperationsStatus.ready, ), ); } else { // Crea un template vuoto con lo store di default (se disponibile) emit( state.copyWith( - currentService: ServiceModel( + currentOperation: OperationModel( storeId: _sessionCubit.state.currentStore?.id ?? '', number: '', // Sarà compilato dall'utente createdAt: DateTime.now(), companyId: _sessionCubit.state.company!.id!, ), - status: ServicesStatus.ready, + status: OperationsStatus.ready, ), ); } @@ -151,9 +152,9 @@ class ServicesCubit extends Cubit { String? customerId, String? customerDisplayName, }) { - if (state.currentService == null) return; + if (state.currentOperation == null) return; - final updated = state.currentService!.copyWith( + final updated = state.currentOperation!.copyWith( al: al, mnp: mnp, nip: nip, @@ -167,34 +168,38 @@ class ServicesCubit extends Cubit { customerDisplayName: customerDisplayName, ); - emit(state.copyWith(currentService: updated)); + emit(state.copyWith(currentOperation: updated)); } // --- GESTIONE MODULI COMPLESSI --- - void updateEnergyServices(List energyList) { + void updateEnergyOperations(List energyList) { emit( state.copyWith( - currentService: state.currentService?.copyWith( - energyServices: energyList, + currentOperation: state.currentOperation?.copyWith( + energyOperations: energyList, ), ), ); } - void updateFinServices(List finList) { + void updateFinOperations(List finList) { emit( state.copyWith( - currentService: state.currentService?.copyWith(finServices: finList), + currentOperation: state.currentOperation?.copyWith( + finOperations: finList, + ), ), ); } - void updateEntertainmentServices(List entList) { + void updateEntertainmentOperations( + List entList, + ) { emit( state.copyWith( - currentService: state.currentService?.copyWith( - entertainmentServices: entList, + currentOperation: state.currentOperation?.copyWith( + entertainmentOperations: entList, ), ), ); @@ -202,36 +207,40 @@ class ServicesCubit extends Cubit { // --- PERSISTENZA --- - Future saveCurrentService({ + Future saveCurrentOperation({ required bool isBozza, bool shouldPop = true, - List? files, + List? files, }) async { - if (state.currentService == null) return; + if (state.currentOperation == null) return; - emit(state.copyWith(status: ServicesStatus.saving, errorMessage: null)); + emit(state.copyWith(status: OperationsStatus.saving, errorMessage: null)); try { // 1. Aggiorniamo il flag bozza in base a quale pulsante ha premuto l'utente - final serviceToSave = state.currentService!.copyWith( + final operationToSave = state.currentOperation!.copyWith( isBozza: isBozza, files: files, ); // 2. Salvataggio corazzato - final updatedService = await _repository.saveFullService(serviceToSave); + final updatedOperation = await _repository.saveFullOperation( + operationToSave, + ); // 3. Reset e ricaricamento emit( state.copyWith( - status: shouldPop ? ServicesStatus.saved : ServicesStatus.savedNoPop, - currentService: shouldPop ? null : updatedService, + status: shouldPop + ? OperationsStatus.saved + : OperationsStatus.savedNoPop, + currentOperation: shouldPop ? null : updatedOperation, ), ); - await loadServices(refresh: true); + await loadOperations(refresh: true); } catch (e) { emit( state.copyWith( - status: ServicesStatus.failure, + status: OperationsStatus.failure, errorMessage: e.toString(), ), ); @@ -242,9 +251,9 @@ class ServicesCubit extends Cubit { void addAttachments(List files) { final newAttachments = files.map((file) { - return ServiceFileModel( + return OperationFileModel( id: null, // Meglio null se non è su DB - serviceId: state.currentService?.id ?? '', + operationId: state.currentOperation?.id ?? '', name: file.name.fileNameWithoutExtension(), extension: file.name.fileExtension(), storagePath: '', @@ -255,44 +264,46 @@ class ServicesCubit extends Cubit { }).toList(); // Creiamo una nuova lista pulita - final List updatedList = [ - ...(state.currentService?.files ?? []), + final List updatedList = [ + ...(state.currentOperation?.files ?? []), ...newAttachments, ]; - // Emettiamo lo stato assicurandoci che il ServiceModel venga clonato - if (state.currentService != null) { + // Emettiamo lo stato assicurandoci che il OperationModel venga clonato + if (state.currentOperation != null) { emit( state.copyWith( - currentService: state.currentService!.copyWith(files: updatedList), + currentOperation: state.currentOperation!.copyWith( + files: updatedList, + ), ), ); } } void removeAttachment(int index) { - if (state.currentService == null) return; + if (state.currentOperation == null) return; - final updatedList = List.from( - state.currentService!.files, + final updatedList = List.from( + state.currentOperation!.files, ); updatedList.removeAt(index); emit( state.copyWith( - currentService: state.currentService?.copyWith(files: updatedList), + currentOperation: state.currentOperation?.copyWith(files: updatedList), ), ); } - void saveAndCopyFileToCustomer(List selectedFiles) async { - final currentService = state.currentService; + void saveAndCopyFileToCustomer(List selectedFiles) async { + final currentOperation = state.currentOperation; // 1. Check di sicurezza: se non c'è il cliente, non sappiamo dove copiare - if (currentService == null || currentService.customerId == null) { + if (currentOperation == null || currentOperation.customerId == null) { emit( state.copyWith( - status: ServicesStatus.failure, + status: OperationsStatus.failure, errorMessage: "Impossibile copiare: nessun cliente associato alla pratica.", ), @@ -300,19 +311,21 @@ class ServicesCubit extends Cubit { return; } - emit(state.copyWith(status: ServicesStatus.loading)); + emit(state.copyWith(status: OperationsStatus.loading)); try { // 2. SALVATAGGIO CORAZZATO // Chiamiamo il repo e otteniamo la pratica con TUTTI i file ora dotati di ID e storagePath - final updatedService = await _repository.saveFullService(currentService); + final updatedOperation = await _repository.saveFullOperation( + currentOperation, + ); // 3. COPIA RELAZIONALE // Per ogni file che l'utente ha selezionato nella UI, cerchiamo la sua versione // "ufficiale" (quella con lo storagePath) nel modello appena tornato dal DB. for (var selectedFile in selectedFiles) { // Cerchiamo il match nel modello aggiornato - final persistedFile = updatedService.files.firstWhere( + final persistedFile = updatedOperation.files.firstWhere( (f) => f.name == selectedFile.name && f.extension == selectedFile.extension, @@ -324,7 +337,7 @@ class ServicesCubit extends Cubit { // Creiamo il link nel database del cliente await _repository.copyFileToCustomer( file: persistedFile, - customerId: currentService.customerId!, + customerId: currentOperation.customerId!, ); } @@ -332,14 +345,14 @@ class ServicesCubit extends Cubit { // Aggiorniamo il Cubit con il servizio salvato così la UI mostra i file come "Remoti" emit( state.copyWith( - status: ServicesStatus.success, - currentService: updatedService, + status: OperationsStatus.success, + currentOperation: updatedOperation, ), ); } catch (e) { emit( state.copyWith( - status: ServicesStatus.failure, + status: OperationsStatus.failure, errorMessage: "Errore durante il salvataggio e copia: $e", ), ); diff --git a/lib/features/operations/blocs/services_state.dart b/lib/features/operations/blocs/operations_state.dart similarity index 60% rename from lib/features/operations/blocs/services_state.dart rename to lib/features/operations/blocs/operations_state.dart index 5af7130..97276ad 100644 --- a/lib/features/operations/blocs/services_state.dart +++ b/lib/features/operations/blocs/operations_state.dart @@ -1,6 +1,6 @@ part of 'operations_cubit.dart'; -enum ServicesStatus { +enum OperationsStatus { initial, loading, ready, @@ -11,20 +11,20 @@ enum ServicesStatus { failure, } -class ServicesState extends Equatable { - final ServicesStatus status; - final List allServices; - final ServiceModel? currentService; // La bozza che stiamo editando +class OperationsState extends Equatable { + final OperationsStatus status; + final List allOperations; + final OperationModel? currentOperation; // La bozza che stiamo editando final String? errorMessage; final String query; final DateTimeRange? dateRange; final bool hasReachedMax; final bool isSavingDraft; - const ServicesState({ + const OperationsState({ required this.status, - this.allServices = const [], - this.currentService, + this.allOperations = const [], + this.currentOperation, this.errorMessage, this.query = '', this.dateRange, @@ -32,20 +32,20 @@ class ServicesState extends Equatable { this.isSavingDraft = false, }); - ServicesState copyWith({ - ServicesStatus? status, - List? allServices, - ServiceModel? currentService, + OperationsState copyWith({ + OperationsStatus? status, + List? allOperations, + OperationModel? currentOperation, String? errorMessage, String? query, DateTimeRange? dateRange, bool? hasReachedMax, bool? isSavingDraft, }) { - return ServicesState( + return OperationsState( status: status ?? this.status, - allServices: allServices ?? this.allServices, - currentService: currentService ?? this.currentService, + allOperations: allOperations ?? this.allOperations, + currentOperation: currentOperation ?? this.currentOperation, errorMessage: errorMessage, query: query ?? this.query, dateRange: dateRange ?? this.dateRange, @@ -57,8 +57,8 @@ class ServicesState extends Equatable { @override List get props => [ status, - allServices, - currentService, + allOperations, + currentOperation, errorMessage, query, dateRange, diff --git a/lib/features/operations/blocs/service_files_bloc.dart b/lib/features/operations/blocs/service_files_bloc.dart deleted file mode 100644 index f34f86d..0000000 --- a/lib/features/operations/blocs/service_files_bloc.dart +++ /dev/null @@ -1,232 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:file_picker/file_picker.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/operations/data/services_repository.dart'; -import 'package:flux/features/operations/models/service_file_model.dart'; -import 'package:flux/features/operations/models/service_model.dart'; -import 'package:get_it/get_it.dart'; - -part 'service_files_events.dart'; -part 'service_files_state.dart'; - -class ServiceFilesBloc extends Bloc { - final _repository = GetIt.I.get(); - final String? serviceId; - - ServiceFilesBloc({this.serviceId}) - : super( - ServiceFilesState( - status: ServiceFilesStatus.initial, - serviceId: serviceId, - ), - ) { - on(_onServiceSaved); - on(_onLoadServiceFiles); - on(_onAddServiceFiles); - on(_onUploadServiceFiles); - on(_onUploadMultipleServiceFiles); - on(_onDeleteServiceFiles); - on(_onToggleServiceFileSelection); - // Se il BLoC nasce con un ID, accendiamo subito lo stream! - if (serviceId != null) { - add(LoadServiceFilesEvent(serviceId: serviceId)); - } - } - - FutureOr _onServiceSaved( - ServiceSavedEvent event, - Emitter emit, - ) { - // 1. Aggiorniamo l'ID nello stato - // 2. PIALLIAMO i file locali: ormai sono partiti per Supabase! - // Così la UI si pulisce all'istante e aspetta quelli remoti. - emit( - state.copyWith( - serviceId: event.serviceId, - localFiles: [], // <-- LA MAGIA ANTI-DUPLICATI - ), - ); - - // Lanciamo il caricamento - add(LoadServiceFilesEvent(serviceId: event.serviceId)); - } - - FutureOr _onLoadServiceFiles( - LoadServiceFilesEvent event, - Emitter emit, - ) async { - // Usiamo l'ID dell'evento, e se non c'è usiamo quello dello stato - final currentId = event.serviceId ?? state.serviceId; - - if (currentId != null) { - emit(state.copyWith(status: ServiceFilesStatus.loading)); - - await emit.forEach( - _repository.getServiceFilesStream( - currentId, - ), // <-- Usiamo l'ID corretto! - onData: (data) => state.copyWith( - status: ServiceFilesStatus.success, - remoteFiles: data, - ), - onError: (error, stackTrace) => state.copyWith( - status: ServiceFilesStatus.failure, - error: error.toString(), - ), - ); - } - } - - void _onAddServiceFiles( - AddServiceFilesEvent event, - Emitter emit, - ) async { - final currentId = state.serviceId; - // BIVIO 1: PRATICA NUOVA (Nessun ID) - if (currentId == null) { - // Mettiamo i file nel "parcheggio" locale dello State - final newLocalFiles = event.files.map((file) { - return ServiceFileModel( - id: null, - serviceId: serviceId ?? '', - name: file.name.fileNameWithoutExtension(), - extension: file.name.fileExtension(), - storagePath: '', - fileSize: file.size, - localBytes: file.bytes, - ); - }).toList(); - final List updatedLocalFiles = [ - ...state.localFiles, - ...newLocalFiles, - ]; - emit( - state.copyWith( - localFiles: updatedLocalFiles, - status: ServiceFilesStatus.success, - ), - ); - return; - } - - // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID) - emit(state.copyWith(status: ServiceFilesStatus.uploading)); - try { - // Logica identica a quella che abbiamo fatto per i clienti - for (var file in event.files) { - await _repository.uploadAndRegisterServiceFile( - serviceId: serviceId!, - pickedFile: file, - ); - } - emit(state.copyWith(status: ServiceFilesStatus.success)); - } catch (e) { - emit( - state.copyWith(status: ServiceFilesStatus.failure, error: e.toString()), - ); - } - } - - FutureOr _onUploadServiceFiles( - UploadServiceFilesEvent event, - Emitter emit, - ) async { - if (event.pickedFiles == null && event.photos == null) return; - if (event.pickedFiles!.isEmpty && event.photos!.isEmpty) return; - - // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID - emit(state.copyWith(status: ServiceFilesStatus.uploading)); - try { - // Logica identica a quella che abbiamo fatto per i clienti - if (event.pickedFiles != null && event.pickedFiles!.isNotEmpty) { - for (var file in event.pickedFiles!) { - await _repository.uploadAndRegisterServiceFile( - serviceId: state.serviceId!, - pickedFile: file, - ); - } - } - emit(state.copyWith(status: ServiceFilesStatus.success)); - } catch (e) { - emit( - state.copyWith(status: ServiceFilesStatus.failure, error: e.toString()), - ); - } - } - - FutureOr _onUploadMultipleServiceFiles( - UploadMultipleServiceFilesEvent event, - Emitter emit, - ) async { - if (event.files.isEmpty) { - emit( - state.copyWith( - status: ServiceFilesStatus.failure, - error: "Nessun file selezionato", - ), - ); - return; - } - emit(state.copyWith(status: ServiceFilesStatus.uploading, error: null)); - try { - // 2. Creiamo una lista di "Promesse" (Futures) per il repository - final List> uploadTasks = []; - for (var file in event.files) { - // Aggiungiamo il task alla lista, ma NON usiamo await qui dentro! - uploadTasks.add( - _repository.uploadAndRegisterServiceFile( - serviceId: state.serviceId!, - pickedFile: file, - ), - ); - } - // 3. ESECUZIONE PARALLELA! - // Aspettiamo che tutti i file siano caricati contemporaneamente. - await Future.wait(uploadTasks); - // 4. GRAN FINALE: Tutto caricato, emettiamo il success! - emit(state.copyWith(status: ServiceFilesStatus.success)); - } catch (e) { - // Se anche un solo file fallisce, catturiamo l'errore - emit( - state.copyWith( - status: ServiceFilesStatus.failure, - error: "Errore durante l'upload multiplo: $e", - ), - ); - } - } - - FutureOr _onDeleteServiceFiles( - DeleteServiceFilesEvent event, - Emitter emit, - ) async { - emit(state.copyWith(status: ServiceFilesStatus.loading)); - try { - await _repository.deleteServiceFiles(state.selectedFiles); - emit( - state.copyWith(status: ServiceFilesStatus.success, selectedFiles: []), - ); - } catch (e) { - emit( - state.copyWith(status: ServiceFilesStatus.failure, error: e.toString()), - ); - } - } - - FutureOr _onToggleServiceFileSelection( - ToggleServiceFileSelectionEvent event, - Emitter emit, - ) { - List selectedFiles = List.from(state.selectedFiles); - if (selectedFiles.contains(event.file)) { - selectedFiles.remove(event.file); - } else { - selectedFiles.add(event.file); - } - emit(state.copyWith(selectedFiles: selectedFiles)); - } -} diff --git a/lib/features/operations/blocs/service_files_events.dart b/lib/features/operations/blocs/service_files_events.dart deleted file mode 100644 index cb02eb2..0000000 --- a/lib/features/operations/blocs/service_files_events.dart +++ /dev/null @@ -1,56 +0,0 @@ -part of 'service_files_bloc.dart'; - -abstract class ServiceFilesEvent extends Equatable { - const ServiceFilesEvent(); - - @override - List get props => []; -} - -class ServiceSavedEvent extends ServiceFilesEvent { - final String serviceId; - const ServiceSavedEvent(this.serviceId); - - @override - List get props => [serviceId]; -} - -class LoadServiceFilesEvent extends ServiceFilesEvent { - final String? serviceId; - final ServiceModel? operation; - const LoadServiceFilesEvent({this.serviceId, this.operation}); - - @override - List get props => [serviceId, operation]; -} - -class AddServiceFilesEvent extends ServiceFilesEvent { - final List files; - const AddServiceFilesEvent(this.files); - - @override - List get props => [files]; -} - -class UploadServiceFilesEvent extends ServiceFilesEvent { - final List? pickedFiles; - final List? photos; - const UploadServiceFilesEvent({this.pickedFiles, this.photos}); - - @override - List get props => [pickedFiles, photos]; -} - -class UploadMultipleServiceFilesEvent extends ServiceFilesEvent { - final List files; - const UploadMultipleServiceFilesEvent(this.files); - @override - List get props => [files]; -} - -class DeleteServiceFilesEvent extends ServiceFilesEvent {} - -class ToggleServiceFileSelectionEvent extends ServiceFilesEvent { - final ServiceFileModel file; - const ToggleServiceFileSelectionEvent(this.file); -} diff --git a/lib/features/operations/blocs/service_files_state.dart b/lib/features/operations/blocs/service_files_state.dart deleted file mode 100644 index f39a133..0000000 --- a/lib/features/operations/blocs/service_files_state.dart +++ /dev/null @@ -1,52 +0,0 @@ -part of 'service_files_bloc.dart'; - -enum ServiceFilesStatus { initial, loading, uploading, success, failure } - -class ServiceFilesState extends Equatable { - const ServiceFilesState({ - this.serviceId, - required this.status, - this.error, - this.localFiles = const [], - this.remoteFiles = const [], - this.selectedFiles = const [], - }); - - final String? serviceId; - final ServiceFilesStatus status; - final String? error; - final List localFiles; - final List remoteFiles; - - final List selectedFiles; - - @override - List get props => [ - serviceId, - status, - error, - localFiles, - remoteFiles, - selectedFiles, - ]; - - List get allFiles => [...remoteFiles, ...localFiles]; - - ServiceFilesState copyWith({ - String? serviceId, - ServiceFilesStatus? status, - String? error, - List? localFiles, - List? remoteFiles, - List? selectedFiles, - }) { - return ServiceFilesState( - serviceId: serviceId ?? this.serviceId, - status: status ?? this.status, - error: error, - localFiles: localFiles ?? this.localFiles, - remoteFiles: remoteFiles ?? this.remoteFiles, - selectedFiles: selectedFiles ?? this.selectedFiles, - ); - } -} diff --git a/lib/features/operations/data/services_repository.dart b/lib/features/operations/data/operations_repository.dart similarity index 75% rename from lib/features/operations/data/services_repository.dart rename to lib/features/operations/data/operations_repository.dart index f0f8233..5d496d4 100644 --- a/lib/features/operations/data/services_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -4,40 +4,40 @@ import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/customers/models/customer_file_model.dart'; -import 'package:flux/features/operations/models/service_file_model.dart'; +import 'package:flux/features/operations/models/operation_file_model.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; -import '../models/service_model.dart'; +import '../models/operation_model.dart'; -class ServicesRepository { +class OperationsRepository { final _supabase = Supabase.instance.client; final companyId = GetIt.I.get().state.company!.id; final CustomerRepository _customerRepository = GetIt.I(); // --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO --- - Future fetchServiceById(String id) async { + Future fetchOperationById(String id) async { try { final response = await _supabase .from('operation') .select(''' *, customer(nome), - energy_service(*), - fin_service(*), - entertainment_service(*), - service_file(*) + energy_operation(*), + fin_operation(*), + entertainment_operation(*), + operation_file(*) ''') .eq('id', id) .single(); - return ServiceModel.fromMap(response); + return OperationModel.fromMap(response); } catch (e) { throw Exception('Errore nel caricamento del servizio: $e'); } } // --- RECUPERO PAGINATO CON FILTRI E JOIN --- - Future> fetchServices({ + Future> fetchOperations({ required String companyId, required int offset, int limit = 50, @@ -51,10 +51,10 @@ class ServicesRepository { .select(''' *, customer(nome), - energy_service(*), - fin_service(*), - entertainment_service(*), - service_file(*) + energy_operation(*), + fin_operation(*), + entertainment_operation(*), + operation_file(*) ''') .eq('company_id', companyId); @@ -77,14 +77,14 @@ class ServicesRepository { .range(offset, offset + limit - 1); return (response as List) - .map((map) => ServiceModel.fromMap(map)) + .map((map) => OperationModel.fromMap(map)) .toList(); } catch (e) { throw Exception('Errore nel caricamento servizi: $e'); } } - Stream> getLastStoreServicesStream({ + Stream> getLastStoreOperationsStream({ required String storeId, required int limit, }) { @@ -96,32 +96,32 @@ class ServicesRepository { .limit(limit) .map( (listOfMaps) => - listOfMaps.map((map) => ServiceModel.fromMap(map)).toList(), + listOfMaps.map((map) => OperationModel.fromMap(map)).toList(), ); } // --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) --- - Future saveFullService(ServiceModel operation) async { + Future saveFullOperation(OperationModel operation) async { try { // 1. Upsert del record principale - final serviceData = await _supabase + final operationData = await _supabase .from('operation') .upsert(operation.toMap()) .select() .single(); - final String newId = serviceData['id']; + final String newId = operationData['id']; // 2. MODIFICA: Pulizia atomica dei figli // Se stiamo modificando (id != null), resettiamo le tabelle collegate if (operation.id != null) { await Future.wait([ - _supabase.from('energy_service').delete().eq('service_id', newId), - _supabase.from('fin_service').delete().eq('service_id', newId), + _supabase.from('energy_operation').delete().eq('operation_id', newId), + _supabase.from('fin_operation').delete().eq('operation_id', newId), _supabase - .from('entertainment_service') + .from('entertainment_operation') .delete() - .eq('service_id', newId), + .eq('operation_id', newId), // Aggiungi qui eventuali altre tabelle pivot o file ]); } @@ -129,37 +129,37 @@ class ServicesRepository { // 3. Inserimento dei moduli in parallelo per velocità final List insertTasks = []; - if (operation.energyServices.isNotEmpty) { + if (operation.energyOperations.isNotEmpty) { insertTasks.add( _supabase - .from('energy_service') + .from('energy_operation') .insert( - operation.energyServices - .map((item) => item.copyWith(serviceId: newId).toMap()) + operation.energyOperations + .map((item) => item.copyWith(operationId: newId).toMap()) .toList(), ), ); } - if (operation.finServices.isNotEmpty) { + if (operation.finOperations.isNotEmpty) { insertTasks.add( _supabase - .from('fin_service') + .from('fin_operation') .insert( - operation.finServices - .map((item) => item.copyWith(serviceId: newId).toMap()) + operation.finOperations + .map((item) => item.copyWith(operationId: newId).toMap()) .toList(), ), ); } - if (operation.entertainmentServices.isNotEmpty) { + if (operation.entertainmentOperations.isNotEmpty) { insertTasks.add( _supabase - .from('entertainment_service') + .from('entertainment_operation') .insert( - operation.entertainmentServices - .map((item) => item.copyWith(serviceId: newId).toMap()) + operation.entertainmentOperations + .map((item) => item.copyWith(operationId: newId).toMap()) .toList(), ), ); @@ -186,7 +186,7 @@ class ServicesRepository { : 'image/${file.extension}'; final fileToSave = file.copyWith( - serviceId: newId, + operationId: newId, storagePath: storagePath, ); @@ -202,7 +202,7 @@ class ServicesRepository { ); // B. Inserimento riga nel DB relazionale - await _supabase.from('service_file').insert(fileToSave.toMap()); + await _supabase.from('operation_file').insert(fileToSave.toMap()); } uploadTasks.add(uploadAndLink()); @@ -214,20 +214,20 @@ class ServicesRepository { // 5. GRAN FINALE: RECUPERO DEL MODELLO COMPLETO E AGGIORNATO // Interroghiamo Supabase per farci restituire la pratica con TUTTI gli ID generati - // (inclusi quelli della tabella service_file appena inseriti) - final updatedServiceData = await _supabase + // (inclusi quelli della tabella operation_file appena inseriti) + final updatedOperationData = await _supabase .from('operation') .select(''' *, - energy_service(*), - fin_service(*), - entertainment_service(*), - service_file(*) + energy_operation(*), + fin_operation(*), + entertainment_operation(*), + operation_file(*) ''') .eq('id', newId) .single(); - return ServiceModel.fromMap(updatedServiceData); + return OperationModel.fromMap(updatedOperationData); } catch (e) { // Qui potresti aggiungere una logica di "rollback manuale" se necessario throw Exception('Errore durante il salvataggio corazzato: $e'); @@ -235,7 +235,7 @@ class ServicesRepository { } // --- ELIMINAZIONE --- - Future deleteService(String id) async { + Future deleteOperation(String id) async { try { await _supabase.from('operation').delete().eq('id', id); } catch (e) { @@ -249,7 +249,7 @@ class ServicesRepository { // Cerchiamo i tipi più frequenti associati ai servizi di questa company // Nota: dobbiamo passare attraverso la tabella 'operation' per filtrare per company_id final response = await _supabase - .from('entertainment_service') + .from('entertainment_operation') .select('type, operation!inner(store!inner(company_id))') .eq('operation.store.company_id', companyId) .limit(100); // Prendiamo un campione @@ -276,20 +276,20 @@ class ServicesRepository { } /// Ascolta in tempo reale i file caricati per una pratica - Stream> getServiceFilesStream(String serviceId) { + Stream> getOperationFilesStream(String operationId) { return _supabase - .from('service_file') + .from('operation_file') .stream(primaryKey: ['id']) - .eq('service_id', serviceId) + .eq('operation_id', operationId) .order('created_at', ascending: false) .map( (listOfMaps) => - listOfMaps.map((map) => ServiceFileModel.fromMap(map)).toList(), + listOfMaps.map((map) => OperationFileModel.fromMap(map)).toList(), ); } - Future uploadAndRegisterServiceFile({ - required String serviceId, + Future uploadAndRegisterOperationFile({ + required String operationId, required PlatformFile pickedFile, }) async { final cleanFileName = pickedFile.name.replaceAll( @@ -297,10 +297,10 @@ class ServicesRepository { '_', ); final storagePath = - '$companyId/operations/$serviceId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; + '$companyId/operations/$operationId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; final int fileSize = pickedFile.size; - final fileToSave = ServiceFileModel( - serviceId: serviceId, + final fileToSave = OperationFileModel( + operationId: operationId, name: cleanFileName.fileNameWithoutExtension(), extension: cleanFileName.fileExtension(), storagePath: storagePath, @@ -327,19 +327,19 @@ class ServicesRepository { } final response = await _supabase - .from('service_file') + .from('operation_file') .insert(fileToSave.toMap()) .select() .single(); - return ServiceFileModel.fromMap(response); + return OperationFileModel.fromMap(response); } catch (e) { throw 'Errore durante l\'upload: $e'; } } Future copyFileToCustomer({ - required ServiceFileModel file, + required OperationFileModel file, required String customerId, }) async { CustomerFileModel fileToCopy = CustomerFileModel( @@ -352,14 +352,17 @@ class ServicesRepository { await _customerRepository.saveFileReference(fileToCopy); } - Future deleteServiceFiles(List files) async { + Future deleteOperationFiles(List files) async { if (files.isEmpty) return; // 1. Prepariamo le liste di ID e di Percorsi final List idsToDelete = files.map((f) => f.id!).toList(); final List storagePaths = files.map((f) => f.storagePath).toList(); try { - await _supabase.from('service_file').delete().inFilter('id', idsToDelete); + await _supabase + .from('operation_file') + .delete() + .inFilter('id', idsToDelete); await _supabase.storage.from('documents').remove(storagePaths); diff --git a/lib/features/operations/models/energy_service_model.dart b/lib/features/operations/models/energy_operation_model.dart similarity index 74% rename from lib/features/operations/models/energy_service_model.dart rename to lib/features/operations/models/energy_operation_model.dart index 9cf9b54..817d76e 100644 --- a/lib/features/operations/models/energy_service_model.dart +++ b/lib/features/operations/models/energy_operation_model.dart @@ -2,38 +2,38 @@ import 'package:equatable/equatable.dart'; enum EnergyType { luce, gas } // Mappa il tuo public.energy_type -class EnergyServiceModel extends Equatable { +class EnergyOperationModel extends Equatable { final String? id; final DateTime? createdAt; final EnergyType type; final DateTime expiration; final String providerId; - final String? serviceId; + final String? operationId; - const EnergyServiceModel({ + const EnergyOperationModel({ this.id, this.createdAt, required this.type, required this.expiration, required this.providerId, - this.serviceId, + this.operationId, }); - EnergyServiceModel copyWith({ + EnergyOperationModel copyWith({ String? id, DateTime? createdAt, EnergyType? type, DateTime? expiration, String? providerId, - String? serviceId, + String? operationId, }) { - return EnergyServiceModel( + return EnergyOperationModel( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, type: type ?? this.type, expiration: expiration ?? this.expiration, providerId: providerId ?? this.providerId, - serviceId: serviceId ?? this.serviceId, + operationId: operationId ?? this.operationId, ); } @@ -44,11 +44,11 @@ class EnergyServiceModel extends Equatable { type, expiration, providerId, - serviceId, + operationId, ]; - factory EnergyServiceModel.fromMap(Map map) { - return EnergyServiceModel( + factory EnergyOperationModel.fromMap(Map map) { + return EnergyOperationModel( id: map['id'], createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) @@ -56,7 +56,7 @@ class EnergyServiceModel extends Equatable { type: map['type'] == 'gas' ? EnergyType.gas : EnergyType.luce, expiration: DateTime.parse(map['expiration']), providerId: map['provider_id'], - serviceId: map['service_id'], + operationId: map['operation_id'], ); } @@ -66,7 +66,7 @@ class EnergyServiceModel extends Equatable { 'type': type.name, // .name trasforma l'enum in 'luce' o 'gas' 'expiration': expiration.toIso8601String(), 'provider_id': providerId, - 'service_id': serviceId, + 'operation_id': operationId, }; } } diff --git a/lib/features/operations/models/entertainment_service_model.dart b/lib/features/operations/models/entertainment_operation_model.dart similarity index 75% rename from lib/features/operations/models/entertainment_service_model.dart rename to lib/features/operations/models/entertainment_operation_model.dart index f34743a..49930b3 100644 --- a/lib/features/operations/models/entertainment_service_model.dart +++ b/lib/features/operations/models/entertainment_operation_model.dart @@ -1,40 +1,40 @@ import 'package:equatable/equatable.dart'; -class EntertainmentServiceModel extends Equatable { +class EntertainmentOperationModel extends Equatable { final String? id; final DateTime? createdAt; final String type; // es. Sky, DAZN, ecc. final bool constrained; // Vincolato? final DateTime constrainExpiration; - final String? serviceId; + final String? operationId; final String? providerId; - const EntertainmentServiceModel({ + const EntertainmentOperationModel({ this.id, this.createdAt, required this.type, required this.constrained, required this.constrainExpiration, - this.serviceId, + this.operationId, this.providerId, }); - EntertainmentServiceModel copyWith({ + EntertainmentOperationModel copyWith({ String? id, DateTime? createdAt, String? type, bool? constrained, DateTime? constrainExpiration, - String? serviceId, + String? operationId, String? providerId, }) { - return EntertainmentServiceModel( + return EntertainmentOperationModel( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, type: type ?? this.type, constrained: constrained ?? this.constrained, constrainExpiration: constrainExpiration ?? this.constrainExpiration, - serviceId: serviceId ?? this.serviceId, + operationId: operationId ?? this.operationId, providerId: providerId ?? this.providerId, ); } @@ -46,12 +46,12 @@ class EntertainmentServiceModel extends Equatable { type, constrained, constrainExpiration, - serviceId, + operationId, providerId, ]; - factory EntertainmentServiceModel.fromMap(Map map) { - return EntertainmentServiceModel( + factory EntertainmentOperationModel.fromMap(Map map) { + return EntertainmentOperationModel( id: map['id'], createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) @@ -59,7 +59,7 @@ class EntertainmentServiceModel extends Equatable { type: map['type'], constrained: map['constrained'] ?? false, constrainExpiration: DateTime.parse(map['constrain_expiration']), - serviceId: map['service_id'], + operationId: map['operation_id'], providerId: map['provider_id'], ); } @@ -70,7 +70,7 @@ class EntertainmentServiceModel extends Equatable { 'type': type, 'constrained': constrained, 'constrain_expiration': constrainExpiration.toIso8601String(), - 'service_id': serviceId, + 'operation_id': operationId, 'provider_id': providerId, }; } diff --git a/lib/features/operations/models/fin_service_model.dart b/lib/features/operations/models/fin_operation_model.dart similarity index 69% rename from lib/features/operations/models/fin_service_model.dart rename to lib/features/operations/models/fin_operation_model.dart index 9cdaa5a..d7bf513 100644 --- a/lib/features/operations/models/fin_service_model.dart +++ b/lib/features/operations/models/fin_operation_model.dart @@ -1,51 +1,51 @@ import 'package:equatable/equatable.dart'; -class FinServiceModel extends Equatable { +class FinOperationModel extends Equatable { final String? id; final DateTime? createdAt; final DateTime expiration; - final String? serviceId; + final String? operationId; final String? modelId; // FK verso model (es. iPhone, Samsung, ecc.) final String? providerId; - const FinServiceModel({ + const FinOperationModel({ this.id, this.createdAt, required this.expiration, - this.serviceId, + this.operationId, this.modelId, this.providerId, }); - FinServiceModel copyWith({ + FinOperationModel copyWith({ String? id, DateTime? createdAt, DateTime? expiration, - String? serviceId, + String? operationId, String? modelId, String? providerId, }) { - return FinServiceModel( + return FinOperationModel( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, expiration: expiration ?? this.expiration, - serviceId: serviceId ?? this.serviceId, + operationId: operationId ?? this.operationId, modelId: modelId ?? this.modelId, providerId: providerId ?? this.providerId, ); } @override - List get props => [id, createdAt, expiration, serviceId, modelId]; + List get props => [id, createdAt, expiration, operationId, modelId]; - factory FinServiceModel.fromMap(Map map) { - return FinServiceModel( + factory FinOperationModel.fromMap(Map map) { + return FinOperationModel( id: map['id'], createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) : null, expiration: DateTime.parse(map['expiration']), - serviceId: map['service_id'], + operationId: map['operation_id'], modelId: map['model_id'], providerId: map['provider_id'], ); @@ -55,7 +55,7 @@ class FinServiceModel extends Equatable { return { if (id != null) 'id': id, 'expiration': expiration.toIso8601String(), - 'service_id': serviceId, + 'operation_id': operationId, 'model_id': modelId, 'provider_id': providerId, }; diff --git a/lib/features/operations/models/service_file_model.dart b/lib/features/operations/models/operation_file_model.dart similarity index 81% rename from lib/features/operations/models/service_file_model.dart rename to lib/features/operations/models/operation_file_model.dart index f804166..376c8c1 100644 --- a/lib/features/operations/models/service_file_model.dart +++ b/lib/features/operations/models/operation_file_model.dart @@ -2,23 +2,23 @@ import 'dart:typed_data'; import 'package:equatable/equatable.dart'; -class ServiceFileModel extends Equatable { +class OperationFileModel extends Equatable { final String? id; final DateTime? createdAt; final String name; final String extension; final String storagePath; - final String serviceId; + final String operationId; final int fileSize; final Uint8List? localBytes; - const ServiceFileModel({ + const OperationFileModel({ this.id, this.createdAt, required this.name, required this.extension, required this.storagePath, - required this.serviceId, + required this.operationId, required this.fileSize, this.localBytes, }); @@ -37,30 +37,30 @@ class ServiceFileModel extends Equatable { bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf'; - ServiceFileModel copyWith({ + OperationFileModel copyWith({ String? id, DateTime? createdAt, String? name, String? extension, String? storagePath, - String? serviceId, + String? operationId, int? fileSize, Uint8List? localBytes, }) { - return ServiceFileModel( + return OperationFileModel( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, name: name ?? this.name, extension: extension ?? this.extension, storagePath: storagePath ?? this.storagePath, - serviceId: serviceId ?? this.serviceId, + operationId: operationId ?? this.operationId, fileSize: fileSize ?? this.fileSize, localBytes: localBytes ?? this.localBytes, ); } - factory ServiceFileModel.fromMap(Map map) { - return ServiceFileModel( + factory OperationFileModel.fromMap(Map map) { + return OperationFileModel( id: map['id'] as String, createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) @@ -68,7 +68,7 @@ class ServiceFileModel extends Equatable { name: map['name'] ?? '', extension: map['extension'] ?? '', storagePath: map['storage_path'] ?? '', - serviceId: map['service_id']?.toString() ?? '', + operationId: map['operation_id']?.toString() ?? '', fileSize: map['file_size'] is int ? map['file_size'] : int.tryParse(map['file_size']?.toString() ?? '0') ?? 0, @@ -81,7 +81,7 @@ class ServiceFileModel extends Equatable { 'name': name, 'extension': extension, 'storage_path': storagePath, - 'service_id': serviceId, + 'operation_id': operationId, 'file_size': fileSize, }; } @@ -93,7 +93,7 @@ class ServiceFileModel extends Equatable { name, extension, storagePath, - serviceId, + operationId, fileSize, localBytes, ]; diff --git a/lib/features/operations/models/service_model.dart b/lib/features/operations/models/operation_model.dart similarity index 67% rename from lib/features/operations/models/service_model.dart rename to lib/features/operations/models/operation_model.dart index 86c0bf4..b938d07 100644 --- a/lib/features/operations/models/service_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -1,11 +1,11 @@ import 'package:equatable/equatable.dart'; import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/operations/models/energy_service_model.dart'; -import 'package:flux/features/operations/models/entertainment_service_model.dart'; -import 'package:flux/features/operations/models/fin_service_model.dart'; -import 'package:flux/features/operations/models/service_file_model.dart'; // <-- Aggiunto Import +import 'package:flux/features/operations/models/energy_operation_model.dart'; +import 'package:flux/features/operations/models/entertainment_operation_model.dart'; +import 'package:flux/features/operations/models/fin_operation_model.dart'; +import 'package:flux/features/operations/models/operation_file_model.dart'; // <-- Aggiunto Import -class ServiceModel extends Equatable { +class OperationModel extends Equatable { final String? id; final DateTime? createdAt; final String storeId; @@ -26,14 +26,14 @@ class ServiceModel extends Equatable { final int telepass; // Moduli (Liste) - final List energyServices; - final List finServices; - final List entertainmentServices; + final List energyOperations; + final List finOperations; + final List entertainmentOperations; // ALLEGATI (Aggiunto) - final List files; + final List files; - const ServiceModel({ + const OperationModel({ this.id, this.createdAt, required this.storeId, @@ -48,15 +48,15 @@ class ServiceModel extends Equatable { this.nip = 0, this.unica = 0, this.telepass = 0, - this.energyServices = const [], - this.finServices = const [], - this.entertainmentServices = const [], + this.energyOperations = const [], + this.finOperations = const [], + this.entertainmentOperations = const [], this.files = const [], // <-- Aggiunto default vuoto this.customerDisplayName, required this.companyId, }); - ServiceModel copyWith({ + OperationModel copyWith({ String? id, DateTime? createdAt, String? storeId, @@ -71,14 +71,14 @@ class ServiceModel extends Equatable { int? nip, int? unica, int? telepass, - List? energyServices, - List? finServices, - List? entertainmentServices, - List? files, // <-- Aggiunto + List? energyOperations, + List? finOperations, + List? entertainmentOperations, + List? files, // <-- Aggiunto String? customerDisplayName, String? companyId, }) { - return ServiceModel( + return OperationModel( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, storeId: storeId ?? this.storeId, @@ -93,10 +93,10 @@ class ServiceModel extends Equatable { nip: nip ?? this.nip, unica: unica ?? this.unica, telepass: telepass ?? this.telepass, - energyServices: energyServices ?? this.energyServices, - finServices: finServices ?? this.finServices, - entertainmentServices: - entertainmentServices ?? this.entertainmentServices, + energyOperations: energyOperations ?? this.energyOperations, + finOperations: finOperations ?? this.finOperations, + entertainmentOperations: + entertainmentOperations ?? this.entertainmentOperations, files: files ?? this.files, // <-- Aggiunto customerDisplayName: customerDisplayName ?? this.customerDisplayName, companyId: companyId ?? this.companyId, @@ -119,16 +119,16 @@ class ServiceModel extends Equatable { nip, unica, telepass, - energyServices, - finServices, - entertainmentServices, + energyOperations, + finOperations, + entertainmentOperations, files, // <-- Aggiunto customerDisplayName, companyId, ]; - factory ServiceModel.fromMap(Map map) { - return ServiceModel( + factory OperationModel.fromMap(Map map) { + return OperationModel( id: map['id'].toString(), createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) @@ -147,26 +147,26 @@ class ServiceModel extends Equatable { telepass: map['telepass'] ?? 0, // Estrazione sicura liste collegate - energyServices: - (map['energy_service'] as List?) - ?.map((x) => EnergyServiceModel.fromMap(x)) + energyOperations: + (map['energy_operation'] as List?) + ?.map((x) => EnergyOperationModel.fromMap(x)) .toList() ?? const [], - finServices: - (map['fin_service'] as List?) - ?.map((x) => FinServiceModel.fromMap(x)) + finOperations: + (map['fin_operation'] as List?) + ?.map((x) => FinOperationModel.fromMap(x)) .toList() ?? const [], - entertainmentServices: - (map['entertainment_service'] as List?) - ?.map((x) => EntertainmentServiceModel.fromMap(x)) + entertainmentOperations: + (map['entertainment_operation'] as List?) + ?.map((x) => EntertainmentOperationModel.fromMap(x)) .toList() ?? const [], // I FILE! (Assicurati che la foreign key su Supabase usi esattamente questo nome) files: - (map['service_file'] as List?) - ?.map((x) => ServiceFileModel.fromMap(x)) + (map['operation_file'] as List?) + ?.map((x) => OperationFileModel.fromMap(x)) .toList() ?? const [], diff --git a/lib/features/operations/ui/service_action_card.dart b/lib/features/operations/ui/operation_action_card.dart similarity index 96% rename from lib/features/operations/ui/service_action_card.dart rename to lib/features/operations/ui/operation_action_card.dart index ef06dc7..c54c2e8 100644 --- a/lib/features/operations/ui/service_action_card.dart +++ b/lib/features/operations/ui/operation_action_card.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -class ServiceActionCard extends StatelessWidget { +class OperationActionCard extends StatelessWidget { final String title; final IconData icon; final VoidCallback onTap; final Color color; final int count; - const ServiceActionCard({ + const OperationActionCard({ super.key, required this.title, required this.icon, diff --git a/lib/features/operations/ui/service_form_screen/action_card.dart b/lib/features/operations/ui/operation_form_screen/action_card.dart similarity index 100% rename from lib/features/operations/ui/service_form_screen/action_card.dart rename to lib/features/operations/ui/operation_form_screen/action_card.dart diff --git a/lib/features/operations/ui/service_form_screen/attachment_section.dart b/lib/features/operations/ui/operation_form_screen/attachment_section.dart similarity index 87% rename from lib/features/operations/ui/service_form_screen/attachment_section.dart rename to lib/features/operations/ui/operation_form_screen/attachment_section.dart index 1a5e960..5980313 100644 --- a/lib/features/operations/ui/service_form_screen/attachment_section.dart +++ b/lib/features/operations/ui/operation_form_screen/attachment_section.dart @@ -5,9 +5,9 @@ import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/widgets/image_viewer_widget.dart'; import 'package:flux/core/widgets/pdf_viewer_widget.dart'; import 'package:flux/core/widgets/qr_upload_dialog.dart'; -import 'package:flux/features/operations/blocs/service_files_bloc.dart'; +import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/service_file_model.dart'; +import 'package:flux/features/operations/models/operation_file_model.dart'; class AttachmentsSection extends StatelessWidget { const AttachmentsSection({super.key}); @@ -22,27 +22,29 @@ class AttachmentsSection extends StatelessWidget { ); if (result != null && context.mounted) { - context.read().add(AddServiceFilesEvent(result.files)); + context.read().add( + AddOperationFilesEvent(result.files), + ); } } @override Widget build(BuildContext context) { - ServiceFilesBloc serviceFilesBloc = BlocProvider.of( + OperationFilesBloc operationFilesBloc = BlocProvider.of( context, ); - return BlocListener( + return BlocListener( listenWhen: (previous, current) => - previous.currentService?.id == null && - current.currentService?.id != null, + previous.currentOperation?.id == null && + current.currentOperation?.id != null, listener: (context, state) { // FIGASSA! La pratica è stata salvata e ora ha un ID. // Diciamo al Bloc dei file di agganciarsi al database. - final newId = state.currentService!.id!; - context.read().add(ServiceSavedEvent(newId)); + final newId = state.currentOperation!.id!; + context.read().add(OperationsavedEvent(newId)); }, - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -125,8 +127,8 @@ class AttachmentsSection extends StatelessWidget { final isSelected = state.selectedFiles.contains(file); return GestureDetector( - onTap: () => serviceFilesBloc.add( - ToggleServiceFileSelectionEvent(file), + onTap: () => operationFilesBloc.add( + ToggleOperationFileSelectionEvent(file), ), onDoubleTap: () => _handleDoubleClick(context, file), child: Card( @@ -216,7 +218,7 @@ class AttachmentsSection extends StatelessWidget { label: const Text("Elimina"), onPressed: () { // Qui lancerai l'evento per eliminare i file selezionati! - // Es: serviceFilesBloc.add(DeleteSelectedFilesEvent()); + // Es: operationFilesBloc.add(DeleteSelectedFilesEvent()); }, ), const SizedBox(width: 8), @@ -243,14 +245,14 @@ class AttachmentsSection extends StatelessWidget { } Future _handleGenerateQr(BuildContext context) async { - final cubit = context.read(); - var currentService = cubit.state.currentService; + final cubit = context.read(); + var currentOperation = cubit.state.currentOperation; // 1. CATTURIAMO IL BLOC MENTRE SIAMO ANCORA NELLA PAGINA - final serviceFilesBloc = context.read(); + final operationFilesBloc = context.read(); // 2. SE LA PRATICA E' NUOVA (Manca l'ID) - if (currentService == null || currentService.id == null) { + if (currentOperation == null || currentOperation.id == null) { // NIENTE BlocListener qui! Solo un semplice Dialog di conferma final bool? confirm = await showDialog( context: context, @@ -275,42 +277,42 @@ class AttachmentsSection extends StatelessWidget { if (confirm != true) return; // Utente ha annullato // Salviamo forzatamente in bozza - await cubit.saveCurrentService( + await cubit.saveCurrentOperation( isBozza: true, shouldPop: false, - files: serviceFilesBloc.state.localFiles, + files: operationFilesBloc.state.localFiles, ); // Recuperiamo il servizio aggiornato con l'ID! - currentService = cubit.state.currentService; + currentOperation = cubit.state.currentOperation; - if (currentService?.id == null) return; + if (currentOperation?.id == null) return; } // 3. MOSTRIAMO IL QR CODE (Con il Ponte e l'Auto-Chiusura!) if (context.mounted) { - final nomePratica = "Pratica ${currentService?.customerDisplayName ?? ''}" - .trim(); + final nomePratica = + "Pratica ${currentOperation?.customerDisplayName ?? ''}".trim(); showDialog( context: context, builder: (dialogContext) => BlocProvider.value( // INIETTIAMO IL BLOC NEL CONTESTO DEL DIALOG ALIENO - value: serviceFilesBloc, + value: operationFilesBloc, // ORA METTIAMO L'AUTO-CHIUSURA SUL QR CODE! - child: BlocListener( + child: BlocListener( listener: (context, state) { // Se arrivano file remoti e lo stato è success, chiudiamo il QR! // (Nota: usiamo dialogContext per assicurarci di chiudere il popup giusto) - if (state.status == ServiceFilesStatus.success && + if (state.status == OperationFilesStatus.success && state.remoteFiles.isNotEmpty) { Navigator.of(dialogContext).pop(); } }, child: QrUploadDialog( deepLinkUrl: - 'fluxapp:///operation/${currentService!.id}/upload?name=${Uri.encodeComponent(nomePratica)}', + 'fluxapp:///operation/${currentOperation!.id}/upload?name=${Uri.encodeComponent(nomePratica)}', title: 'Scatta per\n$nomePratica', ), ), @@ -322,7 +324,7 @@ class AttachmentsSection extends StatelessWidget { // --- LOGICA DI COPIA AL CLIENTE --- void saveAndCopyFilesToCustomer( BuildContext context, - List files, + List files, ) { showDialog( context: context, @@ -341,7 +343,7 @@ class AttachmentsSection extends StatelessWidget { onPressed: () { Navigator.pop(ctx); // 1. Diciamo al Cubit di salvare in Bozza e fare la copia - context.read().saveAndCopyFileToCustomer(files); + context.read().saveAndCopyFileToCustomer(files); }, child: const Text("Salva e Copia"), ), @@ -351,7 +353,7 @@ class AttachmentsSection extends StatelessWidget { } // --- LOGICA DI VISUALIZZAZIONE OVERLAY --- - void _handleDoubleClick(BuildContext context, ServiceFileModel file) { + void _handleDoubleClick(BuildContext context, OperationFileModel file) { showDialog( context: context, barrierDismissible: true, diff --git a/lib/features/operations/ui/service_form_screen/customer_section.dart b/lib/features/operations/ui/operation_form_screen/customer_section.dart similarity index 96% rename from lib/features/operations/ui/service_form_screen/customer_section.dart rename to lib/features/operations/ui/operation_form_screen/customer_section.dart index bb0a7e8..b3546db 100644 --- a/lib/features/operations/ui/service_form_screen/customer_section.dart +++ b/lib/features/operations/ui/operation_form_screen/customer_section.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flux/features/customers/ui/customer_search_sheet.dart'; -import 'package:flux/features/operations/models/service_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; class CustomerSection extends StatelessWidget { - final ServiceModel operation; + final OperationModel operation; const CustomerSection({super.key, required this.operation}); diff --git a/lib/features/operations/ui/service_form_screen/energy_service_dialog.dart b/lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart similarity index 94% rename from lib/features/operations/ui/service_form_screen/energy_service_dialog.dart rename to lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart index 04c0ef4..f44dd40 100644 --- a/lib/features/operations/ui/service_form_screen/energy_service_dialog.dart +++ b/lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart @@ -2,32 +2,32 @@ 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/operations/models/energy_service_model.dart'; // Assicurati degli import +import 'package:flux/features/operations/models/energy_operation_model.dart'; // Assicurati degli import -class EnergyServiceDialog extends StatefulWidget { - final List initialServices; +class EnergyOperationDialog extends StatefulWidget { + final List initialOperations; final String currentStoreId; // Ci serve per sapere per quale negozio caricare i gestori - const EnergyServiceDialog({ + const EnergyOperationDialog({ super.key, - required this.initialServices, + required this.initialOperations, required this.currentStoreId, }); @override - State createState() => _EnergyServiceDialogState(); + State createState() => _EnergyOperationDialogState(); } -class _EnergyServiceDialogState extends State { +class _EnergyOperationDialogState extends State { // Lista temporanea per non "sporcare" il cubit finché non si preme Conferma - late List _tempList; + late List _tempList; bool _isAddingNew = false; @override void initState() { super.initState(); - _tempList = List.from(widget.initialServices); + _tempList = List.from(widget.initialOperations); // Al caricamento della modale, chiediamo al Cubit di recuperare i gestori veri! context.read().loadActiveProvidersForStore( widget.currentStoreId, @@ -52,9 +52,9 @@ class _EnergyServiceDialogState extends State { // Cambia vista in base al flag child: _isAddingNew ? _EnergyForm( - onSave: (newService) { + onSave: (newOperation) { setState(() { - _tempList.add(newService); + _tempList.add(newOperation); _isAddingNew = false; // Torna alla lista }); }, @@ -101,7 +101,7 @@ class _EnergyServiceDialogState extends State { // VISTA 1: LA LISTA DEI CONTRATTI // ========================================== class _EnergyList extends StatelessWidget { - final List operations; + final List operations; final List activeProviders; // <--- NUOVO: La lista vera dal Cubit final Function(int) onDelete; @@ -193,7 +193,7 @@ class _EnergyList extends StatelessWidget { // VISTA 2: IL FORM DI INSERIMENTO // ========================================== class _EnergyForm extends StatefulWidget { - final Function(EnergyServiceModel) onSave; + final Function(EnergyOperationModel) onSave; final VoidCallback onCancel; const _EnergyForm({required this.onSave, required this.onCancel}); @@ -400,12 +400,12 @@ class _EnergyFormState extends State<_EnergyForm> { (_selectedProviderId == null || _selectedExpiration == null) ? null // Disabilitato se mancano dati obbligatori : () { - final newService = EnergyServiceModel( + final newOperation = EnergyOperationModel( type: _selectedType, expiration: _selectedExpiration!, providerId: _selectedProviderId!, ); - widget.onSave(newService); + widget.onSave(newOperation); }, child: const Text("Salva Contratto"), ), diff --git a/lib/features/operations/ui/service_form_screen/entertainment_service_card.dart b/lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart similarity index 92% rename from lib/features/operations/ui/service_form_screen/entertainment_service_card.dart rename to lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart index d7d08a5..88fbc27 100644 --- a/lib/features/operations/ui/service_form_screen/entertainment_service_card.dart +++ b/lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart @@ -3,34 +3,34 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.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/operations/data/services_repository.dart'; -import 'package:flux/features/operations/models/entertainment_service_model.dart'; +import 'package:flux/features/operations/data/operations_repository.dart'; +import 'package:flux/features/operations/models/entertainment_operation_model.dart'; import 'package:get_it/get_it.dart'; -class EntertainmentServiceDialog extends StatefulWidget { - final List initialServices; +class EntertainmentOperationDialog extends StatefulWidget { + final List initialOperations; final String currentStoreId; - const EntertainmentServiceDialog({ + const EntertainmentOperationDialog({ super.key, - required this.initialServices, + required this.initialOperations, required this.currentStoreId, }); @override - State createState() => - _EntertainmentServiceDialogState(); + State createState() => + _EntertainmentOperationDialogState(); } -class _EntertainmentServiceDialogState - extends State { - late List _tempList; +class _EntertainmentOperationDialogState + extends State { + late List _tempList; bool _isAddingNew = false; @override void initState() { super.initState(); - _tempList = List.from(widget.initialServices); + _tempList = List.from(widget.initialOperations); // Carichiamo i provider attivi per lo store corrente context.read().loadActiveProvidersForStore( widget.currentStoreId, @@ -57,8 +57,8 @@ class _EntertainmentServiceDialogState child: _isAddingNew ? _EntertainmentForm( // Il form che abbiamo creato prima - onSave: (newService) => setState(() { - _tempList.add(newService); + onSave: (newOperation) => setState(() { + _tempList.add(newOperation); _isAddingNew = false; }), onCancel: () => setState(() => _isAddingNew = false), @@ -94,7 +94,7 @@ class _EntertainmentServiceDialogState } class _EntertainmentList extends StatelessWidget { - final List operations; + final List operations; final List allProviders; final Function(int) onDelete; final VoidCallback onAddTap; @@ -194,7 +194,7 @@ class _EntertainmentList extends StatelessWidget { // ---ENTERTAINMENT FORM (MODALE)--- class _EntertainmentForm extends StatefulWidget { - final Function(EntertainmentServiceModel) onSave; + final Function(EntertainmentOperationModel) onSave; final VoidCallback onCancel; const _EntertainmentForm({required this.onSave, required this.onCancel}); @@ -280,7 +280,7 @@ class _EntertainmentFormState extends State<_EntertainmentForm> { const SizedBox(height: 8), // Suggerimenti rapidi (Chip) FutureBuilder>( - future: GetIt.I().fetchTopEntertainmentTypes( + future: GetIt.I().fetchTopEntertainmentTypes( GetIt.I().state.company!.id!, ), builder: (context, snapshot) { @@ -376,7 +376,7 @@ class _EntertainmentFormState extends State<_EntertainmentForm> { (_selectedProviderId == null || _typeController.text.isEmpty) ? null : () => widget.onSave( - EntertainmentServiceModel( + EntertainmentOperationModel( providerId: _selectedProviderId!, type: _typeController.text, constrained: _isConstrained, diff --git a/lib/features/operations/ui/service_form_screen/finance_service_dialog.dart b/lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart similarity index 96% rename from lib/features/operations/ui/service_form_screen/finance_service_dialog.dart rename to lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart index a8196c9..fa838a9 100644 --- a/lib/features/operations/ui/service_form_screen/finance_service_dialog.dart +++ b/lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart @@ -5,36 +5,36 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; import 'package:flux/features/master_data/products/models/model_model.dart'; import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; -import 'package:flux/features/operations/models/fin_service_model.dart'; +import 'package:flux/features/operations/models/fin_operation_model.dart'; import 'package:flux/features/master_data/providers/models/provider_model.dart'; // =========================================================================== // DIALOG PRINCIPALE // =========================================================================== -class FinanceServiceDialog extends StatefulWidget { - final List initialServices; +class FinanceOperationDialog extends StatefulWidget { + final List initialOperations; final String currentStoreId; final ProductCubit productCubit; - const FinanceServiceDialog({ + const FinanceOperationDialog({ super.key, - required this.initialServices, + required this.initialOperations, required this.currentStoreId, required this.productCubit, }); @override - State createState() => _FinanceServiceDialogState(); + State createState() => _FinanceOperationDialogState(); } -class _FinanceServiceDialogState extends State { - late List _tempList; +class _FinanceOperationDialogState extends State { + late List _tempList; bool _isAddingNew = false; @override void initState() { super.initState(); - _tempList = List.from(widget.initialServices); + _tempList = List.from(widget.initialOperations); // Carichiamo i dati necessari dai Cubit context.read().loadActiveProvidersForStore( widget.currentStoreId, @@ -109,7 +109,7 @@ class _FinanceServiceDialogState extends State { // VISTA LISTA (STORICA) // =========================================================================== class _FinanceList extends StatelessWidget { - final List operations; + final List operations; final List allProviders; final List allModels; final Function(int) onDelete; @@ -221,7 +221,7 @@ class _FinanceList extends StatelessWidget { // FORM CON OMNI-SEARCH // =========================================================================== class _FinanceForm extends StatefulWidget { - final Function(FinServiceModel) onSave; + final Function(FinOperationModel) onSave; final VoidCallback onCancel; const _FinanceForm({required this.onSave, required this.onCancel}); @@ -428,7 +428,7 @@ class _FinanceFormState extends State<_FinanceForm> { : () { final now = DateTime.now(); widget.onSave( - FinServiceModel( + FinOperationModel( providerId: _selectedProviderId!, modelId: _selectedModel!.id!, expiration: DateTime( diff --git a/lib/features/operations/ui/service_form_screen/general_info_section.dart b/lib/features/operations/ui/operation_form_screen/general_info_section.dart similarity index 88% rename from lib/features/operations/ui/service_form_screen/general_info_section.dart rename to lib/features/operations/ui/operation_form_screen/general_info_section.dart index f679b9f..ceaf3db 100644 --- a/lib/features/operations/ui/service_form_screen/general_info_section.dart +++ b/lib/features/operations/ui/operation_form_screen/general_info_section.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/service_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; class GeneralInfoSection extends StatelessWidget { - final ServiceModel operation; + final OperationModel operation; const GeneralInfoSection({super.key, required this.operation}); @override @@ -44,7 +44,7 @@ class GeneralInfoSection extends StatelessWidget { prefixIcon: Icon(Icons.phone), ), onChanged: (val) { - context.read().updateField(number: val); + context.read().updateField(number: val); }, ), const SizedBox(height: 16), @@ -63,7 +63,7 @@ class GeneralInfoSection extends StatelessWidget { activeThumbColor: Colors.orange, contentPadding: EdgeInsets.zero, onChanged: (val) { - context.read().updateField(isBozza: val); + context.read().updateField(isBozza: val); }, ), ), @@ -79,7 +79,9 @@ class GeneralInfoSection extends StatelessWidget { activeThumbColor: Colors.green, contentPadding: EdgeInsets.zero, onChanged: (val) { - context.read().updateField(resultOk: val); + context.read().updateField( + resultOk: val, + ); }, ), ), @@ -100,7 +102,7 @@ class GeneralInfoSection extends StatelessWidget { alignLabelWithHint: true, ), onChanged: (val) { - context.read().updateField(note: val); + context.read().updateField(note: val); }, ), ], diff --git a/lib/features/operations/ui/service_form_screen/int_dialogs.dart b/lib/features/operations/ui/operation_form_screen/int_dialogs.dart similarity index 100% rename from lib/features/operations/ui/service_form_screen/int_dialogs.dart rename to lib/features/operations/ui/operation_form_screen/int_dialogs.dart diff --git a/lib/features/operations/ui/service_form_screen/service_form_screen.dart b/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart similarity index 76% rename from lib/features/operations/ui/service_form_screen/service_form_screen.dart rename to lib/features/operations/ui/operation_form_screen/operation_form_screen.dart index 400dc41..4608a9a 100644 --- a/lib/features/operations/ui/service_form_screen/service_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart @@ -1,49 +1,49 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/service_model.dart'; -import 'package:flux/features/operations/ui/service_form_screen/attachment_section.dart'; -import 'package:flux/features/operations/ui/service_form_screen/customer_section.dart'; -import 'package:flux/features/operations/ui/service_form_screen/general_info_section.dart'; -import 'package:flux/features/operations/ui/service_form_screen/services_grid.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/attachment_section.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/customer_section.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/general_info_section.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/operations_grid.dart'; -class ServiceFormScreen extends StatefulWidget { - final String? serviceId; - final ServiceModel? existingService; // <-- AGGIUNTO +class OperationFormScreen extends StatefulWidget { + final String? operationId; + final OperationModel? existingOperation; // <-- AGGIUNTO - const ServiceFormScreen({ + const OperationFormScreen({ super.key, - this.serviceId, - this.existingService, // <-- AGGIUNTO + this.operationId, + this.existingOperation, // <-- AGGIUNTO }); @override - State createState() => _ServiceFormScreenState(); + State createState() => _OperationFormScreenState(); } -class _ServiceFormScreenState extends State { +class _OperationFormScreenState extends State { @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { // Diamo in pasto al Cubit tutto quello che abbiamo! - context.read().initServiceForm( - existingService: widget.existingService, - serviceId: widget.serviceId, + context.read().initOperationForm( + existingOperation: widget.existingOperation, + operationId: widget.operationId, ); }); } void _performSave(BuildContext context, {required bool isBozza}) { FocusScope.of(context).unfocus(); - context.read().saveCurrentService(isBozza: isBozza); + context.read().saveCurrentOperation(isBozza: isBozza); } @override Widget build(BuildContext context) { - return BlocConsumer( + return BlocConsumer( listener: (context, state) { - if (state.status == ServicesStatus.saved) { + if (state.status == OperationsStatus.saved) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Pratica salvata con successo!"), @@ -52,7 +52,7 @@ class _ServiceFormScreenState extends State { ); Navigator.pop(context); } - if (state.status == ServicesStatus.failure) { + if (state.status == OperationsStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text("Errore: ${state.errorMessage ?? ''}"), @@ -60,7 +60,7 @@ class _ServiceFormScreenState extends State { ), ); } - if (state.status == ServicesStatus.savedNoPop) { + if (state.status == OperationsStatus.savedNoPop) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Pratica salvata con successo!"), @@ -70,9 +70,9 @@ class _ServiceFormScreenState extends State { } }, builder: (context, state) { - final operation = state.currentService; - final isSaving = state.status == ServicesStatus.saving; - final isEditMode = widget.serviceId != null; + final operation = state.currentOperation; + final isSaving = state.status == OperationsStatus.saving; + final isEditMode = widget.operationId != null; return Scaffold( appBar: AppBar( @@ -120,7 +120,7 @@ class _ServiceFormScreenState extends State { GeneralInfoSection(operation: operation), const SizedBox(height: 24), - ServicesGrid(operation: operation), + OperationsGrid(operation: operation), const SizedBox(height: 32), AttachmentsSection(), diff --git a/lib/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart b/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart similarity index 92% rename from lib/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart rename to lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart index bd64cf5..8adb21c 100644 --- a/lib/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart +++ b/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart @@ -3,24 +3,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:flux/features/operations/blocs/service_files_bloc.dart'; +import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; -class ServiceMobileUploadScreen extends StatefulWidget { - final String serviceId; - final String serviceName; +class OperationMobileUploadScreen extends StatefulWidget { + final String operationId; + final String operationName; - const ServiceMobileUploadScreen({ + const OperationMobileUploadScreen({ super.key, - required this.serviceId, - required this.serviceName, + required this.operationId, + required this.operationName, }); @override - State createState() => - _ServiceMobileUploadScreenState(); + State createState() => + _OperationMobileUploadScreenState(); } -class _ServiceMobileUploadScreenState extends State { +class _OperationMobileUploadScreenState + extends State { // 1. LA NOSTRA STAGING AREA (Il "Carrello") final List _stagedFiles = []; @@ -35,10 +36,10 @@ class _ServiceMobileUploadScreenState extends State { @override Widget build(BuildContext context) { - return BlocListener( + return BlocListener( listener: (context, state) { // Quando il BLoC ci dice che ha finito l'upload (Success), chiudiamo la pagina! - if (state.status == ServiceFilesStatus.success && _isUploading) { + if (state.status == OperationFilesStatus.success && _isUploading) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text("Tutti i file caricati con successo! ✅"), @@ -46,7 +47,7 @@ class _ServiceMobileUploadScreenState extends State { ); Navigator.of(context).pop(); } - if (state.status == ServiceFilesStatus.failure) { + if (state.status == OperationFilesStatus.failure) { setState(() => _isUploading = false); ScaffoldMessenger.of( context, @@ -55,7 +56,7 @@ class _ServiceMobileUploadScreenState extends State { }, child: Scaffold( appBar: AppBar( - title: Text("Upload Pratica:\n${widget.serviceName}"), + title: Text("Upload Pratica:\n${widget.operationName}"), automaticallyImplyLeading: !_isUploading, ), body: Stack( @@ -294,8 +295,8 @@ class _ServiceMobileUploadScreenState extends State { // Diciamo al BLoC di caricare tutti i file. // Usiamo il tuo evento esistente per ogni file (il BLoC li metterà in coda) - final bloc = context.read(); - bloc.add(UploadMultipleServiceFilesEvent(_stagedFiles)); + final bloc = context.read(); + bloc.add(UploadMultipleOperationFilesEvent(_stagedFiles)); // N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"! } diff --git a/lib/features/operations/ui/service_form_screen/services_grid.dart b/lib/features/operations/ui/operation_form_screen/operations_grid.dart similarity index 63% rename from lib/features/operations/ui/service_form_screen/services_grid.dart rename to lib/features/operations/ui/operation_form_screen/operations_grid.dart index bc0ab95..e5e252e 100644 --- a/lib/features/operations/ui/service_form_screen/services_grid.dart +++ b/lib/features/operations/ui/operation_form_screen/operations_grid.dart @@ -2,20 +2,20 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/energy_service_model.dart'; -import 'package:flux/features/operations/models/entertainment_service_model.dart'; -import 'package:flux/features/operations/models/fin_service_model.dart'; -import 'package:flux/features/operations/models/service_model.dart'; -import 'package:flux/features/operations/ui/service_form_screen/action_card.dart'; -import 'package:flux/features/operations/ui/service_form_screen/energy_service_dialog.dart'; -import 'package:flux/features/operations/ui/service_form_screen/entertainment_service_card.dart'; -import 'package:flux/features/operations/ui/service_form_screen/finance_service_dialog.dart'; -import 'package:flux/features/operations/ui/service_form_screen/int_dialogs.dart'; // Assicurati di importare il modello +import 'package:flux/features/operations/models/energy_operation_model.dart'; +import 'package:flux/features/operations/models/entertainment_operation_model.dart'; +import 'package:flux/features/operations/models/fin_operation_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/action_card.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/energy_operation_dialog.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/entertainment_operation_card.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/finance_operation_dialog.dart'; +import 'package:flux/features/operations/ui/operation_form_screen/int_dialogs.dart'; // Assicurati di importare il modello -class ServicesGrid extends StatelessWidget { - final ServiceModel operation; +class OperationsGrid extends StatelessWidget { + final OperationModel operation; - const ServicesGrid({super.key, required this.operation}); + const OperationsGrid({super.key, required this.operation}); @override Widget build(BuildContext context) { @@ -60,7 +60,7 @@ class ServicesGrid extends StatelessWidget { "AL", operation.al, (val) => - context.read().updateField(al: val), + context.read().updateField(al: val), ), ), ActionCard( @@ -73,7 +73,7 @@ class ServicesGrid extends StatelessWidget { "MNP", operation.mnp, (val) => - context.read().updateField(mnp: val), + context.read().updateField(mnp: val), ), ), ActionCard( @@ -86,7 +86,7 @@ class ServicesGrid extends StatelessWidget { "NIP", operation.nip, (val) => - context.read().updateField(nip: val), + context.read().updateField(nip: val), ), ), ActionCard( @@ -98,8 +98,9 @@ class ServicesGrid extends StatelessWidget { context, "Unica", operation.unica, - (val) => - context.read().updateField(unica: val), + (val) => context.read().updateField( + unica: val, + ), ), ), ActionCard( @@ -111,7 +112,7 @@ class ServicesGrid extends StatelessWidget { context, "Telepass", operation.telepass, - (val) => context.read().updateField( + (val) => context.read().updateField( telepass: val, ), ), @@ -120,23 +121,24 @@ class ServicesGrid extends StatelessWidget { // --- MODULI COMPLESSI (Le liste) --- ActionCard( label: "Energia", - count: operation.energyServices.length, + count: operation.energyOperations.length, icon: Icons.bolt, color: Colors.green, onTap: () async { // Apriamo la modale e aspettiamo il risultato - final result = await showDialog>( - context: context, - builder: (context) => EnergyServiceDialog( - currentStoreId: operation.storeId, - initialServices: operation - .energyServices, // Passiamo la lista attuale - ), - ); + final result = + await showDialog>( + context: context, + builder: (context) => EnergyOperationDialog( + currentStoreId: operation.storeId, + initialOperations: operation + .energyOperations, // Passiamo la lista attuale + ), + ); // Se l'utente ha premuto "Conferma" e non "Annulla" o tap fuori if (result != null && context.mounted) { - context.read().updateEnergyServices( + context.read().updateEnergyOperations( result, ); } @@ -144,44 +146,47 @@ class ServicesGrid extends StatelessWidget { ), ActionCard( label: "Finanziam.", - count: operation.finServices.length, + count: operation.finOperations.length, icon: Icons.euro_symbol, color: Colors.teal, onTap: () async { - final result = await showDialog>( + final result = await showDialog>( context: context, - builder: (context) => FinanceServiceDialog( + builder: (context) => FinanceOperationDialog( productCubit: context.read(), currentStoreId: operation.storeId, - initialServices: operation - .finServices, // Passiamo la lista attuale + initialOperations: operation + .finOperations, // Passiamo la lista attuale ), ); if (result != null && context.mounted) { - context.read().updateFinServices(result); + context.read().updateFinOperations( + result, + ); } }, ), ActionCard( label: "Intratten.", - count: operation.entertainmentServices.length, + count: operation.entertainmentOperations.length, icon: Icons.movie_filter_outlined, color: Colors.purple, onTap: () async { final result = - await showDialog>( + await showDialog>( context: context, - builder: (context) => EntertainmentServiceDialog( - initialServices: operation.entertainmentServices, + builder: (context) => EntertainmentOperationDialog( + initialOperations: + operation.entertainmentOperations, currentStoreId: operation.storeId, ), ); if (result != null && context.mounted) { context - .read() - .updateEntertainmentServices(result); + .read() + .updateEntertainmentOperations(result); } }, ), diff --git a/lib/features/operations/ui/services_screen.dart b/lib/features/operations/ui/operations_screen.dart similarity index 77% rename from lib/features/operations/ui/services_screen.dart rename to lib/features/operations/ui/operations_screen.dart index 4935529..a56e73e 100644 --- a/lib/features/operations/ui/services_screen.dart +++ b/lib/features/operations/ui/operations_screen.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/service_model.dart'; -import 'package:flux/features/operations/utils/service_actions.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:flux/features/operations/utils/operation_actions.dart'; import 'package:go_router/go_router.dart'; // Importa i tuoi modelli e cubit -class ServicesScreen extends StatefulWidget { - const ServicesScreen({super.key}); +class OperationsScreen extends StatefulWidget { + const OperationsScreen({super.key}); @override - State createState() => _ServicesScreenState(); + State createState() => _OperationsScreenState(); } -class _ServicesScreenState extends State { +class _OperationsScreenState extends State { final ScrollController _scrollController = ScrollController(); @override @@ -22,12 +22,12 @@ class _ServicesScreenState extends State { // Agganciamo il listener per la paginazione (Scroll Infinito) _scrollController.addListener(_onScroll); // Carichiamo i servizi iniziali - context.read().loadServices(); + context.read().loadOperations(); } void _onScroll() { if (_isBottom) { - context.read().loadServices(); + context.read().loadOperations(); } } @@ -60,16 +60,16 @@ class _ServicesScreenState extends State { ), ], ), - body: BlocBuilder( + body: BlocBuilder( builder: (context, state) { // 1. Stato di caricamento iniziale - if (state.status == ServicesStatus.loading && - state.allServices.isEmpty) { + if (state.status == OperationsStatus.loading && + state.allOperations.isEmpty) { return const Center(child: CircularProgressIndicator()); } // 2. Lista vuota - if (state.allServices.isEmpty) { + if (state.allOperations.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -77,9 +77,9 @@ class _ServicesScreenState extends State { const Text("Nessuna pratica trovata."), const SizedBox(height: 10), ElevatedButton( - onPressed: () => context.read().loadServices( - refresh: true, - ), + onPressed: () => context + .read() + .loadOperations(refresh: true), child: const Text("Riprova"), ), ], @@ -90,15 +90,15 @@ class _ServicesScreenState extends State { // 3. La Lista (con Pull-to-refresh) return RefreshIndicator( onRefresh: () => - context.read().loadServices(refresh: true), + context.read().loadOperations(refresh: true), child: ListView.builder( controller: _scrollController, padding: const EdgeInsets.only(bottom: 80), // Spazio per il FAB itemCount: state.hasReachedMax - ? state.allServices.length - : state.allServices.length + 1, + ? state.allOperations.length + : state.allOperations.length + 1, itemBuilder: (context, index) { - if (index >= state.allServices.length) { + if (index >= state.allOperations.length) { return const Center( child: Padding( padding: EdgeInsets.all(16.0), @@ -107,21 +107,21 @@ class _ServicesScreenState extends State { ); } - final operation = state.allServices[index]; - return _buildServiceCard(context, operation); + final operation = state.allOperations[index]; + return _buildOperationCard(context, operation); }, ), ); }, ), floatingActionButton: FloatingActionButton( - onPressed: () => startNewService(context), + onPressed: () => startNewOperation(context), child: const Icon(Icons.add), ), ); } - Widget _buildServiceCard(BuildContext context, ServiceModel operation) { + Widget _buildOperationCard(BuildContext context, OperationModel operation) { return Card( margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), elevation: 2, @@ -164,11 +164,11 @@ class _ServicesScreenState extends State { children: [ if (operation.al > 0 || operation.mnp > 0) _miniBadge("📞 Tel", Colors.blue), - if (operation.energyServices.isNotEmpty) + if (operation.energyOperations.isNotEmpty) _miniBadge("⚡ Energy", Colors.green), - if (operation.finServices.isNotEmpty) + if (operation.finOperations.isNotEmpty) _miniBadge("💰 Fin", Colors.purple), - if (operation.entertainmentServices.isNotEmpty) + if (operation.entertainmentOperations.isNotEmpty) _miniBadge("📺 Ent", Colors.red), ], ), @@ -180,7 +180,7 @@ class _ServicesScreenState extends State { extra: operation, // <-- LA MAGIA È QUI: Passa l'oggetto intero! // Teniamo anche il parametro URL per coerenza di routing queryParameters: operation.id != null - ? {'serviceId': operation.id!} + ? {'operationId': operation.id!} : {}, ), ), diff --git a/lib/features/operations/utils/service_actions.dart b/lib/features/operations/utils/operation_actions.dart similarity index 92% rename from lib/features/operations/utils/service_actions.dart rename to lib/features/operations/utils/operation_actions.dart index c0bdf38..35396ae 100644 --- a/lib/features/operations/utils/service_actions.dart +++ b/lib/features/operations/utils/operation_actions.dart @@ -3,11 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/service_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; import 'package:go_router/go_router.dart'; /// Avvia la creazione di un nuovo servizio partendo dalla selezione dell'operatore. -void startNewService(BuildContext context) { +void startNewOperation(BuildContext context) { final session = context.read().state; final currentStoreId = session.currentStore?.id; @@ -53,8 +53,8 @@ void startNewService(BuildContext context) { title: Text(member.name), onTap: () { // 1. Inizializza il form nel Cubit - context.read().initServiceForm( - existingService: ServiceModel( + context.read().initOperationForm( + existingOperation: OperationModel( storeId: currentStoreId, employeeId: member.id, number: '', diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index a0fe5df..2a57a54 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,13 +1,13 @@ { "@@locale": "en", "welcomeBack": "Welcome back, {name}! 👋", - "latestServices": "Latest Operations", + "latestOperations": "Latest Operations", "masterData": "Master Data", "settings": "Settings", - "newService": "Operation", + "newOperation": "Operation", "expiring_contracts": "Expiring Contracts", "sticky_notes": "Sticky Notes", "my_tasks": "My Tasks", - "latest_service_tickets": "Latest operation tickets" + "latest_operation_tickets": "Latest operation tickets" } \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 24055c9..a211adf 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -50,15 +50,15 @@ "commonNewPassword": "Nuova Password", "commonNote": "Nota", "commonSave": "Salva", - "commonService": "Servizio", + "commonOperation": "Servizio", "commonSettings": "Impostazioni", "commonStickyNotes": "Sticky Notes", "commonTask": "Attività", "homeExpiringContracts": "Contratti in scadenza", - "homeLatestServiceTickets": "Ultime assistenze", - "homeLatestServices": "Ultimi Servizi", + "homeLatestOperationTickets": "Ultime assistenze", + "homeLatestOperations": "Ultimi Servizi", "homeMyTasks": "Mie Attività", - "homeNewServiceTicket": "Nuova assistenza", + "homeNewOperationTicket": "Nuova assistenza", "homeNoStoreFound": "Nessun negozio trovato", "homeWelcomeBack": "Bentornato, {name}! 👋", "imageViewerWidgetErrorOpening": "Errore durante l'apertura dell'immagine", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index c2b13f8..1a5f96f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -220,11 +220,11 @@ abstract class AppLocalizations { /// **'Salva'** String get commonSave; - /// No description provided for @commonService. + /// No description provided for @commonOperation. /// /// In it, this message translates to: /// **'Servizio'** - String get commonService; + String get commonOperation; /// No description provided for @commonSettings. /// @@ -250,17 +250,17 @@ abstract class AppLocalizations { /// **'Contratti in scadenza'** String get homeExpiringContracts; - /// No description provided for @homeLatestServiceTickets. + /// No description provided for @homeLatestOperationTickets. /// /// In it, this message translates to: /// **'Ultime assistenze'** - String get homeLatestServiceTickets; + String get homeLatestOperationTickets; - /// No description provided for @homeLatestServices. + /// No description provided for @homeLatestOperations. /// /// In it, this message translates to: /// **'Ultimi Servizi'** - String get homeLatestServices; + String get homeLatestOperations; /// No description provided for @homeMyTasks. /// @@ -268,11 +268,11 @@ abstract class AppLocalizations { /// **'Mie Attività'** String get homeMyTasks; - /// No description provided for @homeNewServiceTicket. + /// No description provided for @homeNewOperationTicket. /// /// In it, this message translates to: /// **'Nuova assistenza'** - String get homeNewServiceTicket; + String get homeNewOperationTicket; /// No description provided for @homeNoStoreFound. /// diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index eeebff4..7630adc 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -12,10 +12,10 @@ class AppLocalizationsEn extends AppLocalizations { String get homeExpiringContracts => 'Contratti in scadenza'; @override - String get homeLatestServiceTickets => 'Ultime assistenze'; + String get homeLatestOperationTickets => 'Ultime assistenze'; @override - String get homeLatestServices => 'Ultimi Servizi'; + String get homeLatestOperations => 'Ultimi Servizi'; @override String get homeMasterData => 'Anagrafiche'; @@ -24,7 +24,7 @@ class AppLocalizationsEn extends AppLocalizations { String get homeMyTasks => 'Mie Attività'; @override - String get homeNewService => 'Servizio'; + String get homeNewOperation => 'Servizio'; @override String get homeSettings => 'Impostazioni'; @@ -41,7 +41,7 @@ class AppLocalizationsEn extends AppLocalizations { String get homeNoStoreFound => 'Nessun negozio trovato'; @override - String get homeNewServiceTicket => 'Nuova assistenza'; + String get homeNewOperationTicket => 'Nuova assistenza'; @override String get homeNewNote => 'Nota'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 5ca322c..c226972 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -81,7 +81,7 @@ class AppLocalizationsIt extends AppLocalizations { String get commonSave => 'Salva'; @override - String get commonService => 'Servizio'; + String get commonOperation => 'Servizio'; @override String get commonSettings => 'Impostazioni'; @@ -96,16 +96,16 @@ class AppLocalizationsIt extends AppLocalizations { String get homeExpiringContracts => 'Contratti in scadenza'; @override - String get homeLatestServiceTickets => 'Ultime assistenze'; + String get homeLatestOperationTickets => 'Ultime assistenze'; @override - String get homeLatestServices => 'Ultimi Servizi'; + String get homeLatestOperations => 'Ultimi Servizi'; @override String get homeMyTasks => 'Mie Attività'; @override - String get homeNewServiceTicket => 'Nuova assistenza'; + String get homeNewOperationTicket => 'Nuova assistenza'; @override String get homeNoStoreFound => 'Nessun negozio trovato'; diff --git a/lib/main.dart b/lib/main.dart index a6515df..979c1ec 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flux/features/auth/bloc/auth_cubit.dart'; -import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.dart'; +import 'package:flux/features/operations/data/operations_repository.dart'; import 'package:flux/l10n/app_localizations.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; @@ -27,7 +27,6 @@ import 'package:flux/features/master_data/staff/data/staff_repository.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/data/store_repository.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/data/services_repository.dart'; import 'package:flux/features/settings/settings.dart'; void main() async { @@ -52,7 +51,7 @@ void main() async { BlocProvider(create: (_) => CustomerCubit()), BlocProvider(create: (_) => ProductCubit()), BlocProvider(create: (_) => StaffCubit()), - BlocProvider(create: (_) => ServicesCubit()), + BlocProvider(create: (_) => OperationsCubit()), BlocProvider(create: (_) => ProvidersCubit()), ], child: const FluxApp(), @@ -85,7 +84,9 @@ Future setupLocator() async { getIt.registerLazySingleton(() => CustomerRepository()); getIt.registerLazySingleton(() => ProductRepository()); getIt.registerLazySingleton(() => StaffRepository()); - getIt.registerLazySingleton(() => ServicesRepository()); + getIt.registerLazySingleton( + () => OperationsRepository(), + ); getIt.registerLazySingleton(() => ProviderRepository()); // NOTA: CompanyRepository l'ho tolto perché la logica della Company -- 2.43.0 From ac97e47771d9ad0c79b1996b0b1f85f46d23b2a5 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Fri, 1 May 2026 11:54:39 +0200 Subject: [PATCH 08/18] jkhg Co-authored-by: Copilot --- .../attachments/models/attachment_model.dart | 111 +++++++ .../customers/blocs/customer_cubit.dart | 4 +- .../customers/models/customer_model.dart | 77 +++-- .../customers/ui/customer_detail_screen.dart | 10 +- lib/features/customers/ui/customer_form.dart | 18 +- .../customers/ui/customer_search_sheet.dart | 4 +- .../customers/ui/customers_content.dart | 6 +- .../data/operations_repository.dart | 143 +++----- .../operations/models/operation_model.dart | 306 +++++++++--------- 9 files changed, 358 insertions(+), 321 deletions(-) create mode 100644 lib/features/attachments/models/attachment_model.dart diff --git a/lib/features/attachments/models/attachment_model.dart b/lib/features/attachments/models/attachment_model.dart new file mode 100644 index 0000000..35ad3b2 --- /dev/null +++ b/lib/features/attachments/models/attachment_model.dart @@ -0,0 +1,111 @@ +import 'dart:typed_data'; + +import 'package:equatable/equatable.dart'; + +class AttachmentModel extends Equatable { + final String? id; + final DateTime? createdAt; + final String? customerId; + final String? operationId; + final String name; + final String extension; + final String storagePath; + final int fileSize; + final Uint8List? localBytes; + final String companyId; + + const AttachmentModel({ + this.id, + this.createdAt, + this.customerId, + this.operationId, + required this.name, + required this.extension, + required this.storagePath, + required this.fileSize, + this.localBytes, + required this.companyId, + }); + + @override + List get props => [ + id, + createdAt, + customerId, + operationId, + name, + extension, + storagePath, + fileSize, + localBytes, + companyId, + ]; + + bool get isLocal => localBytes != null; + + bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf'; + + String get sizeFormatted { + if (fileSize <= 0) return "0 B"; + const suffixes = ["B", "KB", "MB", "GB", "TB"]; + var i = (fileSize.toString().length - 1) ~/ 3; + if (i >= suffixes.length) i = suffixes.length - 1; + double num = fileSize / (1 << (i * 10)); + return "${num.toStringAsFixed(i == 0 ? 0 : 1)} ${suffixes[i]}"; + } + + AttachmentModel copyWith({ + String? id, + DateTime? createdAt, + String? customerId, + String? operationId, + String? name, + String? extension, + String? storagePath, + int? fileSize, + Uint8List? localBytes, + String? companyId, + }) => AttachmentModel( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + customerId: customerId ?? this.customerId, + operationId: operationId ?? this.operationId, + name: name ?? this.name, + extension: extension ?? this.extension, + storagePath: storagePath ?? this.storagePath, + fileSize: fileSize ?? this.fileSize, + localBytes: localBytes ?? this.localBytes, + companyId: companyId ?? this.companyId, + ); + + factory AttachmentModel.fromMap(Map map) { + return AttachmentModel( + id: map['id'] as String, + createdAt: map['created_at'] != null + ? DateTime.parse(map['created_at']) + : null, + customerId: map['customer_id'] as String?, + operationId: map['operation_id'] as String?, + name: map['name'] as String, + extension: map['extension'] as String, + storagePath: map['storage_path'] as String, + fileSize: map['file_size'] is int + ? map['file_size'] + : int.tryParse(map['file_size']?.toString() ?? '0') ?? 0, + companyId: map['company_id'] as String, + ); + } + + Map toMap() { + return { + if (id != null) 'id': id, + 'name': name, + 'extension': extension, + 'storage_path': storagePath, + 'customer_id': customerId, + 'operation_id': operationId, + 'file_size': fileSize, + 'company_id': companyId, + }; + } +} diff --git a/lib/features/customers/blocs/customer_cubit.dart b/lib/features/customers/blocs/customer_cubit.dart index d2d3f7c..8a2ff7f 100644 --- a/lib/features/customers/blocs/customer_cubit.dart +++ b/lib/features/customers/blocs/customer_cubit.dart @@ -135,8 +135,8 @@ class CustomerCubit extends Cubit { String? email, }) async { final newCustomer = CustomerModel( - nome: name, - telefono: phone ?? '', + name: name, + phoneNumber: phone ?? '', email: email ?? '', companyId: _sessionCubit.state.company!.id!, note: '', diff --git a/lib/features/customers/models/customer_model.dart b/lib/features/customers/models/customer_model.dart index d1fe55e..eb72c35 100644 --- a/lib/features/customers/models/customer_model.dart +++ b/lib/features/customers/models/customer_model.dart @@ -1,74 +1,74 @@ import 'package:equatable/equatable.dart'; import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/customers/models/customer_file_model.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; class CustomerModel extends Equatable { final String? id; // Bigint in SQL final DateTime? createdAt; - final String nome; - final String telefono; + final String name; + final String phoneNumber; final String email; final String note; - final DateTime? dataUltimoContatto; - final bool nonDisturbare; + final DateTime? lastContactDate; + final bool doNotDisturb; final String companyId; // UUID final bool isActive; - final List files; + final List attachments; const CustomerModel({ this.id, this.createdAt, - required this.nome, - required this.telefono, + required this.name, + required this.phoneNumber, required this.email, required this.note, - this.dataUltimoContatto, - this.nonDisturbare = false, + this.lastContactDate, + this.doNotDisturb = false, required this.companyId, this.isActive = true, - this.files = const [], + this.attachments = const [], }); @override List get props => [ id, createdAt, - nome, - telefono, + name, + phoneNumber, email, note, - dataUltimoContatto, - nonDisturbare, + lastContactDate, + doNotDisturb, companyId, isActive, - files, + attachments, ]; CustomerModel copyWith({ String? id, DateTime? createdAt, - String? nome, - String? telefono, + String? name, + String? phoneNumber, String? email, String? note, - DateTime? dataUltimoContatto, - bool? nonDisturbare, + DateTime? lastContactDate, + bool? doNotDisturb, String? companyId, bool? isActive, - List? files, + List? attachments, }) { return CustomerModel( id: id ?? this.id, createdAt: createdAt ?? this.createdAt, - nome: nome ?? this.nome, - telefono: telefono ?? this.telefono, + name: name ?? this.name, + phoneNumber: phoneNumber ?? this.phoneNumber, email: email ?? this.email, note: note ?? this.note, - dataUltimoContatto: dataUltimoContatto ?? this.dataUltimoContatto, - nonDisturbare: nonDisturbare ?? this.nonDisturbare, + lastContactDate: lastContactDate ?? this.lastContactDate, + doNotDisturb: doNotDisturb ?? this.doNotDisturb, companyId: companyId ?? this.companyId, isActive: isActive ?? this.isActive, - files: files ?? this.files, + attachments: attachments ?? this.attachments, ); } @@ -78,34 +78,29 @@ class CustomerModel extends Equatable { createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) : null, - nome: (map['nome'] as String).myFormat(), - telefono: map['telefono'], + name: (map['name'] as String).myFormat(), + phoneNumber: map['phone_number'], email: map['email'], note: map['note'] ?? '', - dataUltimoContatto: map['data_ultimo_contatto'] != null - ? DateTime.parse(map['data_ultimo_contatto']) + lastContactDate: map['last_contact_date'] != null + ? DateTime.parse(map['last_contact_date']) : null, - nonDisturbare: map['non_disturbare'] ?? false, + doNotDisturb: map['do_not_disturb'] ?? false, companyId: map['company_id'] as String, isActive: map['is_active'] ?? true, - files: - (map['customer_file'] as List?) - ?.map((x) => CustomerFileModel.fromMap(x)) - .toList() ?? - const [], ); } Map toJson() { return { if (id != null) 'id': id, - 'nome': nome.toLowerCase().trim(), - 'telefono': telefono, + 'name': name.toLowerCase().trim(), + 'phone_number': phoneNumber, 'email': email.toLowerCase().trim(), 'note': note, - if (dataUltimoContatto != null) - 'data_ultimo_contatto': dataUltimoContatto!.toIso8601String(), - 'non_disturbare': nonDisturbare, + if (lastContactDate != null) + 'last_contact_date': lastContactDate!.toIso8601String(), + 'do_not_disturb': doNotDisturb, 'company_id': companyId, 'is_active': isActive, }; diff --git a/lib/features/customers/ui/customer_detail_screen.dart b/lib/features/customers/ui/customer_detail_screen.dart index 53224b1..d06fb45 100644 --- a/lib/features/customers/ui/customer_detail_screen.dart +++ b/lib/features/customers/ui/customer_detail_screen.dart @@ -62,7 +62,7 @@ class _CustomerDetailScreenState extends State { backgroundColor: context.background, appBar: AppBar( title: Text( - widget.customer.nome, + widget.customer.name, style: const TextStyle(fontWeight: FontWeight.bold), ), backgroundColor: context.background, @@ -103,7 +103,7 @@ class _CustomerDetailScreenState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - _infoTile(Icons.phone_android, "Telefono", widget.customer.telefono), + _infoTile(Icons.phone_android, "Telefono", widget.customer.phoneNumber), _infoTile( Icons.email_outlined, "Email", @@ -117,7 +117,7 @@ class _CustomerDetailScreenState extends State { : widget.customer.note, ), const SizedBox(height: 20), - if (widget.customer.nonDisturbare) + if (widget.customer.doNotDisturb) Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( @@ -191,8 +191,8 @@ class _CustomerDetailScreenState extends State { context: context, builder: (context) => QrUploadDialog( deepLinkUrl: - 'fluxapp://customer/${widget.customer.id}/upload?name=${Uri.encodeComponent(widget.customer.nome)}', - title: 'Scatta per ${widget.customer.nome}', + 'fluxapp://customer/${widget.customer.id}/upload?name=${Uri.encodeComponent(widget.customer.name)}', + title: 'Scatta per ${widget.customer.name}', ), ); }, diff --git a/lib/features/customers/ui/customer_form.dart b/lib/features/customers/ui/customer_form.dart index 6de9695..c097b16 100644 --- a/lib/features/customers/ui/customer_form.dart +++ b/lib/features/customers/ui/customer_form.dart @@ -30,15 +30,15 @@ class _CustomerFormState extends State { void initState() { super.initState(); // Se widget.customer è null, i campi saranno vuoti - _nomeController = TextEditingController(text: widget.customer?.nome ?? ''); + _nomeController = TextEditingController(text: widget.customer?.name ?? ''); _telefonoController = TextEditingController( - text: widget.customer?.telefono ?? '', + text: widget.customer?.phoneNumber ?? '', ); _emailController = TextEditingController( text: widget.customer?.email ?? '', ); _noteController = TextEditingController(text: widget.customer?.note ?? ''); - _nonDisturbare = widget.customer?.nonDisturbare ?? false; + _nonDisturbare = widget.customer?.doNotDisturb ?? false; } @override @@ -56,19 +56,19 @@ class _CustomerFormState extends State { // o creandone uno da zero, preservando l'ID in caso di modifica. final updatedCustomer = widget.customer?.copyWith( - nome: _nomeController.text.trim(), - telefono: _telefonoController.text.trim(), + name: _nomeController.text.trim(), + phoneNumber: _telefonoController.text.trim(), email: _emailController.text.trim(), note: _noteController.text.trim(), - nonDisturbare: _nonDisturbare, + doNotDisturb: _nonDisturbare, ) ?? CustomerModel( // Caso nuovo cliente - nome: _nomeController.text.trim(), - telefono: _telefonoController.text.trim(), + name: _nomeController.text.trim(), + phoneNumber: _telefonoController.text.trim(), email: _emailController.text.trim(), note: _noteController.text.trim(), - nonDisturbare: _nonDisturbare, + doNotDisturb: _nonDisturbare, companyId: '', // Verrà iniettato dal Bloc o dal chiamante ); diff --git a/lib/features/customers/ui/customer_search_sheet.dart b/lib/features/customers/ui/customer_search_sheet.dart index e93d980..d0a6bcc 100644 --- a/lib/features/customers/ui/customer_search_sheet.dart +++ b/lib/features/customers/ui/customer_search_sheet.dart @@ -103,7 +103,7 @@ class _CustomerSearchSheetState extends State { if (nuovoCliente != null) { operationsCubit.updateField( customerId: nuovoCliente.id, - customerDisplayName: nuovoCliente.nome, + customerDisplayName: nuovoCliente.name, ); setState(() { @@ -151,7 +151,7 @@ class _CustomerSearchSheetState extends State { final customer = state.customers[index]; // Assumo che il tuo CustomerModel abbia le proprietà name e surname. // Adatta queste variabili al tuo modello reale! - final displayName = customer.nome.trim(); + final displayName = customer.name.trim(); return ListTile( contentPadding: EdgeInsets.zero, diff --git a/lib/features/customers/ui/customers_content.dart b/lib/features/customers/ui/customers_content.dart index 46c54cd..e7b6b27 100644 --- a/lib/features/customers/ui/customers_content.dart +++ b/lib/features/customers/ui/customers_content.dart @@ -166,7 +166,7 @@ class _CustomerTile extends StatelessWidget { radius: 24, backgroundColor: context.accent.withValues(alpha: 0.1), child: Text( - customer.nome.isNotEmpty ? customer.nome[0].toUpperCase() : '?', + customer.name.isNotEmpty ? customer.name[0].toUpperCase() : '?', style: TextStyle( color: context.accent, fontWeight: FontWeight.bold, @@ -174,7 +174,7 @@ class _CustomerTile extends StatelessWidget { ), ), title: Text( - customer.nome, + customer.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16), ), subtitle: Padding( @@ -184,7 +184,7 @@ class _CustomerTile extends StatelessWidget { Icon(Icons.phone_android, size: 14, color: context.secondaryText), const SizedBox(width: 4), Text( - customer.telefono, + customer.phoneNumber, style: TextStyle(color: context.secondaryText), ), if (customer.email.isNotEmpty) ...[ diff --git a/lib/features/operations/data/operations_repository.dart b/lib/features/operations/data/operations_repository.dart index 5d496d4..d4298ad 100644 --- a/lib/features/operations/data/operations_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -2,6 +2,7 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/extensions.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:flux/features/operations/models/operation_file_model.dart'; @@ -21,11 +22,8 @@ class OperationsRepository { .from('operation') .select(''' *, - customer(nome), - energy_operation(*), - fin_operation(*), - entertainment_operation(*), - operation_file(*) + customer(name), + staff_member(name) ''') .eq('id', id) .single(); @@ -45,16 +43,13 @@ class OperationsRepository { DateTimeRange? dateRange, }) async { try { - // Nota: 'customer(name, surname)' serve per il display name nella card var query = _supabase .from('operation') .select(''' *, - customer(nome), - energy_operation(*), - fin_operation(*), - entertainment_operation(*), - operation_file(*) + customer(name), + staff_member(name), + attachments(*) ''') .eq('company_id', companyId); @@ -68,7 +63,7 @@ class OperationsRepository { if (searchTerm != null && searchTerm.isNotEmpty) { // Filtra sui campi della tabella principale O su quelli della tabella joinata query = query.or( - 'number.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.nome.ilike.%$searchTerm%', + 'reference.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.name.ilike.%$searchTerm%', ); } @@ -80,7 +75,7 @@ class OperationsRepository { .map((map) => OperationModel.fromMap(map)) .toList(); } catch (e) { - throw Exception('Errore nel caricamento servizi: $e'); + throw Exception('$e'); } } @@ -112,66 +107,9 @@ class OperationsRepository { final String newId = operationData['id']; - // 2. MODIFICA: Pulizia atomica dei figli - // Se stiamo modificando (id != null), resettiamo le tabelle collegate - if (operation.id != null) { - await Future.wait([ - _supabase.from('energy_operation').delete().eq('operation_id', newId), - _supabase.from('fin_operation').delete().eq('operation_id', newId), - _supabase - .from('entertainment_operation') - .delete() - .eq('operation_id', newId), - // Aggiungi qui eventuali altre tabelle pivot o file - ]); - } - - // 3. Inserimento dei moduli in parallelo per velocità - final List insertTasks = []; - - if (operation.energyOperations.isNotEmpty) { - insertTasks.add( - _supabase - .from('energy_operation') - .insert( - operation.energyOperations - .map((item) => item.copyWith(operationId: newId).toMap()) - .toList(), - ), - ); - } - - if (operation.finOperations.isNotEmpty) { - insertTasks.add( - _supabase - .from('fin_operation') - .insert( - operation.finOperations - .map((item) => item.copyWith(operationId: newId).toMap()) - .toList(), - ), - ); - } - - if (operation.entertainmentOperations.isNotEmpty) { - insertTasks.add( - _supabase - .from('entertainment_operation') - .insert( - operation.entertainmentOperations - .map((item) => item.copyWith(operationId: newId).toMap()) - .toList(), - ), - ); - } - - if (insertTasks.isNotEmpty) { - await Future.wait(insertTasks); - } - // 4. UPLOAD DEI FILE LOCALI (Nuovi) // Filtriamo solo i file che non hanno ancora un ID (quindi sono locali) - final localFilesToUpload = operation.files + final localFilesToUpload = operation.attachments .where((f) => f.id == null) .toList(); @@ -202,7 +140,7 @@ class OperationsRepository { ); // B. Inserimento riga nel DB relazionale - await _supabase.from('operation_file').insert(fileToSave.toMap()); + await _supabase.from('attachment').insert(fileToSave.toMap()); } uploadTasks.add(uploadAndLink()); @@ -219,10 +157,9 @@ class OperationsRepository { .from('operation') .select(''' *, - energy_operation(*), - fin_operation(*), - entertainment_operation(*), - operation_file(*) + staff_member(name), + customer(name), + attachments(*) ''') .eq('id', newId) .single(); @@ -230,7 +167,7 @@ class OperationsRepository { return OperationModel.fromMap(updatedOperationData); } catch (e) { // Qui potresti aggiungere una logica di "rollback manuale" se necessario - throw Exception('Errore durante il salvataggio corazzato: $e'); + throw Exception('$e'); } } @@ -239,7 +176,7 @@ class OperationsRepository { try { await _supabase.from('operation').delete().eq('id', id); } catch (e) { - throw Exception('Errore durante l\'eliminazione: $e'); + throw Exception('$e'); } } @@ -249,16 +186,17 @@ class OperationsRepository { // Cerchiamo i tipi più frequenti associati ai servizi di questa company // Nota: dobbiamo passare attraverso la tabella 'operation' per filtrare per company_id final response = await _supabase - .from('entertainment_operation') - .select('type, operation!inner(store!inner(company_id))') - .eq('operation.store.company_id', companyId) - .limit(100); // Prendiamo un campione + .from('operation') + .select('description') + .eq('company_id', companyId) + .eq('type', 'Entertainment') + .limit(50); // Prendiamo un campione // Logica rapida per contare le occorrenze e prendere i primi 5 final Map counts = {}; for (var item in (response as List)) { - final type = item['type'] as String; - counts[type] = (counts[type] ?? 0) + 1; + final description = item['description'] as String; + counts[description] = (counts[description] ?? 0) + 1; } var sortedKeys = counts.keys.toList() @@ -276,19 +214,19 @@ class OperationsRepository { } /// Ascolta in tempo reale i file caricati per una pratica - Stream> getOperationFilesStream(String operationId) { + Stream> getOperationFilesStream(String operationId) { return _supabase - .from('operation_file') + .from('attachment') .stream(primaryKey: ['id']) .eq('operation_id', operationId) .order('created_at', ascending: false) .map( (listOfMaps) => - listOfMaps.map((map) => OperationFileModel.fromMap(map)).toList(), + listOfMaps.map((map) => AttachmentModel.fromMap(map)).toList(), ); } - Future uploadAndRegisterOperationFile({ + Future uploadAndRegisterOperationFile({ required String operationId, required PlatformFile pickedFile, }) async { @@ -299,7 +237,8 @@ class OperationsRepository { final storagePath = '$companyId/operations/$operationId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; final int fileSize = pickedFile.size; - final fileToSave = OperationFileModel( + final fileToSave = AttachmentModel( + companyId: GetIt.I.get().state.company!.id!, operationId: operationId, name: cleanFileName.fileNameWithoutExtension(), extension: cleanFileName.fileExtension(), @@ -327,12 +266,12 @@ class OperationsRepository { } final response = await _supabase - .from('operation_file') + .from('attachment') .insert(fileToSave.toMap()) .select() .single(); - return OperationFileModel.fromMap(response); + return AttachmentModel.fromMap(response); } catch (e) { throw 'Errore durante l\'upload: $e'; } @@ -342,17 +281,13 @@ class OperationsRepository { required OperationFileModel file, required String customerId, }) async { - CustomerFileModel fileToCopy = CustomerFileModel( - customerId: customerId, - name: file.name, - storagePath: file.storagePath, - extension: file.extension, - fileSize: file.fileSize, - ); - await _customerRepository.saveFileReference(fileToCopy); + await _supabase + .from('attachment') + .update({'customer_id': customerId}) + .eq('id', file.id!); } - Future deleteOperationFiles(List files) async { + Future deleteOperationFiles(List files) async { if (files.isEmpty) return; // 1. Prepariamo le liste di ID e di Percorsi final List idsToDelete = files.map((f) => f.id!).toList(); @@ -360,18 +295,14 @@ class OperationsRepository { try { await _supabase - .from('operation_file') - .delete() + .from('attachment') + .update({'operation_id': null}) .inFilter('id', idsToDelete); await _supabase.storage.from('documents').remove(storagePaths); - - debugPrint("Eliminati con successo ${files.length} file."); } on PostgrestException catch (e) { - debugPrint("Errore DB: ${e.message}"); throw 'Errore database: ${e.message}'; } catch (e) { - debugPrint("Errore generico: $e"); throw 'Errore durante l\'eliminazione dei file: $e'; } } diff --git a/lib/features/operations/models/operation_model.dart b/lib/features/operations/models/operation_model.dart index b938d07..6f304a1 100644 --- a/lib/features/operations/models/operation_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -1,200 +1,200 @@ import 'package:equatable/equatable.dart'; -import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/operations/models/energy_operation_model.dart'; -import 'package:flux/features/operations/models/entertainment_operation_model.dart'; -import 'package:flux/features/operations/models/fin_operation_model.dart'; -import 'package:flux/features/operations/models/operation_file_model.dart'; // <-- Aggiunto Import +import 'package:flux/features/attachments/models/attachment_model.dart'; + +enum OperationStatus { + ok('ok'), + waitingforaction('waiting_for_action'), + waitingforsupport('waiting_for_support'), + waitingfordeployment('waiting_for_deployment'), + ko('ko'), + draft('draft'), + canceled('canceled'); + + static OperationStatus fromString(String value) { + final normalizedValue = value.replaceAll('_', '').toLowerCase(); + return OperationStatus.values.firstWhere( + (e) => e.name.toLowerCase() == normalizedValue, + ); + } + + final String supabaseName; + + const OperationStatus(this.supabaseName); +} class OperationModel extends Equatable { final String? id; final DateTime? createdAt; - final String storeId; - final String? employeeId; - final String? customerId; - final String number; - final bool isBozza; + final String type; + final String? providerId; + final String? modelId; + final String? description; + final DateTime? expirationDate; final String note; - final bool resultOk; - final String? customerDisplayName; + final bool showInDashboard; + final String batchUuid; final String companyId; - - // Telefonia - final int al; - final int mnp; - final int nip; - final int unica; - final int telepass; - - // Moduli (Liste) - final List energyOperations; - final List finOperations; - final List entertainmentOperations; + final String storeId; + final int quantity; + final String? staffId; + final String staffDisplayName; + final String? lastCampaignId; + final OperationStatus status; + final String? customerId; + final String customerDisplayName; + final String reference; // ALLEGATI (Aggiunto) - final List files; + final List attachments; const OperationModel({ this.id, this.createdAt, - required this.storeId, - this.employeeId, - this.customerId, - required this.number, - this.isBozza = true, + this.type = '', + this.providerId, + this.modelId, + this.description, + this.expirationDate, this.note = '', - this.resultOk = true, - this.al = 0, - this.mnp = 0, - this.nip = 0, - this.unica = 0, - this.telepass = 0, - this.energyOperations = const [], - this.finOperations = const [], - this.entertainmentOperations = const [], - this.files = const [], // <-- Aggiunto default vuoto - this.customerDisplayName, + this.showInDashboard = true, + this.batchUuid = '', required this.companyId, + this.storeId = '', + this.quantity = 1, + this.staffId, + this.staffDisplayName = '', + this.lastCampaignId, + this.status = OperationStatus.draft, + this.customerId, + this.customerDisplayName = '', + this.reference = '', + this.attachments = const [], }); OperationModel copyWith({ String? id, DateTime? createdAt, - String? storeId, - String? employeeId, - String? customerId, - String? number, - bool? isBozza, + String? type, + String? providerId, + String? modelId, + String? description, + DateTime? expirationDate, String? note, - bool? resultOk, - int? al, - int? mnp, - int? nip, - int? unica, - int? telepass, - List? energyOperations, - List? finOperations, - List? entertainmentOperations, - List? files, // <-- Aggiunto - String? customerDisplayName, + bool? showInDashboard, + String? batchUuid, String? companyId, - }) { - return OperationModel( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - storeId: storeId ?? this.storeId, - employeeId: employeeId ?? this.employeeId, - customerId: customerId ?? this.customerId, - number: number ?? this.number, - isBozza: isBozza ?? this.isBozza, - note: note ?? this.note, - resultOk: resultOk ?? this.resultOk, - al: al ?? this.al, - mnp: mnp ?? this.mnp, - nip: nip ?? this.nip, - unica: unica ?? this.unica, - telepass: telepass ?? this.telepass, - energyOperations: energyOperations ?? this.energyOperations, - finOperations: finOperations ?? this.finOperations, - entertainmentOperations: - entertainmentOperations ?? this.entertainmentOperations, - files: files ?? this.files, // <-- Aggiunto - customerDisplayName: customerDisplayName ?? this.customerDisplayName, - companyId: companyId ?? this.companyId, - ); - } + String? storeId, + int? quantity, + String? staffId, + String? staffDisplayName, + String? lastCampaignId, + OperationStatus? status, + String? customerId, + String? customerDisplayName, + String? reference, + List? attachments, + }) => OperationModel( + id: id ?? this.id, + createdAt: createdAt ?? this.createdAt, + type: type ?? this.type, + providerId: providerId ?? this.providerId, + modelId: modelId ?? this.modelId, + description: description ?? this.description, + expirationDate: expirationDate ?? this.expirationDate, + note: note ?? this.note, + showInDashboard: showInDashboard ?? this.showInDashboard, + batchUuid: batchUuid ?? this.batchUuid, + companyId: companyId ?? this.companyId, + storeId: storeId ?? this.storeId, + quantity: quantity ?? this.quantity, + staffId: staffId ?? this.staffId, + staffDisplayName: staffDisplayName ?? this.staffDisplayName, + lastCampaignId: lastCampaignId ?? this.lastCampaignId, + status: status ?? this.status, + customerId: customerId ?? this.customerId, + customerDisplayName: customerDisplayName ?? this.customerDisplayName, + reference: reference ?? this.reference, + attachments: attachments ?? this.attachments, + ); @override List get props => [ id, createdAt, - storeId, - employeeId, - customerId, - number, - isBozza, + type, + providerId, + modelId, + description, + expirationDate, note, - resultOk, - al, - mnp, - nip, - unica, - telepass, - energyOperations, - finOperations, - entertainmentOperations, - files, // <-- Aggiunto - customerDisplayName, + showInDashboard, + batchUuid, companyId, + storeId, + quantity, + staffId, + staffDisplayName, + lastCampaignId, + status, + customerId, + customerDisplayName, + reference, + attachments, ]; + factory OperationModel.empty({required String companyId}) { + return OperationModel(id: null, createdAt: null, companyId: companyId); + } + factory OperationModel.fromMap(Map map) { return OperationModel( - id: map['id'].toString(), + id: map['id'], createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) - : DateTime.now(), - storeId: map['store_id'] ?? '', - employeeId: map['employee_id']?.toString(), - customerId: map['customer_id']?.toString(), - number: map['number']?.toString() ?? '', - isBozza: map['bozza'] ?? true, - note: map['note'] ?? '', - resultOk: map['result_ok'] ?? true, - al: map['al'] ?? 0, - mnp: map['mnp'] ?? 0, - nip: map['nip'] ?? 0, - unica: map['unica'] ?? 0, - telepass: map['telepass'] ?? 0, - - // Estrazione sicura liste collegate - energyOperations: - (map['energy_operation'] as List?) - ?.map((x) => EnergyOperationModel.fromMap(x)) - .toList() ?? - const [], - finOperations: - (map['fin_operation'] as List?) - ?.map((x) => FinOperationModel.fromMap(x)) - .toList() ?? - const [], - entertainmentOperations: - (map['entertainment_operation'] as List?) - ?.map((x) => EntertainmentOperationModel.fromMap(x)) - .toList() ?? - const [], - - // I FILE! (Assicurati che la foreign key su Supabase usi esattamente questo nome) - files: - (map['operation_file'] as List?) - ?.map((x) => OperationFileModel.fromMap(x)) - .toList() ?? - const [], - - // Display name del cliente con fallback - customerDisplayName: map['customer'] != null - ? "${map['customer']['nome'] ?? ''}".myFormat() - : "Cliente non assegnato", + : null, + type: map['type'] as String? ?? '', + providerId: map['provider_id'] as String? ?? '', + modelId: map['model_id'] as String? ?? '', + description: map['description'] as String? ?? '', + expirationDate: map['expiration_date'] != null + ? DateTime.parse(map['expiration_date']) + : null, + note: map['note'] as String? ?? '', + showInDashboard: map['show_in_dashboard'] as bool, + batchUuid: map['batch_uuid'] as String, companyId: map['company_id'] as String, + storeId: map['store_id'] as String? ?? '', + quantity: map['quantity'] is int + ? map['quantity'] + : int.tryParse(map['quantity']?.toString() ?? '0') ?? 0, + staffId: map['staff_id'] as String? ?? '', + lastCampaignId: map['last_campaign_id'] as String? ?? '', + status: OperationStatus.fromString(map['status']), + customerId: map['customer_id'] as String? ?? '', + reference: map['reference'] as String? ?? '', ); } Map toMap() { return { if (id != null) 'id': id, - 'store_id': storeId, - 'employee_id': employeeId, - 'customer_id': customerId, - 'number': number, - 'bozza': isBozza, + 'type': type, + 'provider_id': providerId, + 'model_id': modelId, + 'description': description, + if (expirationDate != null) + 'expiration_date': expirationDate!.toIso8601String(), 'note': note, - 'result_ok': resultOk, - 'al': al, - 'mnp': mnp, - 'nip': nip, - 'unica': unica, - 'telepass': telepass, + 'show_in_dashboard': showInDashboard, + 'batch_uuid': batchUuid, 'company_id': companyId, - // Le liste non le mettiamo qui perché vanno in tabelle diverse! + 'store_id': storeId, + 'quantity': quantity, + if (staffId != null) 'staff_id': staffId, + if (lastCampaignId != null) 'last_campaign_id': lastCampaignId, + 'status': status.supabaseName, + if (customerId != null) 'customer_id': customerId, + 'reference': reference, }; } } -- 2.43.0 From 1721b2ff89f40d8dbae7ddea543ce7fbc0750f9e Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sat, 2 May 2026 10:22:47 +0200 Subject: [PATCH 09/18] aaaaaaaaaaaa Co-authored-by: Copilot --- .../customers/blocs/customer_cubit.dart | 1 - .../customers/blocs/customer_files_bloc.dart | 6 +- .../blocs/customer_files_events.dart | 2 +- .../customers/blocs/customer_files_state.dart | 8 +- .../customers/blocs/customer_state.dart | 5 - .../customers/data/customer_repository.dart | 79 +++--- .../customers/models/customer_file_model.dart | 91 ------- .../customers/models/customer_model.dart | 5 + .../customers/ui/customer_detail_screen.dart | 8 +- .../customers/ui/customers_content.dart | 4 +- .../ui/latest_store_operations_card.dart | 2 +- .../blocs/operation_files_bloc.dart | 246 ++++++++++++------ .../blocs/operation_files_events.dart | 16 +- .../blocs/operation_files_state.dart | 14 +- .../operations/blocs/operations_cubit.dart | 232 ++++------------- .../data/operations_repository.dart | 91 +++---- .../models/operation_file_model.dart | 100 ------- .../operations/models/operation_model.dart | 35 ++- .../attachment_section.dart | 68 +++-- .../general_info_section.dart | 50 +--- .../operation_form_screen.dart | 39 ++- .../operation_mobile_upload_screen.dart | 2 +- .../operations_grid.dart | 201 -------------- .../operations/ui/operations_screen.dart | 68 +++-- .../operations/utils/operation_actions.dart | 82 ------ lib/l10n/app_en.arb | 13 - lib/l10n/app_it.arb | 3 +- lib/l10n/app_localizations.dart | 6 + lib/l10n/app_localizations_it.dart | 4 + lib/l10n/intl_en.arb | 1 - pubspec.lock | 2 +- pubspec.yaml | 1 + 32 files changed, 454 insertions(+), 1031 deletions(-) delete mode 100644 lib/features/customers/models/customer_file_model.dart delete mode 100644 lib/features/operations/models/operation_file_model.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/operations_grid.dart delete mode 100644 lib/features/operations/utils/operation_actions.dart delete mode 100644 lib/l10n/app_en.arb delete mode 100644 lib/l10n/intl_en.arb diff --git a/lib/features/customers/blocs/customer_cubit.dart b/lib/features/customers/blocs/customer_cubit.dart index 8a2ff7f..fba9b4d 100644 --- a/lib/features/customers/blocs/customer_cubit.dart +++ b/lib/features/customers/blocs/customer_cubit.dart @@ -3,7 +3,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; -import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:get_it/get_it.dart'; diff --git a/lib/features/customers/blocs/customer_files_bloc.dart b/lib/features/customers/blocs/customer_files_bloc.dart index 0fdffe1..c8662e0 100644 --- a/lib/features/customers/blocs/customer_files_bloc.dart +++ b/lib/features/customers/blocs/customer_files_bloc.dart @@ -4,8 +4,8 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:flux/features/customers/data/customer_repository.dart'; -import 'package:flux/features/customers/models/customer_file_model.dart'; import 'package:get_it/get_it.dart'; part 'customer_files_events.dart'; @@ -26,7 +26,7 @@ class CustomerFilesBloc extends Bloc { LoadCustomerFilesEvent event, Emitter emit, ) async { - await emit.forEach>( + await emit.forEach>( _repository.getCustomerFilesStream(customerId), onData: (customerFiles) => CustomerFilesState( status: CustomerFilesStatus.success, @@ -128,7 +128,7 @@ class CustomerFilesBloc extends Bloc { ToggleCustomerFileSelectionEvent event, Emitter emit, ) { - List selectedFiles = List.from(state.selectedFiles); + List selectedFiles = List.from(state.selectedFiles); if (selectedFiles.contains(event.file)) { selectedFiles.remove(event.file); } else { diff --git a/lib/features/customers/blocs/customer_files_events.dart b/lib/features/customers/blocs/customer_files_events.dart index ad235d1..b893ce8 100644 --- a/lib/features/customers/blocs/customer_files_events.dart +++ b/lib/features/customers/blocs/customer_files_events.dart @@ -25,6 +25,6 @@ class UploadMultipleCustomerFilesEvent extends CustomerFilesEvent { class DeleteCustomerFilesEvent extends CustomerFilesEvent {} class ToggleCustomerFileSelectionEvent extends CustomerFilesEvent { - final CustomerFileModel file; + final AttachmentModel file; const ToggleCustomerFileSelectionEvent(this.file); } diff --git a/lib/features/customers/blocs/customer_files_state.dart b/lib/features/customers/blocs/customer_files_state.dart index 88fbe5b..bdb525d 100644 --- a/lib/features/customers/blocs/customer_files_state.dart +++ b/lib/features/customers/blocs/customer_files_state.dart @@ -12,8 +12,8 @@ class CustomerFilesState extends Equatable { final CustomerFilesStatus status; final String? error; - final List customerFiles; - final List selectedFiles; + final List customerFiles; + final List selectedFiles; @override List get props => [status, error, customerFiles, selectedFiles]; @@ -21,8 +21,8 @@ class CustomerFilesState extends Equatable { CustomerFilesState copyWith({ CustomerFilesStatus? status, String? error, - List? customerFiles, - List? selectedFiles, + List? customerFiles, + List? selectedFiles, }) { return CustomerFilesState( status: status ?? this.status, diff --git a/lib/features/customers/blocs/customer_state.dart b/lib/features/customers/blocs/customer_state.dart index 9e26c69..da5ffec 100644 --- a/lib/features/customers/blocs/customer_state.dart +++ b/lib/features/customers/blocs/customer_state.dart @@ -14,14 +14,12 @@ class CustomerState extends Equatable { final List customers; final CustomerModel? lastCreatedCustomer; final String? errorMessage; - final List customerFiles; const CustomerState({ this.status = CustomerStatus.initial, this.customers = const [], this.lastCreatedCustomer, this.errorMessage, - this.customerFiles = const [], }); CustomerState copyWith({ @@ -29,14 +27,12 @@ class CustomerState extends Equatable { List? customers, CustomerModel? lastCreatedCustomer, String? errorMessage, - List? customerFiles, }) { return CustomerState( status: status ?? this.status, customers: customers ?? this.customers, lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer, errorMessage: errorMessage ?? this.errorMessage, - customerFiles: customerFiles ?? this.customerFiles, ); } @@ -46,6 +42,5 @@ class CustomerState extends Equatable { customers, lastCreatedCustomer, errorMessage, - customerFiles, ]; } diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index 0831685..086db52 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -1,8 +1,7 @@ import 'package:file_picker/file_picker.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/customers/models/customer_file_model.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/customer_model.dart'; @@ -46,11 +45,11 @@ class CustomerRepository { .from('customer') .select(''' *, - customer_file(*) + attachment(*) ''') .eq('company_id', companyId) .eq('is_active', true) - .order('nome'); + .order('name'); return (response as List).map((c) => CustomerModel.fromMap(c)).toList(); } catch (e) { @@ -78,36 +77,34 @@ class CustomerRepository { } /// Ascolta in tempo reale i file caricati per un cliente - Stream> getCustomerFilesStream(String customerId) { + Stream> getCustomerFilesStream(String customerId) { return _supabase - .from('customer_file') + .from('attachment') .stream(primaryKey: ['id']) .eq('customer_id', customerId) .order('created_at', ascending: false) .map( (listOfMaps) => - listOfMaps.map((map) => CustomerFileModel.fromMap(map)).toList(), + listOfMaps.map((map) => AttachmentModel.fromMap(map)).toList(), ); } /// Recupera i file di un cliente specifico - Future> getCustomerFiles(String customerId) async { + Future> getCustomerFiles(String customerId) async { try { final response = await _supabase - .from('customer_file') + .from('attachment') .select() .eq('customer_id', customerId); - return (response as List) - .map((f) => CustomerFileModel.fromMap(f)) - .toList(); + return (response as List).map((f) => AttachmentModel.fromMap(f)).toList(); } catch (e) { throw '$e'; } } /// Carica un file e salva il riferimento nel database - Future uploadAndRegisterFile({ + Future uploadAndRegisterFile({ required String customerId, required PlatformFile pickedFile, }) async { @@ -118,7 +115,8 @@ class CustomerRepository { final storagePath = '$companyId/customers/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; final int fileSize = pickedFile.size; - final fileToSave = CustomerFileModel( + final fileToSave = AttachmentModel( + companyId: companyId, customerId: customerId, name: cleanFileName.fileNameWithoutExtension(), extension: cleanFileName.fileExtension(), @@ -146,46 +144,47 @@ class CustomerRepository { } final response = await _supabase - .from('customer_file') + .from('attachment') .insert(fileToSave.toMap()) .select() .single(); - return CustomerFileModel.fromMap(response); + return AttachmentModel.fromMap(response); } catch (e) { throw '$e'; } } - Future saveFileReference(CustomerFileModel file) async { - await _supabase.from('customer_file').upsert(file.toMap()); + Future saveFileReference(AttachmentModel file) async { + await _supabase.from('attachment').upsert(file.toMap()); } - /// Aggiorna la lista degli URL nel database - Future updateCustomerDocuments(int id, List urls) async { - await _supabase - .from('customer') - .update({'document_urls': urls}) - .eq('id', id); - } - - Future deleteDocuments(List files) async { + Future deleteDocuments(List files) async { if (files.isEmpty) return; - // 1. Prepariamo le liste di ID e di Percorsi - final List idsToDelete = files.map((f) => f.id!).toList(); - final List storagePaths = files.map((f) => f.storagePath).toList(); - + final List idsToDelete = []; + final List storagePathsToDelete = []; + final List idsToEdit = []; + for (var file in files) { + if (file.operationId == null) { + idsToDelete.add(file.id!); + storagePathsToDelete.add(file.storagePath); + } else { + idsToEdit.add(file.id!); + } + } try { - // 2. Cancellazione MASSIVA dal DB (una sola chiamata invece di un ciclo!) - // .in_ dice: "cancella tutti i record il cui ID è contenuto in questa lista" - await _supabase - .from('customer_file') - .delete() - .inFilter('id', idsToDelete); - - // 3. Cancellazione MASSIVA dallo Storage - await _supabase.storage.from('documents').remove(storagePaths); + if (idsToDelete.isNotEmpty) { + await _supabase.from('attachment').delete().inFilter('id', idsToDelete); + // 3. Cancellazione MASSIVA dallo Storage + await _supabase.storage.from('documents').remove(storagePathsToDelete); + } + if (idsToEdit.isNotEmpty) { + await _supabase + .from('attachment') + .update({'customer_id': null}) + .inFilter('id', idsToEdit); + } } on PostgrestException catch (e) { throw e.message; } catch (e) { diff --git a/lib/features/customers/models/customer_file_model.dart b/lib/features/customers/models/customer_file_model.dart deleted file mode 100644 index 00d1e92..0000000 --- a/lib/features/customers/models/customer_file_model.dart +++ /dev/null @@ -1,91 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class CustomerFileModel extends Equatable { - final String? id; - final String customerId; // Riferimento UUID - final String name; - final String storagePath; - final String extension; - final DateTime? createdAt; - final int fileSize; - - const CustomerFileModel({ - this.id, - required this.customerId, - required this.name, - required this.storagePath, - required this.extension, - this.createdAt, - required this.fileSize, - }); - - // Trasforma i byte in qualcosa di leggibile (KB, MB, GB) - String get sizeFormatted { - if (fileSize <= 0) return "0 B"; - const suffixes = ["B", "KB", "MB", "GB", "TB"]; - var i = (fileSize.toString().length - 1) ~/ 3; - if (i >= suffixes.length) i = suffixes.length - 1; - double num = fileSize / (1 << (i * 10)); - return "${num.toStringAsFixed(i == 0 ? 0 : 1)} ${suffixes[i]}"; - } - - bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf'; - - CustomerFileModel copyWith({ - String? id, - String? customerId, - String? name, - String? storagePath, - String? extension, - DateTime? createdAt, - int? fileSize, - }) { - return CustomerFileModel( - id: id ?? this.id, - customerId: customerId ?? this.customerId, - name: name ?? this.name, - storagePath: storagePath ?? this.storagePath, - extension: extension ?? this.extension, - createdAt: createdAt ?? this.createdAt, - fileSize: fileSize ?? this.fileSize, - ); - } - - factory CustomerFileModel.fromMap(Map map) { - return CustomerFileModel( - id: map['id'] as String, - customerId: map['customer_id'], - name: map['name'], - storagePath: map['storage_path'], - extension: map['extension'] ?? '', - createdAt: map['created_at'] != null - ? DateTime.parse(map['created_at']) - : null, - fileSize: map['file_size'] is int - ? map['file_size'] - : int.tryParse(map['file_size']?.toString() ?? '0') ?? 0, - ); - } - - Map toMap() { - return { - if (id != null) 'id': id, - 'customer_id': customerId, - 'name': name, - 'storage_path': storagePath, - 'extension': extension, - 'file_size': fileSize, - }; - } - - @override - List get props => [ - id, - customerId, - name, - storagePath, - extension, - createdAt, - fileSize, - ]; -} diff --git a/lib/features/customers/models/customer_model.dart b/lib/features/customers/models/customer_model.dart index eb72c35..db92020 100644 --- a/lib/features/customers/models/customer_model.dart +++ b/lib/features/customers/models/customer_model.dart @@ -88,6 +88,11 @@ class CustomerModel extends Equatable { doNotDisturb: map['do_not_disturb'] ?? false, companyId: map['company_id'] as String, isActive: map['is_active'] ?? true, + attachments: + (map['attachment'] as List?) + ?.map((x) => AttachmentModel.fromMap(x)) + .toList() ?? + const [], ); } diff --git a/lib/features/customers/ui/customer_detail_screen.dart b/lib/features/customers/ui/customer_detail_screen.dart index d06fb45..b6e64da 100644 --- a/lib/features/customers/ui/customer_detail_screen.dart +++ b/lib/features/customers/ui/customer_detail_screen.dart @@ -6,9 +6,9 @@ import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/widgets/image_viewer_widget.dart'; import 'package:flux/core/widgets/pdf_viewer_widget.dart'; import 'package:flux/core/widgets/qr_upload_dialog.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:flux/features/customers/blocs/customer_files_bloc.dart'; import 'package:flux/features/customers/models/customer_model.dart'; -import 'package:flux/features/customers/models/customer_file_model.dart'; class CustomerDetailScreen extends StatefulWidget { final CustomerModel customer; @@ -262,12 +262,12 @@ class _CustomerDetailScreenState extends State { void _showDeleteConfirmationDialog({ required BuildContext context, - required List files, + required List files, }) {} } class _FileCard extends StatelessWidget { - final CustomerFileModel file; + final AttachmentModel file; final CustomerFilesState state; const _FileCard({required this.file, required this.state}); @@ -334,7 +334,7 @@ class _FileCard extends StatelessWidget { } } - void _handleDoubleClickOnFile(BuildContext context, CustomerFileModel file) { + void _handleDoubleClickOnFile(BuildContext context, AttachmentModel file) { showDialog( context: context, barrierDismissible: true, diff --git a/lib/features/customers/ui/customers_content.dart b/lib/features/customers/ui/customers_content.dart index e7b6b27..7021922 100644 --- a/lib/features/customers/ui/customers_content.dart +++ b/lib/features/customers/ui/customers_content.dart @@ -196,11 +196,11 @@ class _CustomerTile extends StatelessWidget { style: TextStyle(color: context.secondaryText), ), ], - if (customer.files.isNotEmpty) ...[ + if (customer.attachments.isNotEmpty) ...[ Text(' - ', style: TextStyle(color: context.secondaryText)), Icon(Icons.attach_file, size: 14, color: context.accent), Text( - '${customer.files.length} doc', + '${customer.attachments.length} doc', style: TextStyle( color: context.accent, fontWeight: FontWeight.bold, diff --git a/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart b/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart index 10123e0..1380b0a 100644 --- a/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart +++ b/lib/features/home/latest_store_operations/ui/latest_store_operations_card.dart @@ -156,7 +156,7 @@ class _LatestOperationsCardContent extends StatelessWidget { Expanded( flex: 5, child: Text( - operation.number, + operation.reference, style: TextStyle( fontWeight: FontWeight.w600, color: context.primaryText, diff --git a/lib/features/operations/blocs/operation_files_bloc.dart b/lib/features/operations/blocs/operation_files_bloc.dart index 6407d5a..34aca84 100644 --- a/lib/features/operations/blocs/operation_files_bloc.dart +++ b/lib/features/operations/blocs/operation_files_bloc.dart @@ -1,14 +1,13 @@ import 'dart:async'; -import 'dart:io'; - import 'package:file_picker/file_picker.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:equatable/equatable.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/extensions.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; -import 'package:flux/features/operations/models/operation_file_model.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; import 'package:get_it/get_it.dart'; +import 'package:image_picker/image_picker.dart'; part 'operation_files_events.dart'; part 'operation_files_state.dart'; @@ -29,9 +28,10 @@ class OperationFilesBloc on(_onLoadOperationFiles); on(_onAddOperationFiles); on(_onUploadOperationFiles); - on(_onUploadMultipleOperationFiles); on(_onDeleteOperationFiles); on(_onToggleOperationFileSelection); + on(_onLinkFilesToCustomer); + // Se il BLoC nasce con un ID, accendiamo subito lo stream! if (operationId != null) { add(LoadOperationFilesEvent(operationId: operationId)); @@ -41,18 +41,53 @@ class OperationFilesBloc FutureOr _onOperationsaved( OperationsavedEvent event, Emitter emit, - ) { - // 1. Aggiorniamo l'ID nello stato - // 2. PIALLIAMO i file locali: ormai sono partiti per Supabase! - // Così la UI si pulisce all'istante e aspetta quelli remoti. + ) async { + // 1. Aggiorniamo l'ID e mettiamo in loading emit( state.copyWith( operationId: event.operationId, - localFiles: [], // <-- LA MAGIA ANTI-DUPLICATI + status: OperationFilesStatus.uploading, ), ); - // Lanciamo il caricamento + // 2. RECUPERO E UPLOAD DEI FILE "PARCHEGGIATI" (Pratica Nuova) + if (state.localFiles.isNotEmpty) { + try { + final List> uploadTasks = []; + + for (var file in state.localFiles) { + // Ricreiamo il PlatformFile dal nostro AttachmentModel + // così il repository lo accetta senza fare storie! + final fakePlatformFile = PlatformFile( + name: '${file.name}.${file.extension}', + size: file.fileSize, + bytes: file.localBytes, + ); + + uploadTasks.add( + _repository.uploadAndRegisterOperationFile( + operationId: event.operationId, // L'ID APPENA NATO! + pickedFile: fakePlatformFile, + ), + ); + } + + // Lanciamo tutti gli upload in parallelo + await Future.wait(uploadTasks); + } catch (e) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: "Errore upload post-salvataggio: $e", + ), + ); + return; // Ci fermiamo qui se esplode qualcosa + } + } + + // 3. FINE DEI GIOCHI! Svuotiamo i locali, passiamo a success e accendiamo lo Stream + emit(state.copyWith(localFiles: [], status: OperationFilesStatus.success)); + add(LoadOperationFilesEvent(operationId: event.operationId)); } @@ -60,17 +95,14 @@ class OperationFilesBloc LoadOperationFilesEvent event, Emitter emit, ) async { - // Usiamo l'ID dell'evento, e se non c'è usiamo quello dello stato final currentId = event.operationId ?? state.operationId; if (currentId != null) { emit(state.copyWith(status: OperationFilesStatus.loading)); await emit.forEach( - _repository.getOperationFilesStream( - currentId, - ), // <-- Usiamo l'ID corretto! - onData: (data) => state.copyWith( + _repository.getOperationFilesStream(currentId), + onData: (List data) => state.copyWith( status: OperationFilesStatus.success, remoteFiles: data, ), @@ -87,13 +119,15 @@ class OperationFilesBloc Emitter emit, ) async { final currentId = state.operationId; - // BIVIO 1: PRATICA NUOVA (Nessun ID) + + // BIVIO 1: PRATICA NUOVA (Nessun ID - salvataggio locale) if (currentId == null) { - // Mettiamo i file nel "parcheggio" locale dello State + final companyId = GetIt.I.get().state.company!.id!; final newLocalFiles = event.files.map((file) { - return OperationFileModel( + return AttachmentModel( id: null, - operationId: operationId ?? '', + companyId: companyId, + operationId: '', // Sarà riempito al salvataggio name: file.name.fileNameWithoutExtension(), extension: file.name.fileExtension(), storagePath: '', @@ -101,29 +135,29 @@ class OperationFilesBloc localBytes: file.bytes, ); }).toList(); - final List updatedLocalFiles = [ - ...state.localFiles, - ...newLocalFiles, - ]; + emit( state.copyWith( - localFiles: updatedLocalFiles, + localFiles: [...state.localFiles, ...newLocalFiles], status: OperationFilesStatus.success, ), ); return; } - // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID) + // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID - Upload immediato) emit(state.copyWith(status: OperationFilesStatus.uploading)); try { - // Logica identica a quella che abbiamo fatto per i clienti + final List> uploadTasks = []; for (var file in event.files) { - await _repository.uploadAndRegisterOperationFile( - operationId: operationId!, - pickedFile: file, + uploadTasks.add( + _repository.uploadAndRegisterOperationFile( + operationId: currentId, + pickedFile: file, + ), ); } + await Future.wait(uploadTasks); emit(state.copyWith(status: OperationFilesStatus.success)); } catch (e) { emit( @@ -139,21 +173,55 @@ class OperationFilesBloc UploadOperationFilesEvent event, Emitter emit, ) async { - if (event.pickedFiles == null && event.photos == null) return; - if (event.pickedFiles!.isEmpty && event.photos!.isEmpty) return; + if ((event.pickedFiles == null || event.pickedFiles!.isEmpty) && + (event.photos == null || event.photos!.isEmpty)) { + return; + } + + if (state.operationId == null) return; - // BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID emit(state.copyWith(status: OperationFilesStatus.uploading)); try { - // Logica identica a quella che abbiamo fatto per i clienti - if (event.pickedFiles != null && event.pickedFiles!.isNotEmpty) { + final List> uploadTasks = []; + + // 1. Gestione Documenti normali (PlatformFile) + if (event.pickedFiles != null) { for (var file in event.pickedFiles!) { - await _repository.uploadAndRegisterOperationFile( - operationId: state.operationId!, - pickedFile: file, + uploadTasks.add( + _repository.uploadAndRegisterOperationFile( + operationId: state.operationId!, + pickedFile: file, + ), ); } } + + // 2. Gestione Foto Fotocamera (XFile) + if (event.photos != null) { + for (var photo in event.photos!) { + // Leggiamo i byte asincronamente + final bytes = await photo.readAsBytes(); + final fileSize = await photo.length(); + + // Lo travestiamo da PlatformFile per passarlo al Repository! + final fakePlatformFile = PlatformFile( + name: photo.name, + size: fileSize, + bytes: bytes, + path: photo.path, + ); + + uploadTasks.add( + _repository.uploadAndRegisterOperationFile( + operationId: state.operationId!, + pickedFile: fakePlatformFile, + ), + ); + } + } + + // Esecuzione parallela di tutti i documenti e foto + await Future.wait(uploadTasks); emit(state.copyWith(status: OperationFilesStatus.success)); } catch (e) { emit( @@ -165,48 +233,6 @@ class OperationFilesBloc } } - FutureOr _onUploadMultipleOperationFiles( - UploadMultipleOperationFilesEvent event, - Emitter emit, - ) async { - if (event.files.isEmpty) { - emit( - state.copyWith( - status: OperationFilesStatus.failure, - error: "Nessun file selezionato", - ), - ); - return; - } - emit(state.copyWith(status: OperationFilesStatus.uploading, error: null)); - try { - // 2. Creiamo una lista di "Promesse" (Futures) per il repository - final List> uploadTasks = []; - for (var file in event.files) { - // Aggiungiamo il task alla lista, ma NON usiamo await qui dentro! - uploadTasks.add( - _repository.uploadAndRegisterOperationFile( - operationId: state.operationId!, - pickedFile: file, - ), - ); - } - // 3. ESECUZIONE PARALLELA! - // Aspettiamo che tutti i file siano caricati contemporaneamente. - await Future.wait(uploadTasks); - // 4. GRAN FINALE: Tutto caricato, emettiamo il success! - emit(state.copyWith(status: OperationFilesStatus.success)); - } catch (e) { - // Se anche un solo file fallisce, catturiamo l'errore - emit( - state.copyWith( - status: OperationFilesStatus.failure, - error: "Errore durante l'upload multiplo: $e", - ), - ); - } - } - FutureOr _onDeleteOperationFiles( DeleteOperationFilesEvent event, Emitter emit, @@ -231,7 +257,7 @@ class OperationFilesBloc ToggleOperationFileSelectionEvent event, Emitter emit, ) { - List selectedFiles = List.from(state.selectedFiles); + final selectedFiles = List.from(state.selectedFiles); if (selectedFiles.contains(event.file)) { selectedFiles.remove(event.file); } else { @@ -239,4 +265,62 @@ class OperationFilesBloc } emit(state.copyWith(selectedFiles: selectedFiles)); } + + FutureOr _onLinkFilesToCustomer( + LinkFilesToCustomerEvent event, + Emitter emit, + ) async { + if (state.selectedFiles.isEmpty) return; + + // BIVIO 1: PRATICA NUOVA (Modalità Locale) + if (state.operationId == null) { + // Mappiamo i file locali: se sono tra quelli selezionati, iniettiamo il customerId + final updatedLocalFiles = state.localFiles.map((file) { + if (state.selectedFiles.contains(file)) { + return file.copyWith(customerId: event.customerId); + } + return file; + }).toList(); + + emit( + state.copyWith( + localFiles: updatedLocalFiles, + selectedFiles: [], // Svuotiamo la selezione dopo averli associati + status: OperationFilesStatus.success, // o un toast di feedback + ), + ); + return; + } + + // BIVIO 2: PRATICA ESISTENTE (Modalità Remota su DB) + emit(state.copyWith(status: OperationFilesStatus.loading)); + try { + final List> linkTasks = []; + + for (var file in state.selectedFiles) { + linkTasks.add( + _repository.copyFileToCustomer( + file: file, + customerId: event.customerId, + ), + ); + } + + await Future.wait(linkTasks); + + // Svuotiamo la selezione. + // NON serve aggiornare la lista a mano, perché il DB si aggiorna + // e lo Stream di Supabase spingerà automaticamente in UI i file aggiornati! + emit( + state.copyWith(status: OperationFilesStatus.success, selectedFiles: []), + ); + } catch (e) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: "Errore associazione: $e", + ), + ); + } + } } diff --git a/lib/features/operations/blocs/operation_files_events.dart b/lib/features/operations/blocs/operation_files_events.dart index 4542902..44a3b5e 100644 --- a/lib/features/operations/blocs/operation_files_events.dart +++ b/lib/features/operations/blocs/operation_files_events.dart @@ -17,7 +17,7 @@ class OperationsavedEvent extends OperationFilesEvent { class LoadOperationFilesEvent extends OperationFilesEvent { final String? operationId; - final OperationModel? operation; + final AttachmentModel? operation; const LoadOperationFilesEvent({this.operationId, this.operation}); @override @@ -34,23 +34,25 @@ class AddOperationFilesEvent extends OperationFilesEvent { class UploadOperationFilesEvent extends OperationFilesEvent { final List? pickedFiles; - final List? photos; + final List? photos; const UploadOperationFilesEvent({this.pickedFiles, this.photos}); @override List get props => [pickedFiles, photos]; } -class UploadMultipleOperationFilesEvent extends OperationFilesEvent { - final List files; - const UploadMultipleOperationFilesEvent(this.files); +class LinkFilesToCustomerEvent extends OperationFilesEvent { + final String customerId; + + const LinkFilesToCustomerEvent({required this.customerId}); + @override - List get props => [files]; + List get props => [customerId]; } class DeleteOperationFilesEvent extends OperationFilesEvent {} class ToggleOperationFileSelectionEvent extends OperationFilesEvent { - final OperationFileModel file; + final AttachmentModel file; const ToggleOperationFileSelectionEvent(this.file); } diff --git a/lib/features/operations/blocs/operation_files_state.dart b/lib/features/operations/blocs/operation_files_state.dart index 8ea5eb3..a102dd4 100644 --- a/lib/features/operations/blocs/operation_files_state.dart +++ b/lib/features/operations/blocs/operation_files_state.dart @@ -15,10 +15,10 @@ class OperationFilesState extends Equatable { final String? operationId; final OperationFilesStatus status; final String? error; - final List localFiles; - final List remoteFiles; + final List localFiles; + final List remoteFiles; - final List selectedFiles; + final List selectedFiles; @override List get props => [ @@ -30,15 +30,15 @@ class OperationFilesState extends Equatable { selectedFiles, ]; - List get allFiles => [...remoteFiles, ...localFiles]; + List get allFiles => [...remoteFiles, ...localFiles]; OperationFilesState copyWith({ String? operationId, OperationFilesStatus? status, String? error, - List? localFiles, - List? remoteFiles, - List? selectedFiles, + List? localFiles, + List? remoteFiles, + List? selectedFiles, }) { return OperationFilesState( operationId: operationId ?? this.operationId, diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index f66e1b0..cfdf1a6 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -4,19 +4,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/extensions.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; -import 'package:flux/features/operations/models/energy_operation_model.dart'; -import 'package:flux/features/operations/models/entertainment_operation_model.dart'; -import 'package:flux/features/operations/models/fin_operation_model.dart'; -import 'package:flux/features/operations/models/operation_file_model.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:get_it/get_it.dart'; import 'package:collection/collection.dart'; +import 'package:uuid/uuid.dart'; part 'operations_state.dart'; class OperationsCubit extends Cubit { final OperationsRepository _repository = GetIt.I(); final SessionCubit _sessionCubit = GetIt.I(); + final Uuid _uuid = const Uuid(); // Generatore di UUID per il batch OperationsCubit() : super(const OperationsState(status: OperationsStatus.initial)); @@ -24,17 +23,13 @@ class OperationsCubit extends Cubit { // --- CARICAMENTO E PAGINAZIONE --- Future loadOperations({bool refresh = false}) async { - // Se stiamo già caricando, evitiamo chiamate doppie if (state.status == OperationsStatus.loading) return; - - // Se non è un refresh e abbiamo già raggiunto la fine dei dati, ci fermiamo if (!refresh && state.hasReachedMax) return; emit( state.copyWith( status: OperationsStatus.loading, errorMessage: null, - // Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading allOperations: refresh ? [] : state.allOperations, hasReachedMax: refresh ? false : state.hasReachedMax, ), @@ -56,7 +51,6 @@ class OperationsCubit extends Cubit { dateRange: state.dateRange, ); - // Se ricevi meno record del limite, significa che non ce ne sono altri sul DB final bool reachedMax = newOperations.length < 50; emit( @@ -72,7 +66,7 @@ class OperationsCubit extends Cubit { emit( state.copyWith( status: OperationsStatus.failure, - errorMessage: "Errore nel caricamento servizi: $e", + errorMessage: "Errore nel caricamento operazioni: $e", ), ); } @@ -80,7 +74,6 @@ class OperationsCubit extends Cubit { // --- GESTIONE FILTRI --- - /// Aggiorna i parametri di ricerca e ricarica da zero void updateFilters({String? query, DateTimeRange? range}) { emit( state.copyWith( @@ -91,15 +84,11 @@ class OperationsCubit extends Cubit { loadOperations(refresh: true); } - /// Pulisce tutti i filtri void clearFilters() { emit(state.copyWith(query: '', dateRange: null)); loadOperations(refresh: true); } - // --- GESTIONE BOZZA (DRAFT) --- - - /// Inizializza un nuovo servizio o ne carica uno esistente per la modifica void initOperationForm({ OperationModel? existingOperation, String? operationId, @@ -123,14 +112,16 @@ class OperationsCubit extends Cubit { ), ); } else { - // Crea un template vuoto con lo store di default (se disponibile) + // NUOVA PRATICA: Creiamo un nuovo Batch UUID emit( state.copyWith( currentOperation: OperationModel( storeId: _sessionCubit.state.currentStore?.id ?? '', - number: '', // Sarà compilato dall'utente + reference: '', createdAt: DateTime.now(), companyId: _sessionCubit.state.company!.id!, + status: OperationStatus.draft, + batchUuid: _uuid.v4(), // <-- GENERIAMO IL BATCH UNIVOCO ), status: OperationsStatus.ready, ), @@ -138,68 +129,25 @@ class OperationsCubit extends Cubit { } } - /// Metodo generico per aggiornare i campi base (AL, MNP, Note, ecc.) - void updateField({ - int? al, - int? mnp, - int? nip, - int? unica, - int? telepass, - String? note, - String? number, - bool? isBozza, - bool? resultOk, - String? customerId, - String? customerDisplayName, - }) { + /// MAGIA PURA: Prepara il form per inserire un altro servizio nella stessa pratica. + /// Mantiene il Cliente, il Batch e lo Store, ma svuota il resto. + void prepareNextOperationInBatch() { if (state.currentOperation == null) return; - final updated = state.currentOperation!.copyWith( - al: al, - mnp: mnp, - nip: nip, - unica: unica, - telepass: telepass, - note: note, - number: number, - isBozza: isBozza, - resultOk: resultOk, - customerId: customerId, - customerDisplayName: customerDisplayName, - ); + final current = state.currentOperation!; - emit(state.copyWith(currentOperation: updated)); - } - - // --- GESTIONE MODULI COMPLESSI --- - - void updateEnergyOperations(List energyList) { emit( state.copyWith( - currentOperation: state.currentOperation?.copyWith( - energyOperations: energyList, - ), - ), - ); - } - - void updateFinOperations(List finList) { - emit( - state.copyWith( - currentOperation: state.currentOperation?.copyWith( - finOperations: finList, - ), - ), - ); - } - - void updateEntertainmentOperations( - List entList, - ) { - emit( - state.copyWith( - currentOperation: state.currentOperation?.copyWith( - entertainmentOperations: entList, + status: OperationsStatus.ready, + currentOperation: OperationModel( + companyId: current.companyId, + storeId: current.storeId, + storeDisplayName: current.storeDisplayName, + batchUuid: current.batchUuid, // <-- MANTIENE IL COLLEGAMENTO + customerId: current.customerId, // <-- MANTIENE IL CLIENTE + customerDisplayName: current.customerDisplayName, + status: OperationStatus.draft, + createdAt: DateTime.now(), ), ), ); @@ -208,35 +156,33 @@ class OperationsCubit extends Cubit { // --- PERSISTENZA --- Future saveCurrentOperation({ - required bool isBozza, + required OperationStatus targetStatus, bool shouldPop = true, - List? files, }) async { if (state.currentOperation == null) return; emit(state.copyWith(status: OperationsStatus.saving, errorMessage: null)); try { - // 1. Aggiorniamo il flag bozza in base a quale pulsante ha premuto l'utente final operationToSave = state.currentOperation!.copyWith( - isBozza: isBozza, - files: files, + status: targetStatus, ); - // 2. Salvataggio corazzato final updatedOperation = await _repository.saveFullOperation( operationToSave, ); - // 3. Reset e ricaricamento emit( state.copyWith( + // Se non facciamo Pop (es. l'utente vuole aggiungere un altro servizio), non killiamo l'operazione corrente status: shouldPop ? OperationsStatus.saved : OperationsStatus.savedNoPop, currentOperation: shouldPop ? null : updatedOperation, ), ); - await loadOperations(refresh: true); + + // Ricarica in background per la dashboard + loadOperations(refresh: true); } catch (e) { emit( state.copyWith( @@ -247,115 +193,29 @@ class OperationsCubit extends Cubit { } } - // --- GESTIONE ALLEGATI LOCALI --- + // --- RECUPERO OPERAZIONI DELLO STESSO BATCH (Per UI di riepilogo) --- - void addAttachments(List files) { - final newAttachments = files.map((file) { - return OperationFileModel( - id: null, // Meglio null se non è su DB - operationId: state.currentOperation?.id ?? '', - name: file.name.fileNameWithoutExtension(), - extension: file.name.fileExtension(), - storagePath: '', - fileSize: file.size, - localBytes: file.bytes, - createdAt: DateTime.now(), - ); - }).toList(); + /// Puoi usare questa funzione se nella UI vuoi mostrare "Hai inserito 3 servizi in questa pratica" + List getOperationsInCurrentBatch() { + if (state.currentOperation == null) return []; + final currentBatch = state.currentOperation!.batchUuid; - // Creiamo una nuova lista pulita - final List updatedList = [ - ...(state.currentOperation?.files ?? []), - ...newAttachments, - ]; - - // Emettiamo lo stato assicurandoci che il OperationModel venga clonato - if (state.currentOperation != null) { - emit( - state.copyWith( - currentOperation: state.currentOperation!.copyWith( - files: updatedList, - ), - ), - ); - } + // Filtriamo dalla lista caricata (o potresti fare una query diretta a Supabase se preferisci) + return state.allOperations + .where( + (op) => + op.batchUuid == currentBatch && + op.id != state.currentOperation!.id, + ) + .toList(); } - void removeAttachment(int index) { + void updateField({String? customerId, String? customerDisplayName}) { if (state.currentOperation == null) return; - - final updatedList = List.from( - state.currentOperation!.files, + final updated = state.currentOperation!.copyWith( + customerId: customerId, + customerDisplayName: customerDisplayName, ); - updatedList.removeAt(index); - - emit( - state.copyWith( - currentOperation: state.currentOperation?.copyWith(files: updatedList), - ), - ); - } - - void saveAndCopyFileToCustomer(List selectedFiles) async { - final currentOperation = state.currentOperation; - - // 1. Check di sicurezza: se non c'è il cliente, non sappiamo dove copiare - if (currentOperation == null || currentOperation.customerId == null) { - emit( - state.copyWith( - status: OperationsStatus.failure, - errorMessage: - "Impossibile copiare: nessun cliente associato alla pratica.", - ), - ); - return; - } - - emit(state.copyWith(status: OperationsStatus.loading)); - - try { - // 2. SALVATAGGIO CORAZZATO - // Chiamiamo il repo e otteniamo la pratica con TUTTI i file ora dotati di ID e storagePath - final updatedOperation = await _repository.saveFullOperation( - currentOperation, - ); - - // 3. COPIA RELAZIONALE - // Per ogni file che l'utente ha selezionato nella UI, cerchiamo la sua versione - // "ufficiale" (quella con lo storagePath) nel modello appena tornato dal DB. - for (var selectedFile in selectedFiles) { - // Cerchiamo il match nel modello aggiornato - final persistedFile = updatedOperation.files.firstWhere( - (f) => - f.name == selectedFile.name && - f.extension == selectedFile.extension, - orElse: () => throw Exception( - "File ${selectedFile.name} non trovato dopo il salvataggio.", - ), - ); - - // Creiamo il link nel database del cliente - await _repository.copyFileToCustomer( - file: persistedFile, - customerId: currentOperation.customerId!, - ); - } - - // 4. AGGIORNAMENTO STATO - // Aggiorniamo il Cubit con il servizio salvato così la UI mostra i file come "Remoti" - emit( - state.copyWith( - status: OperationsStatus.success, - currentOperation: updatedOperation, - ), - ); - } catch (e) { - emit( - state.copyWith( - status: OperationsStatus.failure, - errorMessage: "Errore durante il salvataggio e copia: $e", - ), - ); - } + emit(state.copyWith(currentOperation: updated)); } } diff --git a/lib/features/operations/data/operations_repository.dart b/lib/features/operations/data/operations_repository.dart index d4298ad..e846ca5 100644 --- a/lib/features/operations/data/operations_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -3,9 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/attachments/models/attachment_model.dart'; -import 'package:flux/features/customers/data/customer_repository.dart'; -import 'package:flux/features/customers/models/customer_file_model.dart'; -import 'package:flux/features/operations/models/operation_file_model.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/operation_model.dart'; @@ -13,7 +10,6 @@ import '../models/operation_model.dart'; class OperationsRepository { final _supabase = Supabase.instance.client; final companyId = GetIt.I.get().state.company!.id; - final CustomerRepository _customerRepository = GetIt.I(); // --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO --- Future fetchOperationById(String id) async { @@ -23,7 +19,11 @@ class OperationsRepository { .select(''' *, customer(name), - staff_member(name) + store(name), + staff_member(name), + provider(name), + model(name_with_brand), + attachments(*) ''') .eq('id', id) .single(); @@ -48,6 +48,9 @@ class OperationsRepository { .select(''' *, customer(name), + store(name), + provider(name), + model(name_with_brand), staff_member(name), attachments(*) ''') @@ -107,49 +110,6 @@ class OperationsRepository { final String newId = operationData['id']; - // 4. UPLOAD DEI FILE LOCALI (Nuovi) - // Filtriamo solo i file che non hanno ancora un ID (quindi sono locali) - final localFilesToUpload = operation.attachments - .where((f) => f.id == null) - .toList(); - - if (localFilesToUpload.isNotEmpty) { - final List uploadTasks = []; - - for (var file in localFilesToUpload) { - final storagePath = - '$companyId/operations/$newId/${DateTime.now().millisecondsSinceEpoch}_${file.name}.${file.extension}'; - final String mimeType = file.extension.toLowerCase() == 'pdf' - ? 'application/pdf' - : 'image/${file.extension}'; - - final fileToSave = file.copyWith( - operationId: newId, - storagePath: storagePath, - ); - - // Creiamo una funzione asincrona per caricare file e scrivere nel DB - Future uploadAndLink() async { - // A. Upload nel Bucket Storage (usiamo i bytes così funziona anche su Web!) - await _supabase.storage - .from('documents') - .uploadBinary( - storagePath, - fileToSave.localBytes!, - fileOptions: FileOptions(contentType: mimeType, upsert: true), - ); - - // B. Inserimento riga nel DB relazionale - await _supabase.from('attachment').insert(fileToSave.toMap()); - } - - uploadTasks.add(uploadAndLink()); - } - - // Eseguiamo tutti gli upload in parallelo per la massima velocità - await Future.wait(uploadTasks); - } - // 5. GRAN FINALE: RECUPERO DEL MODELLO COMPLETO E AGGIORNATO // Interroghiamo Supabase per farci restituire la pratica con TUTTI gli ID generati // (inclusi quelli della tabella operation_file appena inseriti) @@ -158,6 +118,9 @@ class OperationsRepository { .select(''' *, staff_member(name), + store(name), + provider(name), + model(name_with_brand), customer(name), attachments(*) ''') @@ -278,7 +241,7 @@ class OperationsRepository { } Future copyFileToCustomer({ - required OperationFileModel file, + required AttachmentModel file, required String customerId, }) async { await _supabase @@ -290,16 +253,28 @@ class OperationsRepository { Future deleteOperationFiles(List files) async { if (files.isEmpty) return; // 1. Prepariamo le liste di ID e di Percorsi - final List idsToDelete = files.map((f) => f.id!).toList(); - final List storagePaths = files.map((f) => f.storagePath).toList(); - + final List idsToDelete = []; + final List idsToEdit = []; + final List storagePathsToDelete = []; + for (var file in files) { + if (file.customerId == null) { + idsToDelete.add(file.id!); + storagePathsToDelete.add(file.storagePath); + } else { + idsToEdit.add(file.id!); + } + } try { - await _supabase - .from('attachment') - .update({'operation_id': null}) - .inFilter('id', idsToDelete); - - await _supabase.storage.from('documents').remove(storagePaths); + if (idsToDelete.isNotEmpty) { + await _supabase.from('attachment').delete().inFilter('id', idsToDelete); + await _supabase.storage.from('documents').remove(storagePathsToDelete); + } + if (idsToEdit.isNotEmpty) { + await _supabase + .from('attachment') + .update({'operation_id': null}) + .inFilter('id', idsToEdit); + } } on PostgrestException catch (e) { throw 'Errore database: ${e.message}'; } catch (e) { diff --git a/lib/features/operations/models/operation_file_model.dart b/lib/features/operations/models/operation_file_model.dart deleted file mode 100644 index 376c8c1..0000000 --- a/lib/features/operations/models/operation_file_model.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:typed_data'; - -import 'package:equatable/equatable.dart'; - -class OperationFileModel extends Equatable { - final String? id; - final DateTime? createdAt; - final String name; - final String extension; - final String storagePath; - final String operationId; - final int fileSize; - final Uint8List? localBytes; - - const OperationFileModel({ - this.id, - this.createdAt, - required this.name, - required this.extension, - required this.storagePath, - required this.operationId, - required this.fileSize, - this.localBytes, - }); - - bool get isLocal => localBytes != null; - - // Trasforma i byte in qualcosa di leggibile (KB, MB, GB) - String get sizeFormatted { - if (fileSize <= 0) return "0 B"; - const suffixes = ["B", "KB", "MB", "GB", "TB"]; - var i = (fileSize.toString().length - 1) ~/ 3; - if (i >= suffixes.length) i = suffixes.length - 1; - double num = fileSize / (1 << (i * 10)); - return "${num.toStringAsFixed(i == 0 ? 0 : 1)} ${suffixes[i]}"; - } - - bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf'; - - OperationFileModel copyWith({ - String? id, - DateTime? createdAt, - String? name, - String? extension, - String? storagePath, - String? operationId, - int? fileSize, - Uint8List? localBytes, - }) { - return OperationFileModel( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - name: name ?? this.name, - extension: extension ?? this.extension, - storagePath: storagePath ?? this.storagePath, - operationId: operationId ?? this.operationId, - fileSize: fileSize ?? this.fileSize, - localBytes: localBytes ?? this.localBytes, - ); - } - - factory OperationFileModel.fromMap(Map map) { - return OperationFileModel( - id: map['id'] as String, - createdAt: map['created_at'] != null - ? DateTime.parse(map['created_at']) - : null, - name: map['name'] ?? '', - extension: map['extension'] ?? '', - storagePath: map['storage_path'] ?? '', - operationId: map['operation_id']?.toString() ?? '', - fileSize: map['file_size'] is int - ? map['file_size'] - : int.tryParse(map['file_size']?.toString() ?? '0') ?? 0, - ); - } - - Map toMap() { - return { - if (id != null) 'id': id, - 'name': name, - 'extension': extension, - 'storage_path': storagePath, - 'operation_id': operationId, - 'file_size': fileSize, - }; - } - - @override - List get props => [ - id, - createdAt, - name, - extension, - storagePath, - operationId, - fileSize, - localBytes, - ]; -} diff --git a/lib/features/operations/models/operation_model.dart b/lib/features/operations/models/operation_model.dart index 6f304a1..7ffe874 100644 --- a/lib/features/operations/models/operation_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -1,4 +1,5 @@ import 'package:equatable/equatable.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/features/attachments/models/attachment_model.dart'; enum OperationStatus { @@ -27,7 +28,9 @@ class OperationModel extends Equatable { final DateTime? createdAt; final String type; final String? providerId; + final String? providerDisplayName; final String? modelId; + final String? modelDisplayName; final String? description; final DateTime? expirationDate; final String note; @@ -35,13 +38,14 @@ class OperationModel extends Equatable { final String batchUuid; final String companyId; final String storeId; + final String? storeDisplayName; final int quantity; final String? staffId; - final String staffDisplayName; + final String? staffDisplayName; final String? lastCampaignId; final OperationStatus status; final String? customerId; - final String customerDisplayName; + final String? customerDisplayName; final String reference; // ALLEGATI (Aggiunto) @@ -52,7 +56,9 @@ class OperationModel extends Equatable { this.createdAt, this.type = '', this.providerId, + this.providerDisplayName, this.modelId, + this.modelDisplayName, this.description, this.expirationDate, this.note = '', @@ -60,13 +66,14 @@ class OperationModel extends Equatable { this.batchUuid = '', required this.companyId, this.storeId = '', + this.storeDisplayName, this.quantity = 1, this.staffId, - this.staffDisplayName = '', + this.staffDisplayName, this.lastCampaignId, this.status = OperationStatus.draft, this.customerId, - this.customerDisplayName = '', + this.customerDisplayName, this.reference = '', this.attachments = const [], }); @@ -76,7 +83,9 @@ class OperationModel extends Equatable { DateTime? createdAt, String? type, String? providerId, + String? providerDisplayName, String? modelId, + String? modelDisplayName, String? description, DateTime? expirationDate, String? note, @@ -84,6 +93,7 @@ class OperationModel extends Equatable { String? batchUuid, String? companyId, String? storeId, + String? storeDisplayName, int? quantity, String? staffId, String? staffDisplayName, @@ -98,7 +108,9 @@ class OperationModel extends Equatable { createdAt: createdAt ?? this.createdAt, type: type ?? this.type, providerId: providerId ?? this.providerId, + providerDisplayName: providerDisplayName ?? this.providerDisplayName, modelId: modelId ?? this.modelId, + modelDisplayName: modelDisplayName ?? this.modelDisplayName, description: description ?? this.description, expirationDate: expirationDate ?? this.expirationDate, note: note ?? this.note, @@ -106,6 +118,7 @@ class OperationModel extends Equatable { batchUuid: batchUuid ?? this.batchUuid, companyId: companyId ?? this.companyId, storeId: storeId ?? this.storeId, + storeDisplayName: storeDisplayName ?? this.storeDisplayName, quantity: quantity ?? this.quantity, staffId: staffId ?? this.staffId, staffDisplayName: staffDisplayName ?? this.staffDisplayName, @@ -123,7 +136,9 @@ class OperationModel extends Equatable { createdAt, type, providerId, + providerDisplayName, modelId, + modelDisplayName, description, expirationDate, note, @@ -131,6 +146,7 @@ class OperationModel extends Equatable { batchUuid, companyId, storeId, + storeDisplayName, quantity, staffId, staffDisplayName, @@ -154,7 +170,9 @@ class OperationModel extends Equatable { : null, type: map['type'] as String? ?? '', providerId: map['provider_id'] as String? ?? '', + providerDisplayName: "${map['provider']['name']}".myFormat(), modelId: map['model_id'] as String? ?? '', + modelDisplayName: "${map['model']['name_with_brand'] ?? ''}".myFormat(), description: map['description'] as String? ?? '', expirationDate: map['expiration_date'] != null ? DateTime.parse(map['expiration_date']) @@ -164,13 +182,22 @@ class OperationModel extends Equatable { batchUuid: map['batch_uuid'] as String, companyId: map['company_id'] as String, storeId: map['store_id'] as String? ?? '', + storeDisplayName: "${map['store']['name']}".myFormat(), quantity: map['quantity'] is int ? map['quantity'] : int.tryParse(map['quantity']?.toString() ?? '0') ?? 0, staffId: map['staff_id'] as String? ?? '', + staffDisplayName: "${map['staff_member']['name'] ?? ''}".myFormat(), lastCampaignId: map['last_campaign_id'] as String? ?? '', status: OperationStatus.fromString(map['status']), customerId: map['customer_id'] as String? ?? '', + customerDisplayName: "${map['customer']['name'] ?? ''}".myFormat(), + attachments: + (map['attachment'] as List?) + ?.map((x) => AttachmentModel.fromMap(x)) + .toList() ?? + const [], + reference: map['reference'] as String? ?? '', ); } diff --git a/lib/features/operations/ui/operation_form_screen/attachment_section.dart b/lib/features/operations/ui/operation_form_screen/attachment_section.dart index 5980313..7933045 100644 --- a/lib/features/operations/ui/operation_form_screen/attachment_section.dart +++ b/lib/features/operations/ui/operation_form_screen/attachment_section.dart @@ -2,12 +2,14 @@ import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/widgets/image_viewer_widget.dart'; import 'package:flux/core/widgets/pdf_viewer_widget.dart'; import 'package:flux/core/widgets/qr_upload_dialog.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/operation_file_model.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; class AttachmentsSection extends StatelessWidget { const AttachmentsSection({super.key}); @@ -227,10 +229,30 @@ class AttachmentsSection extends StatelessWidget { ElevatedButton.icon( icon: const Icon(Icons.copy), label: const Text("Copia in Cliente"), - onPressed: () => saveAndCopyFilesToCustomer( - context, - state.selectedFiles, - ), + onPressed: () { + final cubit = context.read(); + if (cubit.state.currentOperation?.customerId == + null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + context + .l10n + .operationFormAttachmentSectionNoCustomer, + ), + ), + ); + } else { + context.read().add( + LinkFilesToCustomerEvent( + customerId: cubit + .state + .currentOperation! + .customerId!, + ), + ); + } + }, ), ], ), @@ -278,9 +300,8 @@ class AttachmentsSection extends StatelessWidget { // Salviamo forzatamente in bozza await cubit.saveCurrentOperation( - isBozza: true, + targetStatus: OperationStatus.draft, shouldPop: false, - files: operationFilesBloc.state.localFiles, ); // Recuperiamo il servizio aggiornato con l'ID! @@ -321,39 +342,8 @@ class AttachmentsSection extends StatelessWidget { } } - // --- LOGICA DI COPIA AL CLIENTE --- - void saveAndCopyFilesToCustomer( - BuildContext context, - List files, - ) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("Copia nei documenti Cliente"), - content: const Text( - "Vuoi copiare i file selezionati nell'anagrafica del cliente? \n\n" - "Attenzione: per procedere, la pratica attuale verrà prima salvata in stato BOZZA.", - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx), - child: const Text("Annulla"), - ), - ElevatedButton( - onPressed: () { - Navigator.pop(ctx); - // 1. Diciamo al Cubit di salvare in Bozza e fare la copia - context.read().saveAndCopyFileToCustomer(files); - }, - child: const Text("Salva e Copia"), - ), - ], - ), - ); - } - // --- LOGICA DI VISUALIZZAZIONE OVERLAY --- - void _handleDoubleClick(BuildContext context, OperationFileModel file) { + void _handleDoubleClick(BuildContext context, AttachmentModel file) { showDialog( context: context, barrierDismissible: true, diff --git a/lib/features/operations/ui/operation_form_screen/general_info_section.dart b/lib/features/operations/ui/operation_form_screen/general_info_section.dart index ceaf3db..183224c 100644 --- a/lib/features/operations/ui/operation_form_screen/general_info_section.dart +++ b/lib/features/operations/ui/operation_form_screen/general_info_section.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; class GeneralInfoSection extends StatelessWidget { @@ -34,7 +32,7 @@ class GeneralInfoSection extends StatelessWidget { // Numero di Riferimento / Telefono TextFormField( - initialValue: operation.number, + initialValue: operation.reference, keyboardType: TextInputType .phone, // Fa aprire il tastierino numerico su mobile decoration: const InputDecoration( @@ -43,49 +41,6 @@ class GeneralInfoSection extends StatelessWidget { border: OutlineInputBorder(), prefixIcon: Icon(Icons.phone), ), - onChanged: (val) { - context.read().updateField(number: val); - }, - ), - const SizedBox(height: 16), - - // I due Switch affiancati (Bozza e A buon fine) - Row( - children: [ - Expanded( - child: SwitchListTile( - title: const Text("Bozza"), - subtitle: const Text( - "Pratica in lavorazione", - style: TextStyle(fontSize: 12), - ), - value: operation.isBozza, - activeThumbColor: Colors.orange, - contentPadding: EdgeInsets.zero, - onChanged: (val) { - context.read().updateField(isBozza: val); - }, - ), - ), - const SizedBox(width: 16), - Expanded( - child: SwitchListTile( - title: const Text("A buon fine"), - subtitle: const Text( - "Esito positivo", - style: TextStyle(fontSize: 12), - ), - value: operation.resultOk, - activeThumbColor: Colors.green, - contentPadding: EdgeInsets.zero, - onChanged: (val) { - context.read().updateField( - resultOk: val, - ); - }, - ), - ), - ], ), const SizedBox(height: 16), @@ -101,9 +56,6 @@ class GeneralInfoSection extends StatelessWidget { border: OutlineInputBorder(), alignLabelWithHint: true, ), - onChanged: (val) { - context.read().updateField(note: val); - }, ), ], ), diff --git a/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart index 4608a9a..b7379e3 100644 --- a/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart @@ -5,7 +5,6 @@ import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/ui/operation_form_screen/attachment_section.dart'; import 'package:flux/features/operations/ui/operation_form_screen/customer_section.dart'; import 'package:flux/features/operations/ui/operation_form_screen/general_info_section.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/operations_grid.dart'; class OperationFormScreen extends StatefulWidget { final String? operationId; @@ -34,9 +33,16 @@ class _OperationFormScreenState extends State { }); } - void _performSave(BuildContext context, {required bool isBozza}) { + void _performSave( + BuildContext context, { + required OperationStatus targetStatus, + required bool shouldPop, + }) { FocusScope.of(context).unfocus(); - context.read().saveCurrentOperation(isBozza: isBozza); + context.read().saveCurrentOperation( + targetStatus: targetStatus, + shouldPop: shouldPop, + ); } @override @@ -93,7 +99,11 @@ class _OperationFormScreenState extends State { IconButton( icon: const Icon(Icons.edit_note), tooltip: "Salva come Bozza", - onPressed: () => _performSave(context, isBozza: true), + onPressed: () => _performSave( + context, + targetStatus: OperationStatus.draft, + shouldPop: false, + ), ), IconButton( icon: const Icon( @@ -101,7 +111,11 @@ class _OperationFormScreenState extends State { color: Colors.green, ), tooltip: "Conferma Pratica", - onPressed: () => _performSave(context, isBozza: false), + onPressed: () => _performSave( + context, + targetStatus: OperationStatus.ok, + shouldPop: true, + ), ), const SizedBox(width: 8), ], @@ -120,9 +134,6 @@ class _OperationFormScreenState extends State { GeneralInfoSection(operation: operation), const SizedBox(height: 24), - OperationsGrid(operation: operation), - const SizedBox(height: 32), - AttachmentsSection(), const SizedBox(height: 32), _buildBottomActionButtons(context, isSaving: isSaving), @@ -152,7 +163,11 @@ class _OperationFormScreenState extends State { label: const Text("Salva in Bozza"), onPressed: isSaving ? null - : () => _performSave(context, isBozza: true), + : () => _performSave( + context, + targetStatus: OperationStatus.draft, + shouldPop: false, + ), ), ), @@ -173,7 +188,11 @@ class _OperationFormScreenState extends State { ), onPressed: isSaving ? null - : () => _performSave(context, isBozza: false), + : () => _performSave( + context, + targetStatus: OperationStatus.ok, + shouldPop: true, + ), ), ), ], diff --git a/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart b/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart index 8adb21c..ad3fde1 100644 --- a/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart +++ b/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart @@ -296,7 +296,7 @@ class _OperationMobileUploadScreenState // Diciamo al BLoC di caricare tutti i file. // Usiamo il tuo evento esistente per ogni file (il BLoC li metterà in coda) final bloc = context.read(); - bloc.add(UploadMultipleOperationFilesEvent(_stagedFiles)); + bloc.add(UploadOperationFilesEvent(pickedFiles: _stagedFiles)); // N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"! } diff --git a/lib/features/operations/ui/operation_form_screen/operations_grid.dart b/lib/features/operations/ui/operation_form_screen/operations_grid.dart deleted file mode 100644 index e5e252e..0000000 --- a/lib/features/operations/ui/operation_form_screen/operations_grid.dart +++ /dev/null @@ -1,201 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/energy_operation_model.dart'; -import 'package:flux/features/operations/models/entertainment_operation_model.dart'; -import 'package:flux/features/operations/models/fin_operation_model.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/action_card.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/energy_operation_dialog.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/entertainment_operation_card.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/finance_operation_dialog.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/int_dialogs.dart'; // Assicurati di importare il modello - -class OperationsGrid extends StatelessWidget { - final OperationModel operation; - - const OperationsGrid({super.key, required this.operation}); - - @override - Widget build(BuildContext context) { - return Card( - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - - children: [ - Row( - children: [ - Icon( - Icons.layers_outlined, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text( - "Servizi e Accessori", - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - const SizedBox(height: 20), - SizedBox( - width: double.infinity, - child: Wrap( - spacing: 16, - runSpacing: 16, - alignment: WrapAlignment.center, - children: [ - // --- CONTATORI SEMPLICI --- - ActionCard( - label: "AL", - count: operation.al, - icon: Icons.sim_card, - color: Colors.blue, - onTap: () => updateCountDialog( - context, - "AL", - operation.al, - (val) => - context.read().updateField(al: val), - ), - ), - ActionCard( - label: "MNP", - count: operation.mnp, - icon: Icons.phone_android, - color: Colors.indigo, - onTap: () => updateCountDialog( - context, - "MNP", - operation.mnp, - (val) => - context.read().updateField(mnp: val), - ), - ), - ActionCard( - label: "NIP", - count: operation.nip, - icon: Icons.compare_arrows, - color: Colors.cyan, - onTap: () => updateCountDialog( - context, - "NIP", - operation.nip, - (val) => - context.read().updateField(nip: val), - ), - ), - ActionCard( - label: "Unica", - count: operation.unica, - icon: Icons.all_inclusive, - color: Colors.purple, - onTap: () => updateCountDialog( - context, - "Unica", - operation.unica, - (val) => context.read().updateField( - unica: val, - ), - ), - ), - ActionCard( - label: "Telepass", - count: operation.telepass, - icon: Icons.directions_car, - color: Colors.amber.shade700, - onTap: () => updateCountDialog( - context, - "Telepass", - operation.telepass, - (val) => context.read().updateField( - telepass: val, - ), - ), - ), - - // --- MODULI COMPLESSI (Le liste) --- - ActionCard( - label: "Energia", - count: operation.energyOperations.length, - icon: Icons.bolt, - color: Colors.green, - onTap: () async { - // Apriamo la modale e aspettiamo il risultato - final result = - await showDialog>( - context: context, - builder: (context) => EnergyOperationDialog( - currentStoreId: operation.storeId, - initialOperations: operation - .energyOperations, // Passiamo la lista attuale - ), - ); - - // Se l'utente ha premuto "Conferma" e non "Annulla" o tap fuori - if (result != null && context.mounted) { - context.read().updateEnergyOperations( - result, - ); - } - }, - ), - ActionCard( - label: "Finanziam.", - count: operation.finOperations.length, - icon: Icons.euro_symbol, - color: Colors.teal, - onTap: () async { - final result = await showDialog>( - context: context, - builder: (context) => FinanceOperationDialog( - productCubit: context.read(), - currentStoreId: operation.storeId, - initialOperations: operation - .finOperations, // Passiamo la lista attuale - ), - ); - - if (result != null && context.mounted) { - context.read().updateFinOperations( - result, - ); - } - }, - ), - ActionCard( - label: "Intratten.", - count: operation.entertainmentOperations.length, - icon: Icons.movie_filter_outlined, - color: Colors.purple, - onTap: () async { - final result = - await showDialog>( - context: context, - builder: (context) => EntertainmentOperationDialog( - initialOperations: - operation.entertainmentOperations, - currentStoreId: operation.storeId, - ), - ); - - if (result != null && context.mounted) { - context - .read() - .updateEntertainmentOperations(result); - } - }, - ), - ], - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/operations/ui/operations_screen.dart b/lib/features/operations/ui/operations_screen.dart index a56e73e..d2d5eed 100644 --- a/lib/features/operations/ui/operations_screen.dart +++ b/lib/features/operations/ui/operations_screen.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; -import 'package:flux/features/operations/utils/operation_actions.dart'; import 'package:go_router/go_router.dart'; // Importa i tuoi modelli e cubit @@ -139,15 +138,6 @@ class _OperationsScreenState extends State { ), ), ), - if (operation.isBozza) - const Chip( - label: Text( - "BOZZA", - style: TextStyle(fontSize: 10, color: Colors.white), - ), - backgroundColor: Colors.orange, - visualDensity: VisualDensity.compact, - ), ], ), subtitle: Column( @@ -155,21 +145,14 @@ class _OperationsScreenState extends State { children: [ const SizedBox(height: 4), Text( - "Pratica: ${operation.number} • ${operation.createdAt?.day}/${operation.createdAt?.month}/${operation.createdAt?.year}", + "Pratica: ${operation.reference} • ${operation.createdAt?.day}/${operation.createdAt?.month}/${operation.createdAt?.year}", ), const SizedBox(height: 8), - // I nostri mini-chip per i servizi attivati - Wrap( - spacing: 6, + Row( children: [ - if (operation.al > 0 || operation.mnp > 0) - _miniBadge("📞 Tel", Colors.blue), - if (operation.energyOperations.isNotEmpty) - _miniBadge("⚡ Energy", Colors.green), - if (operation.finOperations.isNotEmpty) - _miniBadge("💰 Fin", Colors.purple), - if (operation.entertainmentOperations.isNotEmpty) - _miniBadge("📺 Ent", Colors.red), + Text(operation.type), + const SizedBox(width: 8), + _buildOperationStatus(operation.status), ], ), ], @@ -187,22 +170,31 @@ class _OperationsScreenState extends State { ); } - Widget _miniBadge(String text, Color color) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(4), - border: Border.all(color: color.withValues(alpha: 0.5)), - ), - child: Text( - text, - style: TextStyle( - color: color, - fontSize: 10, - fontWeight: FontWeight.bold, - ), - ), + Widget _buildOperationStatus(OperationStatus status) { + Color color; + switch (status) { + case OperationStatus.canceled || OperationStatus.ko: + color = Colors.grey.shade800; + break; + case OperationStatus.waitingforaction || OperationStatus.draft: + color = Colors.orange; + break; + case OperationStatus.ok: + color = Colors.green; + break; + case OperationStatus.waitingfordeployment || + OperationStatus.waitingforsupport: + color = Colors.blue; + break; + } + return Chip( + label: Text("BOZZA", style: TextStyle(fontSize: 10, color: Colors.white)), + backgroundColor: color, + visualDensity: VisualDensity.compact, ); } + + void startNewOperation(BuildContext context) { + context.pushNamed('operation-form'); + } } diff --git a/lib/features/operations/utils/operation_actions.dart b/lib/features/operations/utils/operation_actions.dart deleted file mode 100644 index 35396ae..0000000 --- a/lib/features/operations/utils/operation_actions.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; -import 'package:go_router/go_router.dart'; - -/// Avvia la creazione di un nuovo servizio partendo dalla selezione dell'operatore. -void startNewOperation(BuildContext context) { - final session = context.read().state; - final currentStoreId = session.currentStore?.id; - - if (currentStoreId == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text("Seleziona uno store prima di iniziare")), - ); - return; - } - - showModalBottomSheet( - context: context, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - builder: (modalContext) { - // Usiamo lo StoreCubit invece dello StaffCubit! - return BlocBuilder( - builder: (context, storeState) { - // Recuperiamo lo staff assegnato a questo specifico store usando la mappa che avevi già creato - final storeStaff = storeState.staffByStore[currentStoreId] ?? []; - - return Container( - padding: const EdgeInsets.all(24), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - "Chi sta eseguendo l'operazione?", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 20), - - if (storeStaff.isEmpty) - const Text( - "Nessun membro dello staff configurato per questo store.\nVai in Anagrafica > Negozi per assegnare il personale.", - textAlign: TextAlign.center, - ), - - ...storeStaff.map( - (member) => ListTile( - leading: const CircleAvatar(child: Icon(Icons.person)), - title: Text(member.name), - onTap: () { - // 1. Inizializza il form nel Cubit - context.read().initOperationForm( - existingOperation: OperationModel( - storeId: currentStoreId, - employeeId: member.id, - number: '', - createdAt: DateTime.now(), - companyId: session.company!.id!, - ), - ); - - // 2. Chiudi la modal - Navigator.pop(modalContext); - - // 3. Naviga verso il form - context.pushNamed('operation-form'); - }, - ), - ), - const SizedBox(height: 16), - ], - ), - ); - }, - ); - }, - ); -} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb deleted file mode 100644 index 2a57a54..0000000 --- a/lib/l10n/app_en.arb +++ /dev/null @@ -1,13 +0,0 @@ -{ - "@@locale": "en", - "welcomeBack": "Welcome back, {name}! 👋", - "latestOperations": "Latest Operations", - "masterData": "Master Data", - "settings": "Settings", - "newOperation": "Operation", - "expiring_contracts": "Expiring Contracts", - "sticky_notes": "Sticky Notes", - "my_tasks": "My Tasks", - "latest_operation_tickets": "Latest operation tickets" - -} \ No newline at end of file diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index a211adf..3e195ea 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -86,5 +86,6 @@ "createCompanyScreenWillBeUsedForReceipts": "Verrà utilizzato per le tue stampe e ricevute", "createCompanyScreenSaveCompany": "SALVA AZIENDA", "createCompanyScreenSetupYourCompany": "Configura la tua Azienda", - "createCompanyScreenFluxNeedsYourFiscalData": "FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi." + "createCompanyScreenFluxNeedsYourFiscalData": "FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.", + "operationFormAttachmentSectionNoCustomer": "Devi prima selezionare un cliente" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 1a5f96f..707d88f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -441,6 +441,12 @@ abstract class AppLocalizations { /// In it, this message translates to: /// **'FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.'** String get createCompanyScreenFluxNeedsYourFiscalData; + + /// No description provided for @operationFormAttachmentSectionNoCustomer. + /// + /// In it, this message translates to: + /// **'Devi prima selezionare un cliente'** + String get operationFormAttachmentSectionNoCustomer; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index c226972..563bdce 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -199,4 +199,8 @@ class AppLocalizationsIt extends AppLocalizations { @override String get createCompanyScreenFluxNeedsYourFiscalData => 'FLUX ha bisogno dei tuoi dati fiscali per gestire correttamente le fatturazioni e le attivazioni dei tuoi negozi.'; + + @override + String get operationFormAttachmentSectionNoCustomer => + 'Devi prima selezionare un cliente'; } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb deleted file mode 100644 index 9e26dfe..0000000 --- a/lib/l10n/intl_en.arb +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 30588e2..3573306 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1035,7 +1035,7 @@ packages: source: hosted version: "3.1.5" uuid: - dependency: transitive + dependency: "direct main" description: name: uuid sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" diff --git a/pubspec.yaml b/pubspec.yaml index f511d7a..9a63d01 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,6 +28,7 @@ dependencies: qr_flutter: ^4.1.0 shared_preferences: ^2.5.5 supabase_flutter: ^2.12.2 + uuid: ^4.5.3 dev_dependencies: flutter_test: -- 2.43.0 From 67e8b8b6548b1d15e02adaa19376346ce84034a6 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sat, 2 May 2026 12:19:04 +0200 Subject: [PATCH 10/18] maremma maiala impestata, buonissima base dopo ultra refactor Co-authored-by: Copilot --- lib/core/routes/app_router.dart | 5 +- ...stomer_cubit.dart => customers_cubit.dart} | 28 +- ...stomer_state.dart => customers_state.dart} | 18 +- .../customers/ui/customer_search_sheet.dart | 202 ------ .../customers/ui/customers_content.dart | 14 +- .../customers/ui/quick_customer_dialog.dart | 18 +- .../operations/blocs/operations_cubit.dart | 32 +- .../models/energy_operation_model.dart | 72 -- .../models/entertainment_operation_model.dart | 77 --- .../models/fin_operation_model.dart | 63 -- .../operations/models/operation_model.dart | 7 + .../operations/ui/operation_form_screen.dart | 624 ++++++++++++++++++ .../ui/operation_form_screen/action_card.dart | 85 --- .../attachment_section.dart | 376 ----------- .../customer_section.dart | 96 --- .../energy_operation_dialog.dart | 417 ------------ .../entertainment_operation_card.dart | 393 ----------- .../finance_operation_dialog.dart | 479 -------------- .../general_info_section.dart | 65 -- .../ui/operation_form_screen/int_dialogs.dart | 158 ----- .../operation_form_screen.dart | 201 ------ .../operation_mobile_upload_screen.dart | 2 +- lib/main.dart | 4 +- 23 files changed, 706 insertions(+), 2730 deletions(-) rename lib/features/customers/blocs/{customer_cubit.dart => customers_cubit.dart} (84%) rename lib/features/customers/blocs/{customer_state.dart => customers_state.dart} (72%) delete mode 100644 lib/features/customers/ui/customer_search_sheet.dart delete mode 100644 lib/features/operations/models/energy_operation_model.dart delete mode 100644 lib/features/operations/models/entertainment_operation_model.dart delete mode 100644 lib/features/operations/models/fin_operation_model.dart create mode 100644 lib/features/operations/ui/operation_form_screen.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/action_card.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/attachment_section.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/customer_section.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/general_info_section.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/int_dialogs.dart delete mode 100644 lib/features/operations/ui/operation_form_screen/operation_form_screen.dart rename lib/features/operations/ui/{operation_form_screen => }/operation_mobile_upload_screen.dart (100%) diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 087125a..c577ed2 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -12,7 +12,6 @@ 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_mobile_upload_screen.dart'; import 'package:flux/features/customers/ui/customers_content.dart'; -import 'package:flux/features/home/latest_store_operations/bloc/latest_store_operations_bloc.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/products/ui/products_screen.dart'; @@ -23,8 +22,8 @@ import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; import 'package:flux/features/onboarding/ui/onboarding_screen.dart'; import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; import 'package:flux/features/operations/models/operation_model.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/operation_form_screen.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart'; +import 'package:flux/features/operations/ui/operation_form_screen.dart'; +import 'package:flux/features/operations/ui/operation_mobile_upload_screen.dart'; import 'package:flux/features/operations/ui/operations_screen.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/features/customers/blocs/customer_cubit.dart b/lib/features/customers/blocs/customers_cubit.dart similarity index 84% rename from lib/features/customers/blocs/customer_cubit.dart rename to lib/features/customers/blocs/customers_cubit.dart index fba9b4d..44e58d5 100644 --- a/lib/features/customers/blocs/customer_cubit.dart +++ b/lib/features/customers/blocs/customers_cubit.dart @@ -6,31 +6,31 @@ import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:get_it/get_it.dart'; -part 'customer_state.dart'; +part 'customers_state.dart'; -class CustomerCubit extends Cubit { +class CustomersCubit extends Cubit { final CustomerRepository _repository = GetIt.I(); final SessionCubit _sessionCubit = GetIt.I(); // Variabile per gestire il debounce della ricerca Timer? _searchDebounce; - CustomerCubit() : super(const CustomerState()); + CustomersCubit() : super(const CustomersState()); // --- LETTURA --- Future loadCustomers() async { - emit(state.copyWith(status: CustomerStatus.loading)); + emit(state.copyWith(status: CustomersStatus.loading)); try { final customers = await _repository.getCustomers( _sessionCubit.state.company!.id!, ); emit( - state.copyWith(status: CustomerStatus.success, customers: customers), + state.copyWith(status: CustomersStatus.success, customers: customers), ); } catch (e) { emit( state.copyWith( - status: CustomerStatus.failure, + status: CustomersStatus.failure, errorMessage: e.toString(), ), ); @@ -39,7 +39,7 @@ class CustomerCubit extends Cubit { // --- CREAZIONE --- Future createCustomer(CustomerModel customer) async { - emit(state.copyWith(status: CustomerStatus.loading)); + emit(state.copyWith(status: CustomersStatus.loading)); try { final newCustomer = await _repository.saveCustomer(customer); @@ -49,7 +49,7 @@ class CustomerCubit extends Cubit { emit( state.copyWith( - status: CustomerStatus.success, + status: CustomersStatus.success, customers: updatedList, lastCreatedCustomer: newCustomer, ), @@ -57,7 +57,7 @@ class CustomerCubit extends Cubit { } catch (e) { emit( state.copyWith( - status: CustomerStatus.failure, + status: CustomersStatus.failure, errorMessage: e.toString(), ), ); @@ -66,7 +66,7 @@ class CustomerCubit extends Cubit { // --- AGGIORNAMENTO --- Future updateCustomer(CustomerModel customer) async { - emit(state.copyWith(status: CustomerStatus.loading)); + emit(state.copyWith(status: CustomersStatus.loading)); try { final updatedCustomer = await _repository.updateCustomer(customer); @@ -79,7 +79,7 @@ class CustomerCubit extends Cubit { emit( state.copyWith( - status: CustomerStatus.success, + status: CustomersStatus.success, customers: updatedList, lastCreatedCustomer: updatedCustomer, // Utile se modifichi un cliente appena creato @@ -88,7 +88,7 @@ class CustomerCubit extends Cubit { } catch (e) { emit( state.copyWith( - status: CustomerStatus.failure, + status: CustomersStatus.failure, errorMessage: e.toString(), ), ); @@ -115,12 +115,12 @@ class CustomerCubit extends Cubit { query, ); emit( - state.copyWith(status: CustomerStatus.success, customers: results), + state.copyWith(status: CustomersStatus.success, customers: results), ); } catch (e) { emit( state.copyWith( - status: CustomerStatus.failure, + status: CustomersStatus.failure, errorMessage: e.toString(), ), ); diff --git a/lib/features/customers/blocs/customer_state.dart b/lib/features/customers/blocs/customers_state.dart similarity index 72% rename from lib/features/customers/blocs/customer_state.dart rename to lib/features/customers/blocs/customers_state.dart index da5ffec..453aaf7 100644 --- a/lib/features/customers/blocs/customer_state.dart +++ b/lib/features/customers/blocs/customers_state.dart @@ -1,6 +1,6 @@ -part of 'customer_cubit.dart'; +part of 'customers_cubit.dart'; -enum CustomerStatus { +enum CustomersStatus { initial, loading, filesLoading, @@ -9,26 +9,26 @@ enum CustomerStatus { failure, } -class CustomerState extends Equatable { - final CustomerStatus status; +class CustomersState extends Equatable { + final CustomersStatus status; final List customers; final CustomerModel? lastCreatedCustomer; final String? errorMessage; - const CustomerState({ - this.status = CustomerStatus.initial, + const CustomersState({ + this.status = CustomersStatus.initial, this.customers = const [], this.lastCreatedCustomer, this.errorMessage, }); - CustomerState copyWith({ - CustomerStatus? status, + CustomersState copyWith({ + CustomersStatus? status, List? customers, CustomerModel? lastCreatedCustomer, String? errorMessage, }) { - return CustomerState( + return CustomersState( status: status ?? this.status, customers: customers ?? this.customers, lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer, diff --git a/lib/features/customers/ui/customer_search_sheet.dart b/lib/features/customers/ui/customer_search_sheet.dart deleted file mode 100644 index d0a6bcc..0000000 --- a/lib/features/customers/ui/customer_search_sheet.dart +++ /dev/null @@ -1,202 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/customers/blocs/customer_cubit.dart'; -import 'package:flux/features/customers/models/customer_model.dart'; -import 'package:flux/features/customers/ui/quick_customer_dialog.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; - -class CustomerSearchSheet extends StatefulWidget { - const CustomerSearchSheet({super.key}); - - @override - State createState() => _CustomerSearchSheetState(); -} - -class _CustomerSearchSheetState extends State { - final TextEditingController _searchController = TextEditingController(); - - @override - void initState() { - super.initState(); - context.read().loadCustomers(); - } - - @override - void dispose() { - _searchController.dispose(); - super.dispose(); - } - - void _onSearchChanged(String query) { - context.read().searchCustomers(query); - } - - @override - Widget build(BuildContext context) { - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.85, - ), - child: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // --- HEADER --- - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Trova Cliente", - style: TextStyle(fontSize: 22, fontWeight: FontWeight.bold), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - tooltip: "Chiudi", - ), - ], - ), - const SizedBox(height: 16), - - // --- BARRA DI RICERCA --- - TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: "Cerca per nome, cognome o CF...", - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(12), - ), - filled: true, - fillColor: Theme.of( - context, - ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.3), - suffixIcon: IconButton( - icon: const Icon(Icons.clear), - onPressed: () { - _searchController.clear(); - _onSearchChanged(""); - }, - ), - ), - onChanged: _onSearchChanged, - ), - const SizedBox(height: 16), - - // --- TASTO NUOVO CLIENTE --- - SizedBox( - width: double.infinity, - child: IconButton( - icon: const Icon(Icons.person_add), - onPressed: () async { - final operationsCubit = context.read(); - // Apriamo la dialog passando la query attuale - final CustomerModel? nuovoCliente = await showDialog( - context: context, - builder: (context) => QuickCustomerDialog( - initialQuery: _searchController.text, - ), - ); - - if (nuovoCliente != null) { - operationsCubit.updateField( - customerId: nuovoCliente.id, - customerDisplayName: nuovoCliente.name, - ); - - setState(() { - _searchController.clear(); - }); - } - }, - ), - ), - const SizedBox(height: 24), - - // --- LISTA RISULTATI CON BLOC BUILDER --- - const Text( - "Risultati", - style: TextStyle(fontWeight: FontWeight.bold, color: Colors.grey), - ), - const SizedBox(height: 8), - - Expanded( - // AGGANCIO AL CUBIT REALE - child: BlocBuilder( - builder: (context, state) { - // 1. Stato di caricamento - if (state.status == CustomerStatus.loading) { - return const Center(child: CircularProgressIndicator()); - } - - // 2. Nessun risultato trovato - if (state.customers.isEmpty) { - return const Center( - child: Text( - "Nessun cliente trovato.\nProva a cambiare i termini di ricerca.", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - ); - } - - // 3. Mostriamo la lista vera - return ListView.separated( - itemCount: state.customers.length, - separatorBuilder: (context, index) => - const Divider(height: 1), - itemBuilder: (context, index) { - final customer = state.customers[index]; - // Assumo che il tuo CustomerModel abbia le proprietà name e surname. - // Adatta queste variabili al tuo modello reale! - final displayName = customer.name.trim(); - - return ListTile( - contentPadding: EdgeInsets.zero, - leading: CircleAvatar( - backgroundColor: Theme.of( - context, - ).colorScheme.primaryContainer, - foregroundColor: Theme.of( - context, - ).colorScheme.onPrimaryContainer, - // Mostra l'iniziale - child: Text( - displayName.isNotEmpty - ? displayName[0].toUpperCase() - : "?", - ), - ), - title: Text( - displayName, - style: const TextStyle(fontWeight: FontWeight.w500), - ), - subtitle: Text(customer.email), - trailing: const Icon( - Icons.check_circle_outline, - color: Colors.grey, - ), - onTap: () { - // Salviamo l'ID e il nome formattato nel form dei servizi - context.read().updateField( - customerId: customer.id, - customerDisplayName: displayName, - ); - - // Chiudiamo la modale - Navigator.pop(context); - }, - ); - }, - ); - }, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/customers/ui/customers_content.dart b/lib/features/customers/ui/customers_content.dart index 7021922..c52a9a9 100644 --- a/lib/features/customers/ui/customers_content.dart +++ b/lib/features/customers/ui/customers_content.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/theme/theme.dart'; -import 'package:flux/features/customers/blocs/customer_cubit.dart'; +import 'package:flux/features/customers/blocs/customers_cubit.dart'; import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/ui/customer_form.dart'; import 'package:go_router/go_router.dart'; @@ -26,14 +26,14 @@ class _CustomersContentState extends State { void _loadInitialCustomers() { final companyId = context.read().state.company?.id; if (companyId != null) { - context.read().loadCustomers(); + context.read().loadCustomers(); } } void _onSearch(String query) { final companyId = context.read().state.company?.id; if (companyId != null) { - context.read().searchCustomers(query); + context.read().searchCustomers(query); } } @@ -86,9 +86,9 @@ class _CustomersContentState extends State { // LISTA CLIENTI Expanded( - child: BlocBuilder( + child: BlocBuilder( builder: (context, state) { - if (state.status == CustomerStatus.loading && + if (state.status == CustomersStatus.loading && state.customers.isEmpty) { return const Center(child: CircularProgressIndicator()); } @@ -242,12 +242,12 @@ void openCustomerForm({ if (customer == null) { // CASO NUOVO: Iniettiamo il companyId e inviamo l'evento create - context.read().createCustomer( + context.read().createCustomer( customerFromForm.copyWith(companyId: companyId), ); } else { // CASO MODIFICA: L'ID e il companyId sono già nel modello - context.read().updateCustomer(customerFromForm); + context.read().updateCustomer(customerFromForm); } Navigator.pop(dialogContext); }, diff --git a/lib/features/customers/ui/quick_customer_dialog.dart b/lib/features/customers/ui/quick_customer_dialog.dart index 3082491..2137dee 100644 --- a/lib/features/customers/ui/quick_customer_dialog.dart +++ b/lib/features/customers/ui/quick_customer_dialog.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/customers/blocs/customer_cubit.dart'; +import 'package:flux/features/customers/blocs/customers_cubit.dart'; class QuickCustomerDialog extends StatefulWidget { final String initialQuery; @@ -42,13 +42,15 @@ class _QuickCustomerDialogState extends State { setState(() => _isLoading = true); // Chiamata al Cubit (aggiorna i parametri in base a come li hai definiti) - final newCustomer = await context.read().quickCreateCustomer( - name: _nameCtrl.text.trim(), - phone: _phoneCtrl.text.trim(), - // Aggiungi questi se li hai inseriti nel tuo CustomerCubit: - // email: _emailCtrl.text.trim(), - // note: _noteCtrl.text.trim(), - ); + final newCustomer = await context + .read() + .quickCreateCustomer( + name: _nameCtrl.text.trim(), + phone: _phoneCtrl.text.trim(), + // Aggiungi questi se li hai inseriti nel tuo CustomerCubit: + // email: _emailCtrl.text.trim(), + // note: _noteCtrl.text.trim(), + ); setState(() => _isLoading = false); diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index cfdf1a6..536f9eb 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -210,12 +210,40 @@ class OperationsCubit extends Cubit { .toList(); } - void updateField({String? customerId, String? customerDisplayName}) { + // --- GESTIONE DELLO STATO DEL FORM IN TEMPO REALE --- + void updateOperationFields({ + String? customerId, + String? customerDisplayName, + String? type, + String? providerId, + String? subtype, + DateTime? expirationDate, + int? quantity, + // Aggiungiamo questi flag per forzare la pulizia dei campi quando cambi tipo + bool clearProvider = false, + bool clearType = false, + bool clearSubtype = false, + bool clearExpiration = false, + }) { if (state.currentOperation == null) return; - final updated = state.currentOperation!.copyWith( + + final current = state.currentOperation!; + + // Creiamo il modello aggiornato + // ATTENZIONE: adatta questa logica in base a come è scritto il tuo copyWith! + final updated = current.copyWith( customerId: customerId, 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) + providerId: clearProvider ? null : (providerId ?? current.providerId), + // Idem per subtype e date. + // Se expirationDate è nullabile nel copyWith, dovresti poterlo gestire + quantity: quantity ?? current.quantity, ); + emit(state.copyWith(currentOperation: updated)); } } diff --git a/lib/features/operations/models/energy_operation_model.dart b/lib/features/operations/models/energy_operation_model.dart deleted file mode 100644 index 817d76e..0000000 --- a/lib/features/operations/models/energy_operation_model.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:equatable/equatable.dart'; - -enum EnergyType { luce, gas } // Mappa il tuo public.energy_type - -class EnergyOperationModel extends Equatable { - final String? id; - final DateTime? createdAt; - final EnergyType type; - final DateTime expiration; - final String providerId; - final String? operationId; - - const EnergyOperationModel({ - this.id, - this.createdAt, - required this.type, - required this.expiration, - required this.providerId, - this.operationId, - }); - - EnergyOperationModel copyWith({ - String? id, - DateTime? createdAt, - EnergyType? type, - DateTime? expiration, - String? providerId, - String? operationId, - }) { - return EnergyOperationModel( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - type: type ?? this.type, - expiration: expiration ?? this.expiration, - providerId: providerId ?? this.providerId, - operationId: operationId ?? this.operationId, - ); - } - - @override - List get props => [ - id, - createdAt, - type, - expiration, - providerId, - operationId, - ]; - - factory EnergyOperationModel.fromMap(Map map) { - return EnergyOperationModel( - id: map['id'], - createdAt: map['created_at'] != null - ? DateTime.parse(map['created_at']) - : null, - type: map['type'] == 'gas' ? EnergyType.gas : EnergyType.luce, - expiration: DateTime.parse(map['expiration']), - providerId: map['provider_id'], - operationId: map['operation_id'], - ); - } - - Map toMap() { - return { - if (id != null) 'id': id, - 'type': type.name, // .name trasforma l'enum in 'luce' o 'gas' - 'expiration': expiration.toIso8601String(), - 'provider_id': providerId, - 'operation_id': operationId, - }; - } -} diff --git a/lib/features/operations/models/entertainment_operation_model.dart b/lib/features/operations/models/entertainment_operation_model.dart deleted file mode 100644 index 49930b3..0000000 --- a/lib/features/operations/models/entertainment_operation_model.dart +++ /dev/null @@ -1,77 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class EntertainmentOperationModel extends Equatable { - final String? id; - final DateTime? createdAt; - final String type; // es. Sky, DAZN, ecc. - final bool constrained; // Vincolato? - final DateTime constrainExpiration; - final String? operationId; - final String? providerId; - - const EntertainmentOperationModel({ - this.id, - this.createdAt, - required this.type, - required this.constrained, - required this.constrainExpiration, - this.operationId, - this.providerId, - }); - - EntertainmentOperationModel copyWith({ - String? id, - DateTime? createdAt, - String? type, - bool? constrained, - DateTime? constrainExpiration, - String? operationId, - String? providerId, - }) { - return EntertainmentOperationModel( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - type: type ?? this.type, - constrained: constrained ?? this.constrained, - constrainExpiration: constrainExpiration ?? this.constrainExpiration, - operationId: operationId ?? this.operationId, - providerId: providerId ?? this.providerId, - ); - } - - @override - List get props => [ - id, - createdAt, - type, - constrained, - constrainExpiration, - operationId, - providerId, - ]; - - factory EntertainmentOperationModel.fromMap(Map map) { - return EntertainmentOperationModel( - id: map['id'], - createdAt: map['created_at'] != null - ? DateTime.parse(map['created_at']) - : null, - type: map['type'], - constrained: map['constrained'] ?? false, - constrainExpiration: DateTime.parse(map['constrain_expiration']), - operationId: map['operation_id'], - providerId: map['provider_id'], - ); - } - - Map toMap() { - return { - if (id != null) 'id': id, - 'type': type, - 'constrained': constrained, - 'constrain_expiration': constrainExpiration.toIso8601String(), - 'operation_id': operationId, - 'provider_id': providerId, - }; - } -} diff --git a/lib/features/operations/models/fin_operation_model.dart b/lib/features/operations/models/fin_operation_model.dart deleted file mode 100644 index d7bf513..0000000 --- a/lib/features/operations/models/fin_operation_model.dart +++ /dev/null @@ -1,63 +0,0 @@ -import 'package:equatable/equatable.dart'; - -class FinOperationModel extends Equatable { - final String? id; - final DateTime? createdAt; - final DateTime expiration; - final String? operationId; - final String? modelId; // FK verso model (es. iPhone, Samsung, ecc.) - final String? providerId; - - const FinOperationModel({ - this.id, - this.createdAt, - required this.expiration, - this.operationId, - this.modelId, - this.providerId, - }); - - FinOperationModel copyWith({ - String? id, - DateTime? createdAt, - DateTime? expiration, - String? operationId, - String? modelId, - String? providerId, - }) { - return FinOperationModel( - id: id ?? this.id, - createdAt: createdAt ?? this.createdAt, - expiration: expiration ?? this.expiration, - operationId: operationId ?? this.operationId, - modelId: modelId ?? this.modelId, - providerId: providerId ?? this.providerId, - ); - } - - @override - List get props => [id, createdAt, expiration, operationId, modelId]; - - factory FinOperationModel.fromMap(Map map) { - return FinOperationModel( - id: map['id'], - createdAt: map['created_at'] != null - ? DateTime.parse(map['created_at']) - : null, - expiration: DateTime.parse(map['expiration']), - operationId: map['operation_id'], - modelId: map['model_id'], - providerId: map['provider_id'], - ); - } - - Map toMap() { - return { - if (id != null) 'id': id, - 'expiration': expiration.toIso8601String(), - 'operation_id': operationId, - 'model_id': modelId, - 'provider_id': providerId, - }; - } -} diff --git a/lib/features/operations/models/operation_model.dart b/lib/features/operations/models/operation_model.dart index 7ffe874..727eb11 100644 --- a/lib/features/operations/models/operation_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -27,6 +27,7 @@ class OperationModel extends Equatable { final String? id; final DateTime? createdAt; final String type; + final String? subType; final String? providerId; final String? providerDisplayName; final String? modelId; @@ -55,6 +56,7 @@ class OperationModel extends Equatable { this.id, this.createdAt, this.type = '', + this.subType, this.providerId, this.providerDisplayName, this.modelId, @@ -82,6 +84,7 @@ class OperationModel extends Equatable { String? id, DateTime? createdAt, String? type, + String? subtype, String? providerId, String? providerDisplayName, String? modelId, @@ -107,6 +110,7 @@ class OperationModel extends Equatable { id: id ?? this.id, createdAt: createdAt ?? this.createdAt, type: type ?? this.type, + subType: subtype ?? this.subType, providerId: providerId ?? this.providerId, providerDisplayName: providerDisplayName ?? this.providerDisplayName, modelId: modelId ?? this.modelId, @@ -135,6 +139,7 @@ class OperationModel extends Equatable { id, createdAt, type, + subType, providerId, providerDisplayName, modelId, @@ -169,6 +174,7 @@ class OperationModel extends Equatable { ? DateTime.parse(map['created_at']) : null, type: map['type'] as String? ?? '', + subType: map['sub_type'] as String?, providerId: map['provider_id'] as String? ?? '', providerDisplayName: "${map['provider']['name']}".myFormat(), modelId: map['model_id'] as String? ?? '', @@ -206,6 +212,7 @@ class OperationModel extends Equatable { return { if (id != null) 'id': id, 'type': type, + 'sub_type': subType, 'provider_id': providerId, 'model_id': modelId, 'description': description, diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart new file mode 100644 index 0000000..6da630a --- /dev/null +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -0,0 +1,624 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/features/customers/blocs/customers_cubit.dart'; +import 'package:flux/features/operations/blocs/operations_cubit.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +// import 'package:flux/features/attachments/ui/operation_files_section.dart'; + +class OperationFormScreen extends StatefulWidget { + final String? operationId; + final OperationModel? existingOperation; + + const OperationFormScreen({ + super.key, + this.operationId, + this.existingOperation, + }); + + @override + State createState() => _OperationFormScreenState(); +} + +class _OperationFormScreenState extends State { + final _formKey = GlobalKey(); + + // TEXT CONTROLLERS (Unici detentori di stato locale per evitare lag) + final _referenceController = TextEditingController(); + final _noteController = TextEditingController(); + final _customSubtypeController = TextEditingController(); + + final List _availableTypes = [ + 'AL', + 'MNP', + 'NIP', + 'UNICA', + 'TELEPASS', + 'Energy', + 'Fin', + 'Entertainment', + 'Custom', + ]; + + bool _isInitialized = false; + + @override + void initState() { + super.initState(); + // Inizializziamo il form nel Cubit + context.read().initOperationForm( + existingOperation: widget.existingOperation, + operationId: widget.operationId, + ); + } + + @override + void dispose() { + _referenceController.dispose(); + _noteController.dispose(); + _customSubtypeController.dispose(); + super.dispose(); + } + + // Sincronizza SOLO i testi liberi quando il Cubit ha caricato da DB + void _syncTextControllers(OperationModel model) { + if (_referenceController.text.isEmpty && model.reference.isNotEmpty) { + _referenceController.text = model.reference; + } + if (_noteController.text.isEmpty && model.note.isNotEmpty) { + _noteController.text = model.note; + } + _isInitialized = true; + } + + // --- LOGICA DI SALVATAGGIO --- + void _saveOperation({required bool keepAdding}) { + if (_formKey.currentState!.validate()) { + final cubit = context.read(); + final currentOperation = cubit.state.currentOperation!; + + // 1. "Travasiamo" i testi liberi dai controller al Modello prima di salvare + final operationToSave = currentOperation.copyWith( + reference: _referenceController.text, + note: _noteController.text, + // subtype: currentOperation.type == 'Custom' ? _customSubtypeController.text : currentOperation.subtype, // <-- Scommenta quando aggiungi subtype + ); + + // 2. Aggiorniamo il Cubit con i testi + cubit.initOperationForm(existingOperation: operationToSave); + + // 3. Salviamo! + cubit.saveCurrentOperation( + targetStatus: OperationStatus.ok, + shouldPop: !keepAdding, + ); + } + } + + // --- MODALE SELEZIONE CLIENTE --- + void _showCustomerModal() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (modalContext) { + return DraggableScrollableSheet( + initialChildSize: 0.8, + minChildSize: 0.5, + maxChildSize: 0.95, + expand: false, + builder: (_, scrollController) { + return Column( + children: [ + // Header + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Seleziona Cliente', + style: Theme.of(context).textTheme.titleLarge, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(modalContext), + ), + ], + ), + ), + // Barra di Ricerca + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: TextField( + decoration: InputDecoration( + hintText: 'Cerca per nome, telefono o email...', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: (query) { + // Evento di ricerca (usa debouncer nel cubit!) + // context.read().searchCustomers(query); + }, + ), + ), + // Pulsante Nuovo Cliente + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + icon: const Icon(Icons.person_add), + label: const Text('Crea Nuovo Cliente'), + onPressed: () { + // Apri form nuovo cliente... + }, + ), + ), + const Divider(), + // Lista Clienti dal Bloc + Expanded( + child: BlocBuilder( + builder: (context, state) { + /* Decommenta e adatta al tuo CustomersState + if (state.status == CustomersStatus.loading) { + return const Center(child: CircularProgressIndicator()); + } + if (state.customers.isEmpty) { + return const Center(child: Text('Nessun cliente trovato.', style: TextStyle(color: Colors.grey))); + } + */ + return ListView.builder( + controller: scrollController, + itemCount: 10, // Sostituisci con state.customers.length + itemBuilder: (context, index) { + // final customer = state.customers[index]; + return ListTile( + leading: const CircleAvatar( + child: Icon(Icons.person), + ), + title: Text( + 'Cliente $index', + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), // Sostituisci con customer.name + subtitle: const Text( + '333 1234567', + ), // Sostituisci con customer.phoneNumber + onTap: () { + // Aggiorniamo il form tramite il Cubit delle operazioni + context + .read() + .updateOperationFields( + customerId: + 'id_del_cliente_$index', // customer.id + customerDisplayName: + 'Cliente $index', // customer.name + ); + Navigator.pop(modalContext); + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return BlocConsumer( + listenWhen: (previous, current) => + previous.status != current.status || + previous.currentOperation?.id != current.currentOperation?.id, + listener: (context, state) { + // Sincronizzazione iniziale + if (state.status == OperationsStatus.ready && + state.currentOperation != null && + !_isInitialized) { + _syncTextControllers(state.currentOperation!); + } + + if (state.status == OperationsStatus.saved) { + Navigator.of(context).pop(); + } else if (state.status == OperationsStatus.savedNoPop) { + context.read().prepareNextOperationInBatch(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Servizio aggiunto! Inserisci il prossimo.'), + ), + ); + // Ripuliamo SOLO i testi liberi (il Cubit gestisce già i suoi reset) + _referenceController.clear(); + _noteController.clear(); + _customSubtypeController.clear(); + } else if (state.status == OperationsStatus.failure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.errorMessage ?? 'Errore'), + backgroundColor: theme.colorScheme.error, + ), + ); + } + }, + builder: (context, state) { + // Loader iniziale + if (!_isInitialized && + (widget.operationId != null || widget.existingOperation != null) && + state.status == OperationsStatus.loading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + return Scaffold( + appBar: AppBar( + title: Text( + state.currentOperation?.id == null + ? 'Nuova Pratica' + : 'Modifica Pratica', + ), + ), + body: Form( + key: _formKey, + child: LayoutBuilder( + builder: (context, constraints) { + final isDesktop = constraints.maxWidth > 900; + + if (isDesktop) { + // --- LAYOUT DESKTOP --- + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + flex: 7, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: _buildMainFormContent(theme, state), + ), + ), + VerticalDivider(width: 1, color: theme.dividerColor), + Expanded( + flex: 3, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: _buildNotesSection(isDesktop: true), + ), + ), + ], + ); + } else { + // --- LAYOUT MOBILE --- + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildMainFormContent(theme, state), + const Divider(height: 32), + _buildNotesSection(isDesktop: false), + const SizedBox(height: 80), + ], + ), + ); + } + }, + ), + ), + // --- LA CASSA --- + bottomNavigationBar: SafeArea( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Expanded( + flex: 1, + child: OutlinedButton( + onPressed: state.status == OperationsStatus.saving + ? null + : () => _saveOperation(keepAdding: true), + child: const Text( + 'Salva e Aggiungi Altro', + textAlign: TextAlign.center, + ), + ), + ), + const SizedBox(width: 12), + Expanded( + flex: 1, + child: ElevatedButton( + onPressed: state.status == OperationsStatus.saving + ? null + : () => _saveOperation(keepAdding: false), + child: state.status == OperationsStatus.saving + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Text('Salva ed Esci'), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + // --- COSTRUTTORI UI COMPONENTI --- + + Widget _buildMainFormContent(ThemeData theme, OperationsState state) { + final currentOp = state.currentOperation; + final currentType = currentOp?.type ?? 'AL'; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // --- BLOCCO 1: CONTESTO --- + _buildSectionTitle('Cliente & Riferimento'), + _buildCustomerSelector(currentOp), + const SizedBox(height: 16), + TextFormField( + controller: _referenceController, + decoration: const InputDecoration( + labelText: 'Riferimento (es. numero di telefono, targa...)', + prefixIcon: Icon(Icons.tag), + ), + ), + const Divider(height: 32), + + // --- BLOCCO 2: TIPO DI OPERAZIONE --- + _buildSectionTitle('Cosa stiamo facendo?'), + Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: _availableTypes.map((type) { + return ChoiceChip( + label: Text(type), + selected: currentType == type, + onSelected: (selected) { + if (selected) { + // Diciamo al Cubit di cambiare tipo e spianare i campi dipendenti + context.read().updateOperationFields( + type: type, + clearProvider: true, + clearSubtype: true, + clearExpiration: true, + ); + } + }, + ); + }).toList(), + ), + const Divider(height: 32), + + // --- BLOCCO 3: DETTAGLI REATTIVI --- + _buildSectionTitle('Dettagli Servizio'), + + // PROVIDER (Mostrato quasi sempre) + ListTile( + title: const Text('Seleziona Gestore'), + subtitle: Text( + currentOp?.providerId ?? 'Nessun gestore selezionato', + ), // Adatta se hai displayName + trailing: const Icon(Icons.arrow_drop_down), + shape: RoundedRectangleBorder( + side: BorderSide(color: theme.dividerColor), + borderRadius: BorderRadius.circular(8), + ), + onTap: () { + // TODO: Modale o Dropdown Provider + }, + ), + const SizedBox(height: 16), + + // SOTTO-TIPO (Reattivo) + if (['Energy', 'Fin', 'Entertainment'].contains(currentType)) ...[ + DropdownButtonFormField( + value: + null, // Sostituisci con currentOp?.subtype quando lo aggiungi + decoration: const InputDecoration( + labelText: 'Dettaglio (es. Luce, Gas...)', + ), + items: [ + 'Luce', + 'Gas', + 'Dual', + ].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), + onChanged: (val) { + // context.read().updateOperationFields(subtype: val); + }, + ), + const SizedBox(height: 16), + ], + + // SOTTO-TIPO CUSTOM (Reattivo) + if (currentType == 'Custom') ...[ + TextFormField( + controller: _customSubtypeController, + decoration: const InputDecoration( + labelText: 'Specifica il servizio (es. Monopattino)', + ), + ), + const SizedBox(height: 16), + ], + + // SCADENZA (Reattivo) + if ([ + 'Energy', + 'Fin', + 'Entertainment', + 'Custom', + ].contains(currentType)) ...[ + ListTile( + title: const Text('Data di Scadenza'), + subtitle: Text( + currentOp?.expirationDate?.toLocal().toString().split(' ')[0] ?? + 'Nessuna scadenza', + ), + trailing: const Icon(Icons.calendar_today), + shape: RoundedRectangleBorder( + side: BorderSide(color: theme.dividerColor), + borderRadius: BorderRadius.circular(8), + ), + onTap: () async { + final date = await showDatePicker( + context: context, + initialDate: DateTime.now().add(const Duration(days: 365)), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 3650)), + ); + if (date != null) { + context.read().updateOperationFields( + expirationDate: date, + ); + } + }, + ), + const SizedBox(height: 16), + ], + + // QUANTITÀ + Row( + children: [ + const Text('Quantità: '), + IconButton( + icon: const Icon(Icons.remove), + onPressed: () { + final q = currentOp?.quantity ?? 1; + if (q > 1) + context.read().updateOperationFields( + quantity: q - 1, + ); + }, + ), + Text( + '${currentOp?.quantity ?? 1}', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + final q = currentOp?.quantity ?? 1; + context.read().updateOperationFields( + quantity: q + 1, + ); + }, + ), + ], + ), + const Divider(height: 32), + + // --- BLOCCO 5: ALLEGATI --- + _buildSectionTitle('Documenti & Foto'), + const Center( + child: Text( + "Widget File in arrivo...", + style: TextStyle(color: Colors.grey), + ), + ), + ], + ); + } + + Widget _buildNotesSection({required bool isDesktop}) { + final title = _buildSectionTitle('Note Interne'); + final noteField = TextFormField( + controller: _noteController, + keyboardType: TextInputType.multiline, + minLines: isDesktop ? null : 5, + maxLines: null, + expands: isDesktop, + textAlignVertical: TextAlignVertical.top, + decoration: const InputDecoration( + hintText: 'Incolla qui seriali, ICCID, IBAN, indirizzi...', + alignLabelWithHint: true, + border: OutlineInputBorder(), + ), + ); + + if (isDesktop) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title, + const SizedBox(height: 8), + Expanded(child: noteField), + ], + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [title, const SizedBox(height: 8), noteField], + ); + } + } + + Widget _buildSectionTitle(String title) { + return Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Text( + title, + style: Theme.of( + context, + ).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + ), + ); + } + + Widget _buildCustomerSelector(OperationModel? currentOp) { + final hasCustomer = + currentOp?.customerId != null && currentOp!.customerId!.isNotEmpty; + + return InkWell( + onTap: _showCustomerModal, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).colorScheme.primary), + borderRadius: BorderRadius.circular(8), + color: Theme.of( + context, + ).colorScheme.primaryContainer.withValues(alpha: 0.2), + ), + child: Row( + children: [ + const Icon(Icons.person), + const SizedBox(width: 12), + Expanded( + child: Text( + hasCustomer + ? currentOp.customerDisplayName ?? '' + : 'Seleziona Cliente *', + style: TextStyle( + fontWeight: hasCustomer ? FontWeight.bold : FontWeight.normal, + color: hasCustomer ? null : Colors.grey, + ), + ), + ), + const Icon(Icons.search), + ], + ), + ), + ); + } +} diff --git a/lib/features/operations/ui/operation_form_screen/action_card.dart b/lib/features/operations/ui/operation_form_screen/action_card.dart deleted file mode 100644 index 51f0b42..0000000 --- a/lib/features/operations/ui/operation_form_screen/action_card.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; - -class ActionCard extends StatelessWidget { - final String label; - final int count; - final IconData icon; - final Color color; - final VoidCallback onTap; - - const ActionCard({ - super.key, - required this.label, - required this.count, - required this.icon, - required this.color, - required this.onTap, - }); - - @override - Widget build(BuildContext context) { - final isActive = count > 0; - - return InkWell( - onTap: onTap, - borderRadius: BorderRadius.circular(12), - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 110, // Larghezza fissa per avere una griglia ordinata - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8), - decoration: BoxDecoration( - color: isActive - ? color.withValues(alpha: 0.15) - : Theme.of(context).cardColor, - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: isActive ? color : Colors.grey.withValues(alpha: 0.3), - width: isActive ? 2 : 1, - ), - boxShadow: isActive - ? [ - BoxShadow( - color: color.withValues(alpha: 0.2), - blurRadius: 8, - spreadRadius: 1, - ), - ] - : [], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, color: isActive ? color : Colors.grey, size: 28), - const SizedBox(height: 8), - Text( - label, - style: TextStyle( - fontWeight: isActive ? FontWeight.bold : FontWeight.normal, - color: isActive ? color : Colors.grey.shade700, - ), - textAlign: TextAlign.center, - ), - if (isActive) ...[ - const SizedBox(height: 4), - Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(12), - ), - child: Text( - count.toString(), - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 12, - ), - ), - ), - ], - ], - ), - ), - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/attachment_section.dart b/lib/features/operations/ui/operation_form_screen/attachment_section.dart deleted file mode 100644 index 7933045..0000000 --- a/lib/features/operations/ui/operation_form_screen/attachment_section.dart +++ /dev/null @@ -1,376 +0,0 @@ -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/core/widgets/image_viewer_widget.dart'; -import 'package:flux/core/widgets/pdf_viewer_widget.dart'; -import 'package:flux/core/widgets/qr_upload_dialog.dart'; -import 'package:flux/features/attachments/models/attachment_model.dart'; -import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; - -class AttachmentsSection extends StatelessWidget { - const AttachmentsSection({super.key}); - - Future _pickFiles(BuildContext context) async { - // Usiamo withData: true fondamentale per avere i bytes e caricare su Supabase Storage - FilePickerResult? result = await FilePicker.pickFiles( - allowMultiple: true, - type: FileType.custom, - allowedExtensions: ['pdf', 'jpg', 'jpeg', 'png'], - withData: true, - ); - - if (result != null && context.mounted) { - context.read().add( - AddOperationFilesEvent(result.files), - ); - } - } - - @override - Widget build(BuildContext context) { - OperationFilesBloc operationFilesBloc = BlocProvider.of( - context, - ); - - return BlocListener( - listenWhen: (previous, current) => - previous.currentOperation?.id == null && - current.currentOperation?.id != null, - listener: (context, state) { - // FIGASSA! La pratica è stata salvata e ora ha un ID. - // Diciamo al Bloc dei file di agganciarsi al database. - final newId = state.currentOperation!.id!; - context.read().add(OperationsavedEvent(newId)); - }, - child: BlocBuilder( - builder: (context, state) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // --- HEADER SEZIONE --- - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "DOCUMENTI ALLEGATI", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - letterSpacing: 1.2, - ), - ), - Row( - children: [ - OutlinedButton.icon( - icon: const Icon(Icons.attach_file), - label: const Text("Aggiungi File"), - onPressed: () => _pickFiles(context), - ), - if (!context - .read() - .state - .isMobileDevice) ...[ - const SizedBox(width: 12), - ElevatedButton.icon( - onPressed: () => _handleGenerateQr(context), - icon: const Icon(Icons.qr_code), - label: const Text("GENERA QR"), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.1), - foregroundColor: Theme.of( - context, - ).colorScheme.primary, - elevation: 0, - ), - ), - ], - ], - ), - ], - ), - const SizedBox(height: 12), - - // --- LISTA VUOTA --- - if (state.allFiles.isEmpty) // Furbata: usiamo allFiles.isEmpty! - Container( - width: double.infinity, - padding: const EdgeInsets.all(24), - decoration: BoxDecoration( - border: Border.all( - color: Colors.grey.shade300, - style: BorderStyle.solid, - ), - borderRadius: BorderRadius.circular(8), - color: Colors.grey.shade50, - ), - child: const Text( - "Nessun documento allegato alla bozza.", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - ) - // --- LISTA PIENA --- - else ...[ - ListView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: state.allFiles.length, - itemBuilder: (context, index) { - final file = state.allFiles[index]; - final sizeMb = (file.fileSize / (1024 * 1024)) - .toStringAsFixed(2); - final isPdf = file.extension.toLowerCase() == 'pdf'; - final isSelected = state.selectedFiles.contains(file); - - return GestureDetector( - onTap: () => operationFilesBloc.add( - ToggleOperationFileSelectionEvent(file), - ), - onDoubleTap: () => _handleDoubleClick(context, file), - child: Card( - margin: const EdgeInsets.only(bottom: 8), - elevation: 0, - // UX Fina: cambiamo colore del bordo se selezionato - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: BorderSide( - color: isSelected - ? Theme.of(context).colorScheme.primary - : Colors.grey.shade300, - width: isSelected ? 2 : 1, - ), - ), - // UX Fina: Sfondo leggermente colorato se selezionato - color: isSelected - ? Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.05) - : Theme.of(context).colorScheme.surface, - child: ListTile( - leading: Icon( - isSelected - ? Icons.check_box - : Icons.check_box_outline_blank, - color: Theme.of(context).colorScheme.primary, - size: 32, - ), - title: Text( - file.name, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - file.isLocal ? "$sizeMb MB • (Nuovo)" : " MB", - ), - trailing: Icon( - isPdf ? Icons.picture_as_pdf : Icons.image, - color: isPdf ? Colors.red : Colors.blue, - size: 32, - ), - ), - ), - ); - }, - ), - // --- PANNELLO AZIONI CONTESTUALI (LA MAGIA) --- - // Appare SOLO se c'è almeno un file selezionato - if (state.selectedFiles.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.1), - borderRadius: BorderRadius.circular(12), - border: Border.all( - color: Theme.of( - context, - ).colorScheme.primary.withValues(alpha: 0.3), - ), - ), - child: Row( - children: [ - // Contatore - Text( - "${state.selectedFiles.length} file selezionati", - style: TextStyle( - fontWeight: FontWeight.bold, - color: Theme.of(context).colorScheme.primary, - ), - ), - const Spacer(), - - // Bottone Elimina - TextButton.icon( - style: TextButton.styleFrom( - foregroundColor: Colors.red, - ), - icon: const Icon(Icons.delete_outline), - label: const Text("Elimina"), - onPressed: () { - // Qui lancerai l'evento per eliminare i file selezionati! - // Es: operationFilesBloc.add(DeleteSelectedFilesEvent()); - }, - ), - const SizedBox(width: 8), - - // Bottone Copia - ElevatedButton.icon( - icon: const Icon(Icons.copy), - label: const Text("Copia in Cliente"), - onPressed: () { - final cubit = context.read(); - if (cubit.state.currentOperation?.customerId == - null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - context - .l10n - .operationFormAttachmentSectionNoCustomer, - ), - ), - ); - } else { - context.read().add( - LinkFilesToCustomerEvent( - customerId: cubit - .state - .currentOperation! - .customerId!, - ), - ); - } - }, - ), - ], - ), - ), - ), - ], - ], - ); - }, - ), - ); - } - - Future _handleGenerateQr(BuildContext context) async { - final cubit = context.read(); - var currentOperation = cubit.state.currentOperation; - - // 1. CATTURIAMO IL BLOC MENTRE SIAMO ANCORA NELLA PAGINA - final operationFilesBloc = context.read(); - - // 2. SE LA PRATICA E' NUOVA (Manca l'ID) - if (currentOperation == null || currentOperation.id == null) { - // NIENTE BlocListener qui! Solo un semplice Dialog di conferma - final bool? confirm = await showDialog( - context: context, - builder: (ctx) => AlertDialog( - title: const Text("Salvataggio Necessario"), - content: const Text( - "Per generare il QR Code e caricare file dal telefono, la pratica deve essere prima salvata in BOZZA.\n\nVuoi salvare ora?", - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(ctx, false), - child: const Text("Annulla"), - ), - ElevatedButton( - onPressed: () => Navigator.pop(ctx, true), - child: const Text("Salva in Bozza"), - ), - ], - ), - ); - - if (confirm != true) return; // Utente ha annullato - - // Salviamo forzatamente in bozza - await cubit.saveCurrentOperation( - targetStatus: OperationStatus.draft, - shouldPop: false, - ); - - // Recuperiamo il servizio aggiornato con l'ID! - currentOperation = cubit.state.currentOperation; - - if (currentOperation?.id == null) return; - } - - // 3. MOSTRIAMO IL QR CODE (Con il Ponte e l'Auto-Chiusura!) - if (context.mounted) { - final nomePratica = - "Pratica ${currentOperation?.customerDisplayName ?? ''}".trim(); - - showDialog( - context: context, - builder: (dialogContext) => BlocProvider.value( - // INIETTIAMO IL BLOC NEL CONTESTO DEL DIALOG ALIENO - value: operationFilesBloc, - - // ORA METTIAMO L'AUTO-CHIUSURA SUL QR CODE! - child: BlocListener( - listener: (context, state) { - // Se arrivano file remoti e lo stato è success, chiudiamo il QR! - // (Nota: usiamo dialogContext per assicurarci di chiudere il popup giusto) - if (state.status == OperationFilesStatus.success && - state.remoteFiles.isNotEmpty) { - Navigator.of(dialogContext).pop(); - } - }, - child: QrUploadDialog( - deepLinkUrl: - 'fluxapp:///operation/${currentOperation!.id}/upload?name=${Uri.encodeComponent(nomePratica)}', - title: 'Scatta per\n$nomePratica', - ), - ), - ), - ); - } - } - - // --- LOGICA DI VISUALIZZAZIONE OVERLAY --- - void _handleDoubleClick(BuildContext context, AttachmentModel file) { - showDialog( - context: context, - barrierDismissible: true, - builder: (ctx) => Dialog( - insetPadding: const EdgeInsets.all(16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: ClipRRect( - borderRadius: BorderRadius.circular(12), - child: SizedBox( - width: double.infinity, - height: MediaQuery.of(context).size.height * 0.8, - child: file.isPdf - ? PdfViewerWidget( - storagePath: file.storagePath.isNotEmpty - ? file.storagePath - : null, - bytes: file.localBytes, - ) - : ImageViewerWidget( - storagePath: file.storagePath.isNotEmpty - ? file.storagePath - : null, - bytes: file.localBytes, - ), - ), - ), - ), - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/customer_section.dart b/lib/features/operations/ui/operation_form_screen/customer_section.dart deleted file mode 100644 index b3546db..0000000 --- a/lib/features/operations/ui/operation_form_screen/customer_section.dart +++ /dev/null @@ -1,96 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flux/features/customers/ui/customer_search_sheet.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; - -class CustomerSection extends StatelessWidget { - final OperationModel operation; - - const CustomerSection({super.key, required this.operation}); - - void _openCustomerSearch(BuildContext context) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - ), - builder: (modalContext) { - return Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(modalContext).viewInsets.bottom, - ), - // La modale di ricerca - child: const CustomerSearchSheet(), - ); - }, - ); - } - - @override - Widget build(BuildContext context) { - // Niente BlocBuilder qui! Leggiamo solo la variabile 'operation' - final hasCustomer = operation.customerId != null; - - return Card( - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.person, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text( - "Dati Cliente", - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - const SizedBox(height: 16), - - if (!hasCustomer) - Center( - child: ElevatedButton.icon( - onPressed: () => _openCustomerSearch(context), - icon: const Icon(Icons.search), - label: const Text("Seleziona o Crea Cliente"), - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 24, - vertical: 12, - ), - ), - ), - ) - else - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - operation.customerDisplayName ?? "Cliente Selezionato", - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - ), - TextButton.icon( - onPressed: () => _openCustomerSearch(context), - icon: const Icon(Icons.edit, size: 18), - label: const Text("Cambia"), - ), - ], - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart b/lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart deleted file mode 100644 index f44dd40..0000000 --- a/lib/features/operations/ui/operation_form_screen/energy_operation_dialog.dart +++ /dev/null @@ -1,417 +0,0 @@ -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/operations/models/energy_operation_model.dart'; // Assicurati degli import - -class EnergyOperationDialog extends StatefulWidget { - final List initialOperations; - final String - currentStoreId; // Ci serve per sapere per quale negozio caricare i gestori - - const EnergyOperationDialog({ - super.key, - required this.initialOperations, - required this.currentStoreId, - }); - - @override - State createState() => _EnergyOperationDialogState(); -} - -class _EnergyOperationDialogState extends State { - // Lista temporanea per non "sporcare" il cubit finché non si preme Conferma - late List _tempList; - bool _isAddingNew = false; - - @override - void initState() { - super.initState(); - _tempList = List.from(widget.initialOperations); - // Al caricamento della modale, chiediamo al Cubit di recuperare i gestori veri! - context.read().loadActiveProvidersForStore( - widget.currentStoreId, - ); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Row( - children: [ - Icon(Icons.bolt, color: Theme.of(context).colorScheme.primary), - const SizedBox(width: 8), - Text(_isAddingNew ? "Nuovo Contratto" : "Servizi Energia"), - ], - ), - content: AnimatedSize( - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOut, - child: SizedBox( - width: double.maxFinite, - // Cambia vista in base al flag - child: _isAddingNew - ? _EnergyForm( - onSave: (newOperation) { - setState(() { - _tempList.add(newOperation); - _isAddingNew = false; // Torna alla lista - }); - }, - onCancel: () { - setState(() => _isAddingNew = false); - }, - ) - : _EnergyList( - operations: _tempList, - onDelete: (index) { - setState(() => _tempList.removeAt(index)); - }, - onAddTap: () { - setState(() => _isAddingNew = true); // Passa al form - }, - activeProviders: [ - // Passiamo i provider attivi filtrati per tipo Energia - ...context - .read() - .state - .activeProviders - .where((p) => p.energia == true), - ], - ), - ), - ), - actions: [ - if (!_isAddingNew) ...[ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("Annulla"), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context, _tempList), - child: const Text("Conferma Tutti"), - ), - ], - ], - ); - } -} - -// ========================================== -// VISTA 1: LA LISTA DEI CONTRATTI -// ========================================== -class _EnergyList extends StatelessWidget { - final List operations; - final List - activeProviders; // <--- NUOVO: La lista vera dal Cubit - final Function(int) onDelete; - final VoidCallback onAddTap; - - const _EnergyList({ - required this.operations, - required this.activeProviders, // <--- Richiesto - required this.onDelete, - required this.onAddTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (operations.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Text( - "Nessun contratto energia inserito.", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - ) - else - Flexible( - child: ListView.separated( - shrinkWrap: true, - itemCount: operations.length, - separatorBuilder: (_, _) => const Divider(height: 1), - itemBuilder: (context, index) { - final s = operations[index]; - final isLuce = s.type == EnergyType.luce; - - // LA MAGIA: Troviamo il nome partendo dall'ID salvato nel servizio - final providerIndex = activeProviders.indexWhere( - (p) => p.id == s.providerId, - ); - final providerName = providerIndex >= 0 - ? (activeProviders[providerIndex].nome) - : 'Gestore Rimosso/Sconosciuto'; - - // Formattazione data pulita (es. 04/09/2025) - final day = s.expiration.day.toString().padLeft(2, '0'); - final month = s.expiration.month.toString().padLeft(2, '0'); - final formattedDate = "$day/$month/${s.expiration.year}"; - - return ListTile( - contentPadding: EdgeInsets.zero, - leading: CircleAvatar( - backgroundColor: isLuce - ? Colors.orange.shade100 - : Colors.blue.shade100, - child: Icon( - isLuce - ? Icons.lightbulb_outline - : Icons.local_fire_department, - color: isLuce ? Colors.orange : Colors.blue, - ), - ), - title: Text( - providerName, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text("Scadenza: $formattedDate"), - trailing: IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red), - onPressed: () => onDelete(index), - ), - ); - }, - ), - ), - const SizedBox(height: 16), - OutlinedButton.icon( - onPressed: onAddTap, - icon: const Icon(Icons.add), - label: const Text("Aggiungi Contratto"), - ), - ], - ); - } -} - -// ========================================== -// VISTA 2: IL FORM DI INSERIMENTO -// ========================================== -class _EnergyForm extends StatefulWidget { - final Function(EnergyOperationModel) onSave; - final VoidCallback onCancel; - - const _EnergyForm({required this.onSave, required this.onCancel}); - - @override - State<_EnergyForm> createState() => _EnergyFormState(); -} - -class _EnergyFormState extends State<_EnergyForm> { - EnergyType _selectedType = EnergyType.luce; - String? _selectedProviderId; - DateTime? _selectedExpiration; - int? _selectedMonthsPreset; - - void _applyPreset(int? months) { - if (months == null) return; - setState(() { - _selectedMonthsPreset = months; - // Calcoliamo la data: oggi + X mesi - final now = DateTime.now(); - _selectedExpiration = DateTime(now.year, now.month + months, now.day); - }); - } - - Future _pickDate() async { - final picked = await showDatePicker( - context: context, - initialDate: DateTime.now().add( - const Duration(days: 365), - ), // Default 1 anno - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 365 * 10)), - ); - if (picked != null) { - setState(() => _selectedExpiration = picked); - } - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 1. Tipo (Luce o Gas) - Segmented Button stile M3 - SegmentedButton( - segments: const [ - ButtonSegment( - value: EnergyType.luce, - label: Text("Luce"), - icon: Icon(Icons.lightbulb_outline), - ), - ButtonSegment( - value: EnergyType.gas, - label: Text("Gas"), - icon: Icon(Icons.local_fire_department), - ), - ], - selected: {_selectedType}, - onSelectionChanged: (Set newSelection) { - setState(() => _selectedType = newSelection.first); - }, - ), - const SizedBox(height: 20), - // 2. SCADENZA INTELLIGENTE (La parte PRO) - const Text( - "Scadenza Contratto", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.grey, - ), - ), - const SizedBox(height: 8), - - SegmentedButton( - showSelectedIcon: false, // Per un look più pulito - segments: const [ - ButtonSegment(value: 12, label: Text("12m")), - ButtonSegment(value: 24, label: Text("24m")), - ButtonSegment(value: 36, label: Text("36m")), - ButtonSegment( - value: null, - label: Icon(Icons.calendar_month, size: 20), - ), - ], - selected: {_selectedMonthsPreset}, - onSelectionChanged: (Set newSelection) { - final val = newSelection.first; - if (val == null) { - _pickDate(); // Se clicca l'icona calendario, apre il picker - } else { - _applyPreset(val); // Altrimenti applica 12, 24 o 36 - } - }, - ), - - const SizedBox(height: 12), - - // Visualizzazione della data calcolata (o scelta) - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: _selectedExpiration != null - ? Theme.of(context).colorScheme.primary - : Colors.transparent, - ), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon( - Icons.event, - size: 18, - color: _selectedExpiration != null - ? Theme.of(context).colorScheme.primary - : Colors.grey, - ), - const SizedBox(width: 8), - Text( - _selectedExpiration != null - ? "Scade il: ${_selectedExpiration!.day.toString().padLeft(2, '0')}/${_selectedExpiration!.month.toString().padLeft(2, '0')}/${_selectedExpiration!.year}" - : "Seleziona una scadenza", - style: TextStyle( - fontWeight: FontWeight.bold, - color: _selectedExpiration != null - ? Theme.of(context).colorScheme.onSurface - : Colors.grey, - ), - ), - ], - ), - ), - - const SizedBox(height: 20), - - // 2. Provider Dropdown - BlocBuilder( - builder: (context, state) { - if (state.isLoading) { - return const Center( - child: LinearProgressIndicator(), - ); // Mostra una barretta di caricamento - } - - if (state.activeProviders.isEmpty) { - return const Text( - "Nessun gestore associato a questo negozio.", - style: TextStyle(color: Colors.red), - ); - } - // Filtra solo i provider di tipo Energia (Se hai una categoria nel modello) - // Se non hai una categoria nel ProviderModel, puoi rimuovere il .where - final energyProviders = state.activeProviders; - return DropdownButtonFormField( - decoration: const InputDecoration( - labelText: "Gestore / Provider", - border: OutlineInputBorder(), - ), - initialValue: _selectedProviderId, - items: energyProviders.map((p) { - return DropdownMenuItem(value: p.id, child: Text(p.nome)); - }).toList(), - onChanged: (val) => setState(() => _selectedProviderId = val), - ); - }, - ), - const SizedBox(height: 16), - - // 3. Scadenza (DatePicker integrato in un TextField) - TextFormField( - readOnly: true, - onTap: _pickDate, - decoration: InputDecoration( - labelText: "Data Scadenza", - border: const OutlineInputBorder(), - suffixIcon: const Icon(Icons.calendar_month), - ), - // Mostra la data se selezionata, altrimenti vuoto - controller: TextEditingController( - text: _selectedExpiration != null - ? "${_selectedExpiration!.day}/${_selectedExpiration!.month}/${_selectedExpiration!.year}" - : "", - ), - ), - const SizedBox(height: 24), - - // 4. Pulsanti Interni al Form - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: widget.onCancel, - child: const Text("Indietro"), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: - (_selectedProviderId == null || _selectedExpiration == null) - ? null // Disabilitato se mancano dati obbligatori - : () { - final newOperation = EnergyOperationModel( - type: _selectedType, - expiration: _selectedExpiration!, - providerId: _selectedProviderId!, - ); - widget.onSave(newOperation); - }, - child: const Text("Salva Contratto"), - ), - ], - ), - ], - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart b/lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart deleted file mode 100644 index 88fbc27..0000000 --- a/lib/features/operations/ui/operation_form_screen/entertainment_operation_card.dart +++ /dev/null @@ -1,393 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/core/blocs/session/session_cubit.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/operations/data/operations_repository.dart'; -import 'package:flux/features/operations/models/entertainment_operation_model.dart'; -import 'package:get_it/get_it.dart'; - -class EntertainmentOperationDialog extends StatefulWidget { - final List initialOperations; - final String currentStoreId; - - const EntertainmentOperationDialog({ - super.key, - required this.initialOperations, - required this.currentStoreId, - }); - - @override - State createState() => - _EntertainmentOperationDialogState(); -} - -class _EntertainmentOperationDialogState - extends State { - late List _tempList; - bool _isAddingNew = false; - - @override - void initState() { - super.initState(); - _tempList = List.from(widget.initialOperations); - // Carichiamo i provider attivi per lo store corrente - context.read().loadActiveProvidersForStore( - widget.currentStoreId, - ); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Row( - children: [ - Icon( - Icons.movie_filter_outlined, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text(_isAddingNew ? "Nuovo Servizio" : "Servizi Intrattenimento"), - ], - ), - content: AnimatedSize( - duration: const Duration(milliseconds: 300), - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.9, - child: _isAddingNew - ? _EntertainmentForm( - // Il form che abbiamo creato prima - onSave: (newOperation) => setState(() { - _tempList.add(newOperation); - _isAddingNew = false; - }), - onCancel: () => setState(() => _isAddingNew = false), - ) - : BlocBuilder( - builder: (context, state) { - // Passiamo allProviders per garantire la visione dello storico - return _EntertainmentList( - operations: _tempList, - allProviders: state.allProviders, - onDelete: (index) => - setState(() => _tempList.removeAt(index)), - onAddTap: () => setState(() => _isAddingNew = true), - ); - }, - ), - ), - ), - actions: !_isAddingNew - ? [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("Annulla"), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context, _tempList), - child: const Text("Conferma Tutti"), - ), - ] - : null, // I pulsanti del form sono interni al form stesso - ); - } -} - -class _EntertainmentList extends StatelessWidget { - final List operations; - final List allProviders; - final Function(int) onDelete; - final VoidCallback onAddTap; - - const _EntertainmentList({ - required this.operations, - required this.allProviders, - required this.onDelete, - required this.onAddTap, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (operations.isEmpty) - const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Text( - "Nessun servizio intrattenimento.", - textAlign: TextAlign.center, - style: TextStyle(color: Colors.grey), - ), - ) - else - Flexible( - child: ListView.separated( - shrinkWrap: true, - itemCount: operations.length, - separatorBuilder: (_, _) => const Divider(height: 1), - itemBuilder: (context, index) { - final s = operations[index]; - - final providerName = allProviders - .firstWhere( - (p) => p.id == s.providerId, - orElse: () => ProviderModel( - id: '', - nome: 'Fornitore Storico', - companyId: '', - isActive: false, - energia: false, - telefoniaFissa: false, - telefoniaMobile: false, - assicurazioni: false, - finanziamenti: false, - altro: false, - intrattenimento: false, - ), - ) - .nome; - - return ListTile( - contentPadding: EdgeInsets.zero, - leading: CircleAvatar( - backgroundColor: Colors.purple.shade100, - child: const Icon( - Icons.movie_creation_outlined, - color: Colors.purple, - ), - ), - title: Text( - "${s.type} • $providerName", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text( - s.constrained - ? "Vincolo fino al: ${s.constrainExpiration.day}/${s.constrainExpiration.month}/${s.constrainExpiration.year}" - : "Senza vincoli", - style: TextStyle( - color: s.constrained - ? Colors.red.shade700 - : Colors.green.shade700, - ), - ), - trailing: IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red), - onPressed: () => onDelete(index), - ), - ); - }, - ), - ), - const SizedBox(height: 16), - OutlinedButton.icon( - onPressed: onAddTap, - icon: const Icon(Icons.add), - label: const Text("Aggiungi Servizio"), - ), - ], - ); - } -} - -// ---ENTERTAINMENT FORM (MODALE)--- - -class _EntertainmentForm extends StatefulWidget { - final Function(EntertainmentOperationModel) onSave; - final VoidCallback onCancel; - - const _EntertainmentForm({required this.onSave, required this.onCancel}); - - @override - State<_EntertainmentForm> createState() => _EntertainmentFormState(); -} - -class _EntertainmentFormState extends State<_EntertainmentForm> { - String? _selectedProviderId; - final TextEditingController _typeController = TextEditingController(); - bool _isConstrained = false; - DateTime _expirationDate = DateTime.now().add( - const Duration(days: 365), - ); // Default 12 mesi - - // Preset rapidi per il vincolo (es: 12, 24 mesi) - int? _selectedPresetMonths; - - void _applyPreset(int months) { - setState(() { - _selectedPresetMonths = months; - _isConstrained = true; - final now = DateTime.now(); - _expirationDate = DateTime(now.year, now.month + months, now.day); - }); - } - - Future _pickDate() async { - final picked = await showDatePicker( - context: context, - initialDate: _expirationDate, - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 365 * 10)), - ); - if (picked != null) { - setState(() { - _expirationDate = picked; - _selectedPresetMonths = null; - _isConstrained = true; - }); - } - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 1. GESTORE (Filtro intrattenimento) - BlocBuilder( - builder: (context, state) { - final filtered = state.activeProviders - .where((p) => p.intrattenimento) - .toList(); - return DropdownButtonFormField( - decoration: const InputDecoration( - labelText: "Fornitore (es: Sky, TIM)", - border: OutlineInputBorder(), - ), - items: filtered - .map( - (p) => DropdownMenuItem(value: p.id, child: Text(p.nome)), - ) - .toList(), - onChanged: (val) => setState(() => _selectedProviderId = val), - ); - }, - ), - const SizedBox(height: 16), - - // 2. TIPO SERVIZIO (TextField con suggerimenti rapidi sotto) - TextFormField( - controller: _typeController, - decoration: const InputDecoration( - labelText: "Servizio", - hintText: "es: Netflix, DAZN, Disney+", - border: OutlineInputBorder(), - ), - onChanged: (val) => setState(() {}), - ), - const SizedBox(height: 8), - // Suggerimenti rapidi (Chip) - FutureBuilder>( - future: GetIt.I().fetchTopEntertainmentTypes( - GetIt.I().state.company!.id!, - ), - builder: (context, snapshot) { - final suggestions = snapshot.data ?? ["Netflix", "DAZN", "Sky"]; - return Wrap( - spacing: 8, - children: suggestions.map((s) { - return ActionChip( - label: Text(s, style: const TextStyle(fontSize: 12)), - onPressed: () => setState(() => _typeController.text = s), - ); - }).toList(), - ); - }, - ), - const SizedBox(height: 16), - - // 3. VINCOLO CONTRATTUALE - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Vincolo di permanenza", - style: TextStyle(fontWeight: FontWeight.bold), - ), - Switch( - value: _isConstrained, - onChanged: (val) => setState(() { - _isConstrained = val; - if (!val) _selectedPresetMonths = null; - }), - ), - ], - ), - - if (_isConstrained) ...[ - const SizedBox(height: 8), - SegmentedButton( - segments: const [ - ButtonSegment(value: 12, label: Text("12m")), - ButtonSegment(value: 24, label: Text("24m")), - ButtonSegment( - value: null, - label: Icon(Icons.calendar_month, size: 20), - ), - ], - selected: {_selectedPresetMonths}, - onSelectionChanged: (val) { - if (val.first == null) { - _pickDate(); - } else { - _applyPreset(val.first!); - } - }, - ), - const SizedBox(height: 12), - // Box data scadenza vincolo - Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: Theme.of( - context, - ).colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(Icons.event_busy, size: 18, color: Colors.redAccent), - const SizedBox(width: 8), - Text( - "Scadenza vincolo: ${_expirationDate.day.toString().padLeft(2, '0')}/${_expirationDate.month.toString().padLeft(2, '0')}/${_expirationDate.year}", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - ], - ), - ), - ], - - const SizedBox(height: 24), - - // PULSANTI - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: widget.onCancel, - child: const Text("Annulla"), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: - (_selectedProviderId == null || _typeController.text.isEmpty) - ? null - : () => widget.onSave( - EntertainmentOperationModel( - providerId: _selectedProviderId!, - type: _typeController.text, - constrained: _isConstrained, - constrainExpiration: _expirationDate, - ), - ), - child: const Text("Aggiungi"), - ), - ], - ), - ], - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart b/lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart deleted file mode 100644 index fa838a9..0000000 --- a/lib/features/operations/ui/operation_form_screen/finance_operation_dialog.dart +++ /dev/null @@ -1,479 +0,0 @@ -import 'dart:async'; - -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; -import 'package:flux/features/master_data/products/models/model_model.dart'; -import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart'; -import 'package:flux/features/operations/models/fin_operation_model.dart'; -import 'package:flux/features/master_data/providers/models/provider_model.dart'; - -// =========================================================================== -// DIALOG PRINCIPALE -// =========================================================================== -class FinanceOperationDialog extends StatefulWidget { - final List initialOperations; - final String currentStoreId; - final ProductCubit productCubit; - - const FinanceOperationDialog({ - super.key, - required this.initialOperations, - required this.currentStoreId, - required this.productCubit, - }); - - @override - State createState() => _FinanceOperationDialogState(); -} - -class _FinanceOperationDialogState extends State { - late List _tempList; - bool _isAddingNew = false; - - @override - void initState() { - super.initState(); - _tempList = List.from(widget.initialOperations); - // Carichiamo i dati necessari dai Cubit - context.read().loadActiveProvidersForStore( - widget.currentStoreId, - ); - context.read().loadBrands(); - } - - @override - Widget build(BuildContext context) { - return BlocProvider.value( - value: widget.productCubit, - child: AlertDialog( - title: Row( - children: [ - Icon( - Icons.payments_outlined, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text(_isAddingNew ? "Dettagli Finanziamento" : "Finanziamenti"), - ], - ), - content: AnimatedSize( - duration: const Duration(milliseconds: 300), - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.9, - child: _isAddingNew - ? _FinanceForm( - onSave: (newFin) => setState(() { - _tempList.add(newFin); - _isAddingNew = false; - }), - onCancel: () => setState(() => _isAddingNew = false), - ) - : BlocBuilder( - builder: (context, provState) { - return BlocBuilder( - builder: (context, prodState) { - return _FinanceList( - operations: _tempList, - allProviders: - provState.allProviders, // Per vedere lo storico - allModels: prodState.models, - onDelete: (index) => - setState(() => _tempList.removeAt(index)), - onAddTap: () => setState(() => _isAddingNew = true), - ); - }, - ); - }, - ), - ), - ), - actions: !_isAddingNew - ? [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("Annulla"), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context, _tempList), - child: const Text("Conferma"), - ), - ] - : null, - ), - ); - } -} - -// =========================================================================== -// VISTA LISTA (STORICA) -// =========================================================================== -class _FinanceList extends StatelessWidget { - final List operations; - final List allProviders; - final List allModels; - final Function(int) onDelete; - final VoidCallback onAddTap; - - const _FinanceList({ - required this.operations, - required this.allProviders, - required this.allModels, - required this.onDelete, - required this.onAddTap, - }); - - @override - Widget build(BuildContext context) { - if (operations.isEmpty) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Padding( - padding: EdgeInsets.symmetric(vertical: 32.0), - child: Text( - "Nessun finanziamento inserito.", - style: TextStyle(color: Colors.grey), - ), - ), - OutlinedButton.icon( - onPressed: onAddTap, - icon: const Icon(Icons.add), - label: const Text("Aggiungi primo"), - ), - ], - ); - } - - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: ListView.separated( - shrinkWrap: true, - itemCount: operations.length, - separatorBuilder: (_, _) => const Divider(), - itemBuilder: (context, index) { - final s = operations[index]; - - // Cerchiamo il nome del provider in TUTTI quelli caricati (storico) - final providerName = allProviders - .firstWhere( - (p) => p.id == s.providerId, - orElse: () => ProviderModel( - id: '', - nome: 'Operatore Storico', - companyId: '', - isActive: false, - energia: false, - telefoniaFissa: false, - telefoniaMobile: false, - assicurazioni: false, - altro: false, - intrattenimento: false, - finanziamenti: false, - ), - ) - .nome; - - // Cerchiamo il nome del modello - final modelName = allModels - .firstWhere( - (m) => m.id == s.modelId, - orElse: () => ModelModel( - id: '', - name: 'Prodotto', - nameWithBrand: 'Prodotto Storico', - brandId: '', - ), - ) - .nameWithBrand; - - final dateStr = - "${s.expiration.day.toString().padLeft(2, '0')}/${s.expiration.month.toString().padLeft(2, '0')}/${s.expiration.year}"; - - return ListTile( - title: Text( - modelName, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: Text("$providerName • Scade: $dateStr"), - trailing: IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red), - onPressed: () => onDelete(index), - ), - ); - }, - ), - ), - const SizedBox(height: 16), - TextButton.icon( - onPressed: onAddTap, - icon: const Icon(Icons.add), - label: const Text("Aggiungi altro"), - ), - ], - ); - } -} - -// =========================================================================== -// FORM CON OMNI-SEARCH -// =========================================================================== -class _FinanceForm extends StatefulWidget { - final Function(FinOperationModel) onSave; - final VoidCallback onCancel; - - const _FinanceForm({required this.onSave, required this.onCancel}); - - @override - State<_FinanceForm> createState() => _FinanceFormState(); -} - -class _FinanceFormState extends State<_FinanceForm> { - String? _selectedProviderId; - ModelModel? _selectedModel; - int _selectedMonths = 30; // Default richiesto - Timer? _debounce; - final TextEditingController _searchController = TextEditingController(); - late DateTime _selectedExpirationDate; - - @override - void initState() { - super.initState(); - final now = DateTime.now(); - _selectedExpirationDate = DateTime( - now.year, - now.month + _selectedMonths, - now.day, - ); // Inizialmente 30 mesi dalla data attuale - } - - void _onSearchChanged(String query) { - if (_debounce?.isActive ?? false) _debounce!.cancel(); - _debounce = Timer(const Duration(milliseconds: 500), () { - context.read().searchModels(query); - }); - } - - // Funzione per aggiornare la data quando si clicca sui segmenti 24, 30, 48 - void _updateExpirationByMonths(int months) { - setState(() { - _selectedMonths = months; - final now = DateTime.now(); - // Calcolo preciso: aggiungiamo i mesi alla data attuale - _selectedExpirationDate = DateTime(now.year, now.month + months, now.day); - }); - } - - // Funzione per il picker manuale - Future _selectManualDate() async { - final DateTime? picked = await showDatePicker( - context: context, - initialDate: _selectedExpirationDate, - firstDate: DateTime.now(), - lastDate: DateTime.now().add( - const Duration(days: 365 * 10), - ), // Fino a 10 anni - ); - if (picked != null && picked != _selectedExpirationDate) { - setState(() { - _selectedExpirationDate = picked; - _selectedMonths = 0; // Resettiamo i segmenti perché è una data custom - }); - } - } - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - // 1. SCELTA ISTITUTO (Solo attivi) - BlocBuilder( - builder: (context, state) { - final finProviders = state.activeProviders - .where((p) => p.finanziamenti) - .toList(); // Già filtrati dal caricamento della dialog - return DropdownButtonFormField( - initialValue: _selectedProviderId, - decoration: const InputDecoration( - labelText: "Gestore", - border: OutlineInputBorder(), - ), - items: finProviders - .map( - (p) => DropdownMenuItem(value: p.id, child: Text(p.nome)), - ) - .toList(), - onChanged: (val) => setState(() => _selectedProviderId = val), - ); - }, - ), - const SizedBox(height: 16), - - // 2. RICERCA MODELLO - if (_selectedModel == null) ...[ - TextField( - controller: _searchController, - decoration: InputDecoration( - hintText: "Cerca modello (es: iPhone...)", - prefixIcon: const Icon(Icons.search), - border: const OutlineInputBorder(), - suffixIcon: IconButton( - icon: const Icon(Icons.add_circle_outline), - onPressed: () => _showQuickCreate(context), - ), - ), - onChanged: (val) { - _onSearchChanged(val); - }, - ), - const SizedBox(height: 8), - _buildSearchSuggestions(), - ] else - Card( - color: Theme.of(context).colorScheme.secondaryContainer, - child: ListTile( - leading: const Icon(Icons.phone_android), - title: Text( - _selectedModel!.nameWithBrand, - style: const TextStyle(fontWeight: FontWeight.bold), - ), - trailing: IconButton( - icon: const Icon(Icons.close), - onPressed: () => setState(() => _selectedModel = null), - ), - ), - ), - - const SizedBox(height: 16), - - // 3. DURATA PRESET - const Text( - "Durata Rate", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.grey, - ), - ), - const SizedBox(height: 8), - SegmentedButton( - segments: const [ - ButtonSegment(value: 24, label: Text("24m")), - ButtonSegment(value: 30, label: Text("30m")), - ButtonSegment(value: 48, label: Text("48m")), - ], - selected: {_selectedMonths}, - onSelectionChanged: (val) => _updateExpirationByMonths(val.first), - ), - - const SizedBox(height: 16), - - // RIEPILOGO DATA E PICKER MANUALE (Stile Energia) - const Text( - "Scadenza Finanziamento", - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 12, - color: Colors.grey, - ), - ), - const SizedBox(height: 4), - InkWell( - onTap: _selectManualDate, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.shade400), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - const Icon( - Icons.calendar_today, - size: 18, - color: Colors.blue, - ), - const SizedBox(width: 12), - Text( - "${_selectedExpirationDate.day.toString().padLeft(2, '0')}/${_selectedExpirationDate.month.toString().padLeft(2, '0')}/${_selectedExpirationDate.year}", - style: const TextStyle(fontSize: 16), - ), - ], - ), - const Icon(Icons.edit, size: 18, color: Colors.grey), - ], - ), - ), - ), - const SizedBox(height: 24), - - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: widget.onCancel, - child: const Text("Indietro"), - ), - const SizedBox(width: 8), - ElevatedButton( - onPressed: (_selectedProviderId == null || _selectedModel == null) - ? null - : () { - final now = DateTime.now(); - widget.onSave( - FinOperationModel( - providerId: _selectedProviderId!, - modelId: _selectedModel!.id!, - expiration: DateTime( - now.year, - now.month + _selectedMonths, - now.day, - ), - ), - ); - }, - child: const Text("Salva"), - ), - ], - ), - ], - ); - } - - Widget _buildSearchSuggestions() { - return BlocBuilder( - builder: (context, state) { - final query = _searchController.text.toLowerCase(); - if (query.isEmpty) return const SizedBox.shrink(); - - final filtered = state.models - .where((m) => m.nameWithBrand.toLowerCase().contains(query)) - .take(3) - .toList(); - - return Column( - children: filtered - .map( - (m) => ListTile( - title: Text(m.nameWithBrand), - onTap: () => setState(() => _selectedModel = m), - dense: true, - ), - ) - .toList(), - ); - }, - ); - } - - void _showQuickCreate(BuildContext context) { - // Implementazione rapida dialog creazione Brand/Modello come discusso prima - } -} diff --git a/lib/features/operations/ui/operation_form_screen/general_info_section.dart b/lib/features/operations/ui/operation_form_screen/general_info_section.dart deleted file mode 100644 index 183224c..0000000 --- a/lib/features/operations/ui/operation_form_screen/general_info_section.dart +++ /dev/null @@ -1,65 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; - -class GeneralInfoSection extends StatelessWidget { - final OperationModel operation; - const GeneralInfoSection({super.key, required this.operation}); - - @override - Widget build(BuildContext context) { - return Card( - elevation: 2, - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - Icons.info_outline, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - Text( - "Info Generali", - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - const SizedBox(height: 16), - - // Numero di Riferimento / Telefono - TextFormField( - initialValue: operation.reference, - keyboardType: TextInputType - .phone, // Fa aprire il tastierino numerico su mobile - decoration: const InputDecoration( - labelText: "Numero di Telefono / Riferimento", - hintText: "Es. 3331234567", - border: OutlineInputBorder(), - prefixIcon: Icon(Icons.phone), - ), - ), - const SizedBox(height: 16), - - // Campo Note - TextFormField( - initialValue: operation.note, - maxLines: 4, - minLines: 2, - decoration: const InputDecoration( - labelText: "Note Operazione", - hintText: - "Scrivi qui eventuali dettagli o richieste del cliente...", - border: OutlineInputBorder(), - alignLabelWithHint: true, - ), - ), - ], - ), - ), - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/int_dialogs.dart b/lib/features/operations/ui/operation_form_screen/int_dialogs.dart deleted file mode 100644 index cbda2e7..0000000 --- a/lib/features/operations/ui/operation_form_screen/int_dialogs.dart +++ /dev/null @@ -1,158 +0,0 @@ -import 'dart:async'; // Necessario per il Timer -import 'package:flutter/material.dart'; - -Future updateCountDialog( - BuildContext context, - String title, - int currentValue, - Function(int) onSave, -) async { - int tempValue = - currentValue; // Variabile locale per gestire il conteggio nella dialog - - final result = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: Text("Imposta $title"), - content: QuickCounter( - initialValue: tempValue, - onChanged: (val) => tempValue = - val, // Aggiorna il valore locale quando il counter cambia - ), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: const Text("Annulla"), - ), - ElevatedButton( - onPressed: () => Navigator.pop(context, tempValue), - child: const Text("Conferma"), - ), - ], - ), - ); - - if (result != null) { - onSave(result); - } -} - -// --- Widget Interno Specifico per il Counter Veloce --- -class QuickCounter extends StatefulWidget { - final int initialValue; - final ValueChanged - onChanged; // Callback per notificare il padre dei cambiamenti - - const QuickCounter({ - super.key, - required this.initialValue, - required this.onChanged, - }); - - @override - State createState() => _QuickCounterState(); -} - -class _QuickCounterState extends State { - late int _value; - Timer? _longPressTimer; // Il timer per l'auto-incremento - - @override - void initState() { - super.initState(); - _value = widget.initialValue; - } - - @override - void dispose() { - _longPressTimer - ?.cancel(); // IMPORTANTE: Annulla sempre il timer alla distruzione - super.dispose(); - } - - // Logica comune per incremento/decremento singolo o rapido - void _update(int delta) { - setState(() { - _value += delta; - if (_value < 0) _value = 0; // Impedisci numeri negativi - }); - widget.onChanged(_value); // Notifica il padre - } - - // Gestione dell'inizio della pressione prolungata - void _startLongPress(int delta) { - _update(delta); // Esegui subito il primo aggiornamento al tocco iniziale - _longPressTimer = Timer.periodic(const Duration(milliseconds: 100), ( - timer, - ) { - _update(delta); // Aggiorna velocemente finché la pressione continua - }); - } - - // Gestione della fine della pressione prolungata - void _stopLongPress() { - _longPressTimer?.cancel(); - } - - @override - Widget build(BuildContext context) { - final canDecrement = _value > 0; - - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - // --- Pulsante MENO --- - GestureDetector( - onLongPressStart: canDecrement ? (_) => _startLongPress(-1) : null, - onLongPressEnd: (_) => _stopLongPress(), - onLongPressCancel: () => _stopLongPress(), - onTap: canDecrement ? () => _update(-1) : null, - child: Opacity( - // Visivamente disabilitato se < 0 - opacity: canDecrement ? 1.0 : 0.4, - child: const ActionButton(icon: Icons.remove, color: Colors.red), - ), - ), - - // --- Valore Centrale --- - Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - child: Text( - _value.toString(), - style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold), - ), - ), - - // --- Pulsante PIU' --- - GestureDetector( - onLongPressStart: (_) => _startLongPress(1), - onLongPressEnd: (_) => _stopLongPress(), - onLongPressCancel: () => _stopLongPress(), - onTap: () => _update(1), - child: const ActionButton(icon: Icons.add, color: Colors.green), - ), - ], - ); - } -} - -// Piccolo widget di utilità per l'aspetto del pulsante -class ActionButton extends StatelessWidget { - final IconData icon; - final Color color; - - const ActionButton({super.key, required this.icon, required this.color}); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.1), - shape: BoxShape.circle, - border: Border.all(color: color, width: 2), - ), - child: Icon(icon, color: color, size: 30), - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart deleted file mode 100644 index b7379e3..0000000 --- a/lib/features/operations/ui/operation_form_screen/operation_form_screen.dart +++ /dev/null @@ -1,201 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flux/features/operations/blocs/operations_cubit.dart'; -import 'package:flux/features/operations/models/operation_model.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/attachment_section.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/customer_section.dart'; -import 'package:flux/features/operations/ui/operation_form_screen/general_info_section.dart'; - -class OperationFormScreen extends StatefulWidget { - final String? operationId; - final OperationModel? existingOperation; // <-- AGGIUNTO - - const OperationFormScreen({ - super.key, - this.operationId, - this.existingOperation, // <-- AGGIUNTO - }); - - @override - State createState() => _OperationFormScreenState(); -} - -class _OperationFormScreenState extends State { - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - // Diamo in pasto al Cubit tutto quello che abbiamo! - context.read().initOperationForm( - existingOperation: widget.existingOperation, - operationId: widget.operationId, - ); - }); - } - - void _performSave( - BuildContext context, { - required OperationStatus targetStatus, - required bool shouldPop, - }) { - FocusScope.of(context).unfocus(); - context.read().saveCurrentOperation( - targetStatus: targetStatus, - shouldPop: shouldPop, - ); - } - - @override - Widget build(BuildContext context) { - return BlocConsumer( - listener: (context, state) { - if (state.status == OperationsStatus.saved) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Pratica salvata con successo!"), - backgroundColor: Colors.green, - ), - ); - Navigator.pop(context); - } - if (state.status == OperationsStatus.failure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text("Errore: ${state.errorMessage ?? ''}"), - backgroundColor: Colors.red, - ), - ); - } - if (state.status == OperationsStatus.savedNoPop) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text("Pratica salvata con successo!"), - backgroundColor: Colors.green, - ), - ); - } - }, - builder: (context, state) { - final operation = state.currentOperation; - final isSaving = state.status == OperationsStatus.saving; - final isEditMode = widget.operationId != null; - - return Scaffold( - appBar: AppBar( - title: Text(isEditMode ? "Modifica Pratica" : "Nuova Pratica"), - actions: [ - if (isSaving) - const Padding( - padding: EdgeInsets.only(right: 20.0), - child: Center( - child: SizedBox( - width: 20, - height: 20, - child: CircularProgressIndicator(strokeWidth: 2), - ), - ), - ) - else if (operation != null) ...[ - IconButton( - icon: const Icon(Icons.edit_note), - tooltip: "Salva come Bozza", - onPressed: () => _performSave( - context, - targetStatus: OperationStatus.draft, - shouldPop: false, - ), - ), - IconButton( - icon: const Icon( - Icons.check_circle_outline, - color: Colors.green, - ), - tooltip: "Conferma Pratica", - onPressed: () => _performSave( - context, - targetStatus: OperationStatus.ok, - shouldPop: true, - ), - ), - const SizedBox(width: 8), - ], - ], - ), - body: (operation == null) - ? const Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomerSection(operation: operation), - const SizedBox(height: 24), - - GeneralInfoSection(operation: operation), - const SizedBox(height: 24), - - AttachmentsSection(), - const SizedBox(height: 32), - _buildBottomActionButtons(context, isSaving: isSaving), - const SizedBox(height: 32), - ], - ), - ), - ); - }, - ); - } - - Widget _buildBottomActionButtons( - BuildContext context, { - required bool isSaving, - }) { - return Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Expanded( - flex: 1, - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 16), - ), - icon: const Icon(Icons.edit_note), - label: const Text("Salva in Bozza"), - onPressed: isSaving - ? null - : () => _performSave( - context, - targetStatus: OperationStatus.draft, - shouldPop: false, - ), - ), - ), - - const SizedBox(width: 16), - - Expanded( - flex: 2, - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green.shade600, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - ), - icon: const Icon(Icons.check_circle_outline), - label: const Text( - "CONFERMA PRATICA", - style: TextStyle(fontWeight: FontWeight.bold, letterSpacing: 1), - ), - onPressed: isSaving - ? null - : () => _performSave( - context, - targetStatus: OperationStatus.ok, - shouldPop: true, - ), - ), - ), - ], - ); - } -} diff --git a/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart b/lib/features/operations/ui/operation_mobile_upload_screen.dart similarity index 100% rename from lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart rename to lib/features/operations/ui/operation_mobile_upload_screen.dart index ad3fde1..730efd0 100644 --- a/lib/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart +++ b/lib/features/operations/ui/operation_mobile_upload_screen.dart @@ -1,9 +1,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; import 'package:image_picker/image_picker.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; class OperationMobileUploadScreen extends StatefulWidget { final String operationId; diff --git a/lib/main.dart b/lib/main.dart index 979c1ec..9d86a5b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,7 @@ import 'package:flux/core/data/core_repository.dart'; import 'package:flux/core/routes/app_router.dart'; import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/bloc/theme_bloc.dart'; -import 'package:flux/features/customers/blocs/customer_cubit.dart'; +import 'package:flux/features/customers/blocs/customers_cubit.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'; @@ -48,7 +48,7 @@ void main() async { // Cubit delle feature BlocProvider(create: (_) => StoreCubit()), - BlocProvider(create: (_) => CustomerCubit()), + BlocProvider(create: (_) => CustomersCubit()), BlocProvider(create: (_) => ProductCubit()), BlocProvider(create: (_) => StaffCubit()), BlocProvider(create: (_) => OperationsCubit()), -- 2.43.0 From 40ca1a916045c24e4a5b3a21eadec6eb6f85838b Mon Sep 17 00:00:00 2001 From: mark-cachy Date: Sat, 2 May 2026 15:14:22 +0200 Subject: [PATCH 11/18] hkjg --- .../operations/blocs/operations_cubit.dart | 3 -- .../operations/models/operation_model.dart | 12 ++--- .../operations/ui/operation_form_screen.dart | 47 ++++++++++--------- lib/l10n/app_it.arb | 4 +- lib/l10n/app_localizations.dart | 4 +- lib/l10n/app_localizations_it.dart | 4 +- 6 files changed, 38 insertions(+), 36 deletions(-) diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index 536f9eb..88f4441 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -1,10 +1,7 @@ import 'package:equatable/equatable.dart'; -import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flux/core/blocs/session/session_cubit.dart'; -import 'package:flux/core/utils/extensions.dart'; -import 'package:flux/features/attachments/models/attachment_model.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:get_it/get_it.dart'; diff --git a/lib/features/operations/models/operation_model.dart b/lib/features/operations/models/operation_model.dart index 727eb11..ef2c0b6 100644 --- a/lib/features/operations/models/operation_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -27,7 +27,7 @@ class OperationModel extends Equatable { final String? id; final DateTime? createdAt; final String type; - final String? subType; + final String? subtype; final String? providerId; final String? providerDisplayName; final String? modelId; @@ -56,7 +56,7 @@ class OperationModel extends Equatable { this.id, this.createdAt, this.type = '', - this.subType, + this.subtype, this.providerId, this.providerDisplayName, this.modelId, @@ -110,7 +110,7 @@ class OperationModel extends Equatable { id: id ?? this.id, createdAt: createdAt ?? this.createdAt, type: type ?? this.type, - subType: subtype ?? this.subType, + subtype: subtype ?? this.subtype, providerId: providerId ?? this.providerId, providerDisplayName: providerDisplayName ?? this.providerDisplayName, modelId: modelId ?? this.modelId, @@ -139,7 +139,7 @@ class OperationModel extends Equatable { id, createdAt, type, - subType, + subtype, providerId, providerDisplayName, modelId, @@ -174,7 +174,7 @@ class OperationModel extends Equatable { ? DateTime.parse(map['created_at']) : null, type: map['type'] as String? ?? '', - subType: map['sub_type'] as String?, + subtype: map['sub_type'] as String?, providerId: map['provider_id'] as String? ?? '', providerDisplayName: "${map['provider']['name']}".myFormat(), modelId: map['model_id'] as String? ?? '', @@ -212,7 +212,7 @@ class OperationModel extends Equatable { return { if (id != null) 'id': id, 'type': type, - 'sub_type': subType, + 'sub_type': subtype, 'provider_id': providerId, 'model_id': modelId, 'description': description, diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index 6da630a..ce185d0 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -164,41 +164,46 @@ class _OperationFormScreenState extends State { Expanded( child: BlocBuilder( builder: (context, state) { - /* Decommenta e adatta al tuo CustomersState if (state.status == CustomersStatus.loading) { return const Center(child: CircularProgressIndicator()); } if (state.customers.isEmpty) { - return const Center(child: Text('Nessun cliente trovato.', style: TextStyle(color: Colors.grey))); + return const Center( + child: Text( + 'Nessun cliente trovato.', + style: TextStyle(color: Colors.grey), + ), + ); } - */ + return ListView.builder( controller: scrollController, - itemCount: 10, // Sostituisci con state.customers.length + itemCount: state.customers.length, itemBuilder: (context, index) { - // final customer = state.customers[index]; + final customer = state.customers[index]; return ListTile( - leading: const CircleAvatar( - child: Icon(Icons.person), + leading: CircleAvatar( + child: Text( + customer.name.substring(0, 1).toUpperCase(), + ), ), title: Text( - 'Cliente $index', + customer.name, style: const TextStyle( fontWeight: FontWeight.bold, ), - ), // Sostituisci con customer.name - subtitle: const Text( - '333 1234567', - ), // Sostituisci con customer.phoneNumber + ), + subtitle: Text( + '${customer.phoneNumber} • ${customer.email}', + ), onTap: () { // Aggiorniamo il form tramite il Cubit delle operazioni context .read() .updateOperationFields( - customerId: - 'id_del_cliente_$index', // customer.id + customerId: customer.id, // customer.id customerDisplayName: - 'Cliente $index', // customer.name + customer.name, // customer.name ); Navigator.pop(modalContext); }, @@ -433,8 +438,8 @@ class _OperationFormScreenState extends State { // SOTTO-TIPO (Reattivo) if (['Energy', 'Fin', 'Entertainment'].contains(currentType)) ...[ - DropdownButtonFormField( - value: + DropdownButtonFormField( + initialValue: null, // Sostituisci con currentOp?.subtype quando lo aggiungi decoration: const InputDecoration( labelText: 'Dettaglio (es. Luce, Gas...)', @@ -481,6 +486,7 @@ class _OperationFormScreenState extends State { borderRadius: BorderRadius.circular(8), ), onTap: () async { + final operationsCubit = context.read(); final date = await showDatePicker( context: context, initialDate: DateTime.now().add(const Duration(days: 365)), @@ -488,9 +494,7 @@ class _OperationFormScreenState extends State { lastDate: DateTime.now().add(const Duration(days: 3650)), ); if (date != null) { - context.read().updateOperationFields( - expirationDate: date, - ); + operationsCubit.updateOperationFields(expirationDate: date); } }, ), @@ -505,10 +509,11 @@ class _OperationFormScreenState extends State { icon: const Icon(Icons.remove), onPressed: () { final q = currentOp?.quantity ?? 1; - if (q > 1) + if (q > 1) { context.read().updateOperationFields( quantity: q - 1, ); + } }, ), Text( diff --git a/lib/l10n/app_it.arb b/lib/l10n/app_it.arb index 3e195ea..86bc2ca 100644 --- a/lib/l10n/app_it.arb +++ b/lib/l10n/app_it.arb @@ -50,13 +50,13 @@ "commonNewPassword": "Nuova Password", "commonNote": "Nota", "commonSave": "Salva", - "commonOperation": "Servizio", + "commonOperation": "Operazione", "commonSettings": "Impostazioni", "commonStickyNotes": "Sticky Notes", "commonTask": "Attività", "homeExpiringContracts": "Contratti in scadenza", "homeLatestOperationTickets": "Ultime assistenze", - "homeLatestOperations": "Ultimi Servizi", + "homeLatestOperations": "Ultime Operazioni", "homeMyTasks": "Mie Attività", "homeNewOperationTicket": "Nuova assistenza", "homeNoStoreFound": "Nessun negozio trovato", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 707d88f..6fd9505 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -223,7 +223,7 @@ abstract class AppLocalizations { /// No description provided for @commonOperation. /// /// In it, this message translates to: - /// **'Servizio'** + /// **'Operazione'** String get commonOperation; /// No description provided for @commonSettings. @@ -259,7 +259,7 @@ abstract class AppLocalizations { /// No description provided for @homeLatestOperations. /// /// In it, this message translates to: - /// **'Ultimi Servizi'** + /// **'Ultime Operazioni'** String get homeLatestOperations; /// No description provided for @homeMyTasks. diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index 563bdce..f284efa 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -81,7 +81,7 @@ class AppLocalizationsIt extends AppLocalizations { String get commonSave => 'Salva'; @override - String get commonOperation => 'Servizio'; + String get commonOperation => 'Operazione'; @override String get commonSettings => 'Impostazioni'; @@ -99,7 +99,7 @@ class AppLocalizationsIt extends AppLocalizations { String get homeLatestOperationTickets => 'Ultime assistenze'; @override - String get homeLatestOperations => 'Ultimi Servizi'; + String get homeLatestOperations => 'Ultime Operazioni'; @override String get homeMyTasks => 'Mie Attività'; -- 2.43.0 From 4580173edff33357825f502da7a8db1a3c883c7b Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sun, 3 May 2026 10:08:57 +0200 Subject: [PATCH 12/18] new operation form almost ready Co-authored-by: Copilot --- lib/core/routes/app_router.dart | 15 +- .../customers/data/customer_repository.dart | 2 +- .../products/blocs/product_cubit.dart | 29 +- .../products/data/product_repository.dart | 19 +- .../products/ui/brand_selector.dart | 2 +- .../master_data/products/ui/models_list.dart | 2 +- .../products/ui/product_dialogs.dart | 4 +- .../products/ui/products_screen.dart | 4 +- .../products/ui/quick_product_dialog.dart | 2 +- .../providers/models/provider_model.dart | 7 + .../providers/ui/provider_form_sheet.dart | 8 + .../ui/providers_master_data_screen.dart | 2 + .../operations/blocs/operations_cubit.dart | 49 +- .../operations/ui/operation_form_screen.dart | 492 ++++++++++++++++-- lib/main.dart | 2 +- 15 files changed, 578 insertions(+), 61 deletions(-) diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index c577ed2..1ea8bba 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -8,13 +8,16 @@ import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/widgets/set_password_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/customers_cubit.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_mobile_upload_screen.dart'; import 'package:flux/features/customers/ui/customers_content.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/products/blocs/product_cubit.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/staff/ui/staff_screen.dart'; import 'package:flux/features/master_data/store/ui/stores_screen.dart'; @@ -97,7 +100,11 @@ class AppRouter { routes: [ GoRoute( path: 'products', // Diventa /master-data/products - builder: (context, state) => const ProductsScreen(), + builder: (context, state) { + context.read().refreshCubit(); + + return const ProductsScreen(); + }, ), GoRoute( path: 'staff', // Diventa /master-data/staff @@ -172,6 +179,12 @@ class AppRouter { builder: (context, state) { final existingOperation = state.extra as OperationModel?; final operationId = state.uri.queryParameters['operationId']; + context.read().loadCustomers(); + context.read().loadActiveProvidersForStore( + GetIt.I.get().state.currentStore!.id!, + ); + context.read().loadModels(); + context.read().loadBrands(); return BlocProvider( create: (context) => OperationFilesBloc( operationId: operationId ?? existingOperation?.id, diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index 086db52..f7ffc8f 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -67,7 +67,7 @@ class CustomerRepository { .from('customer') .select() .eq('company_id', companyId) - .or('nome.ilike.%$query%,telefono.ilike.%$query%') + .or('name.ilike.%$query%,phone_number.ilike.%$query%') .limit(10); return (response as List).map((c) => CustomerModel.fromMap(c)).toList(); diff --git a/lib/features/master_data/products/blocs/product_cubit.dart b/lib/features/master_data/products/blocs/product_cubit.dart index b35a6bc..f35c0c1 100644 --- a/lib/features/master_data/products/blocs/product_cubit.dart +++ b/lib/features/master_data/products/blocs/product_cubit.dart @@ -9,19 +9,17 @@ import 'package:get_it/get_it.dart'; part 'product_state.dart'; -class ProductCubit extends Cubit { +class ProductsCubit extends Cubit { final ProductRepository _repository = GetIt.I(); final SessionCubit _sessionCubit = GetIt.I(); - ProductCubit() : super(const ProductState()); + ProductsCubit() : super(const ProductState()); // Caricamento iniziale dei Brand Future loadBrands() async { emit(state.copyWith(status: ProductStatus.loading)); try { - final brands = await _repository.getBrands( - _sessionCubit.state.company!.id!, - ); + final brands = await _repository.getBrands(); emit(state.copyWith(status: ProductStatus.success, brands: brands)); } catch (e) { emit( @@ -30,6 +28,27 @@ class ProductCubit extends Cubit { } } + Future 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 refreshCubit() async { + if (state.selectedBrand != null) { + await selectBrand(state.selectedBrand); + } else { + emit(state.copyWith(status: ProductStatus.initial)); + await loadBrands(); + } + } + // Selezione Brand e caricamento Modelli Future selectBrand(BrandModel? brand) async { if (brand == null) { diff --git a/lib/features/master_data/products/data/product_repository.dart b/lib/features/master_data/products/data/product_repository.dart index d456ae3..1fdbe68 100644 --- a/lib/features/master_data/products/data/product_repository.dart +++ b/lib/features/master_data/products/data/product_repository.dart @@ -1,3 +1,4 @@ +import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:get_it/get_it.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import '../models/brand_model.dart'; @@ -5,16 +6,17 @@ import '../models/model_model.dart'; class ProductRepository { final SupabaseClient _supabase = GetIt.I(); + final String _companyId = GetIt.I().state.company!.id!; // --- BRAND --- /// Recupera tutti i brand dell'azienda - Future> getBrands(String companyId) async { + Future> getBrands() async { try { final response = await _supabase .from('brand') .select() - .eq('company_id', companyId) + .eq('company_id', _companyId) .eq('is_active', true) .order('name'); @@ -57,6 +59,19 @@ class ProductRepository { } } + Future> 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 /// NOTA: name_with_brand verrà gestito dal trigger SQL che hai lanciato! Future upsertModel(ModelModel model) async { diff --git a/lib/features/master_data/products/ui/brand_selector.dart b/lib/features/master_data/products/ui/brand_selector.dart index 857bddc..2e35522 100644 --- a/lib/features/master_data/products/ui/brand_selector.dart +++ b/lib/features/master_data/products/ui/brand_selector.dart @@ -33,7 +33,7 @@ class BrandSelector extends StatelessWidget { return DropdownMenuItem(value: brand, child: Text(brand.name)); }).toList(), onChanged: (brand) => - context.read().selectBrand(brand), + context.read().selectBrand(brand), ), ), const SizedBox(width: 16), diff --git a/lib/features/master_data/products/ui/models_list.dart b/lib/features/master_data/products/ui/models_list.dart index f44e6a5..e9dd5c6 100644 --- a/lib/features/master_data/products/ui/models_list.dart +++ b/lib/features/master_data/products/ui/models_list.dart @@ -64,7 +64,7 @@ class ModelsList extends StatelessWidget { color: model.isActive ? context.accent : Colors.grey, ), onPressed: () => context - .read() + .read() .toggleStatus('model', model.id!, model.isActive), ), ], diff --git a/lib/features/master_data/products/ui/product_dialogs.dart b/lib/features/master_data/products/ui/product_dialogs.dart index 3560d0c..3fe457a 100644 --- a/lib/features/master_data/products/ui/product_dialogs.dart +++ b/lib/features/master_data/products/ui/product_dialogs.dart @@ -40,7 +40,7 @@ void _submitBrand( BrandModel? brand, ) { if (controller.text.trim().isNotEmpty) { - context.read().saveBrand(controller.text, id: brand?.id); + context.read().saveBrand(controller.text, id: brand?.id); Navigator.pop(context); } } @@ -81,7 +81,7 @@ void _submitModel( ModelModel? model, ) { if (controller.text.isNotEmpty) { - context.read().saveModel(controller.text, id: model?.id); + context.read().saveModel(controller.text, id: model?.id); Navigator.pop(context); } } diff --git a/lib/features/master_data/products/ui/products_screen.dart b/lib/features/master_data/products/ui/products_screen.dart index 28ffeab..8602876 100644 --- a/lib/features/master_data/products/ui/products_screen.dart +++ b/lib/features/master_data/products/ui/products_screen.dart @@ -12,7 +12,7 @@ class ProductsScreen extends StatelessWidget { @override Widget build(BuildContext context) { // Carichiamo i brand appena la pagina viene creata - context.read().loadBrands(); + context.read().loadBrands(); return Scaffold( backgroundColor: context.background, @@ -33,7 +33,7 @@ class ProductsScreen extends StatelessWidget { ), ), ), - body: BlocConsumer( + body: BlocConsumer( listener: (context, state) { if (state.status == ProductStatus.error) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/features/master_data/products/ui/quick_product_dialog.dart b/lib/features/master_data/products/ui/quick_product_dialog.dart index aa92bae..d3d1c61 100644 --- a/lib/features/master_data/products/ui/quick_product_dialog.dart +++ b/lib/features/master_data/products/ui/quick_product_dialog.dart @@ -23,7 +23,7 @@ class _QuickProductDialogState extends State { setState(() => _isLoading = true); - final newModel = await context.read().quickCreateProduct( + final newModel = await context.read().quickCreateProduct( brandName: _selectedBrandName.trim(), modelName: _modelCtrl.text.trim(), ); diff --git a/lib/features/master_data/providers/models/provider_model.dart b/lib/features/master_data/providers/models/provider_model.dart index a4a9ede..1ab1844 100644 --- a/lib/features/master_data/providers/models/provider_model.dart +++ b/lib/features/master_data/providers/models/provider_model.dart @@ -10,6 +10,7 @@ class ProviderModel extends Equatable { final bool assicurazioni; final bool intrattenimento; final bool finanziamenti; + final bool telepass; final bool altro; final bool isActive; final String companyId; @@ -24,6 +25,7 @@ class ProviderModel extends Equatable { required this.assicurazioni, required this.intrattenimento, required this.finanziamenti, + required this.telepass, required this.altro, required this.isActive, required this.companyId, @@ -51,6 +53,7 @@ class ProviderModel extends Equatable { assicurazioni: map['assicurazioni'] ?? false, intrattenimento: map['intrattenimento'] ?? false, finanziamenti: map['finanziamenti'] ?? false, + telepass: map['telepass'] ?? false, altro: map['altro'] ?? false, isActive: map['is_active'] ?? true, companyId: map['company_id'], @@ -67,6 +70,7 @@ class ProviderModel extends Equatable { 'assicurazioni': assicurazioni, 'intrattenimento': intrattenimento, 'finanziamenti': finanziamenti, + 'telepass': telepass, 'altro': altro, 'is_active': isActive, 'company_id': companyId, @@ -89,6 +93,7 @@ class ProviderModel extends Equatable { assicurazioni, intrattenimento, finanziamenti, + telepass, altro, isActive, companyId, @@ -104,6 +109,7 @@ class ProviderModel extends Equatable { bool? assicurazioni, bool? intrattenimento, bool? finanziamenti, + bool? telepass, bool? altro, bool? isActive, String? companyId, @@ -118,6 +124,7 @@ class ProviderModel extends Equatable { assicurazioni: assicurazioni ?? this.assicurazioni, intrattenimento: intrattenimento ?? this.intrattenimento, finanziamenti: finanziamenti ?? this.finanziamenti, + telepass: telepass ?? this.telepass, altro: altro ?? this.altro, isActive: isActive ?? this.isActive, companyId: companyId ?? this.companyId, 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 2afc48d..37d7f6c 100644 --- a/lib/features/master_data/providers/ui/provider_form_sheet.dart +++ b/lib/features/master_data/providers/ui/provider_form_sheet.dart @@ -21,6 +21,7 @@ class _ProviderFormSheetState extends State { late bool _assicurazioni; late bool _intrattenimento; late bool _finanziamenti; + late bool _telepass; late bool _altro; late bool _isActive; final List _tempSelectedStoreIds = @@ -40,6 +41,7 @@ class _ProviderFormSheetState extends State { _assicurazioni = p?.assicurazioni ?? false; _intrattenimento = p?.intrattenimento ?? false; _finanziamenti = p?.finanziamenti ?? false; + _telepass = p?.telepass ?? false; _altro = p?.altro ?? false; _isActive = p?.isActive ?? true; } @@ -64,6 +66,7 @@ class _ProviderFormSheetState extends State { assicurazioni: _assicurazioni, intrattenimento: _intrattenimento, finanziamenti: _finanziamenti, + telepass: _telepass, altro: _altro, isActive: _isActive, companyId: @@ -138,6 +141,11 @@ class _ProviderFormSheetState extends State { _finanziamenti, (v) => setState(() => _finanziamenti = v), ), + _buildSwitch( + "Telepass", + _telepass, + (v) => setState(() => _telepass = v), + ), _buildSwitch( "Altro/Accessori", _altro, 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 bc69fe0..8d4a259 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 @@ -146,6 +146,8 @@ class _ProvidersMasterDataScreenState extends State { if (p.energia) _smallTag("⚡ Energy", Colors.orange), if (p.assicurazioni) _smallTag("🛡️ Assic", Colors.teal), 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), ], ); diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index 88f4441..1b4b0e9 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -213,14 +213,19 @@ class OperationsCubit extends Cubit { String? customerDisplayName, String? type, String? providerId, + String? providerDisplayName, String? subtype, DateTime? expirationDate, int? quantity, + String? modelId, + String? modelDisplayName, // Aggiungiamo questi flag per forzare la pulizia dei campi quando cambi tipo bool clearProvider = false, bool clearType = false, bool clearSubtype = false, bool clearExpiration = false, + bool clearQuantity = false, + bool clearModel = false, }) { if (state.currentOperation == null) return; @@ -231,16 +236,48 @@ class OperationsCubit extends Cubit { final updated = current.copyWith( customerId: customerId, 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) providerId: clearProvider ? null : (providerId ?? current.providerId), - // Idem per subtype e date. - // Se expirationDate è nullabile nel copyWith, dovresti poterlo gestire - quantity: quantity ?? current.quantity, + providerDisplayName: clearProvider + ? null + : (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)); } + + // 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, + ); + } } diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index ce185d0..dd48807 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -1,6 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/models/operation_model.dart'; // import 'package:flux/features/attachments/ui/operation_files_section.dart'; @@ -25,7 +29,7 @@ class _OperationFormScreenState extends State { // TEXT CONTROLLERS (Unici detentori di stato locale per evitare lag) final _referenceController = TextEditingController(); final _noteController = TextEditingController(); - final _customSubtypeController = TextEditingController(); + final _freeTextSubtypeController = TextEditingController(); final List _availableTypes = [ 'AL', @@ -39,6 +43,34 @@ class _OperationFormScreenState extends State { '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; @override @@ -55,7 +87,7 @@ class _OperationFormScreenState extends State { void dispose() { _referenceController.dispose(); _noteController.dispose(); - _customSubtypeController.dispose(); + _freeTextSubtypeController.dispose(); super.dispose(); } @@ -67,6 +99,11 @@ class _OperationFormScreenState extends State { if (_noteController.text.isEmpty && model.note.isNotEmpty) { _noteController.text = model.note; } + if (_freeTextSubtypeController.text.isEmpty && + model.subtype != null && + model.subtype!.isNotEmpty) { + _freeTextSubtypeController.text = model.subtype!; + } _isInitialized = true; } @@ -96,6 +133,8 @@ class _OperationFormScreenState extends State { // --- MODALE SELEZIONE CLIENTE --- void _showCustomerModal() { + String currentSearchQuery = ''; + showModalBottomSheet( context: context, isScrollControlled: true, @@ -132,6 +171,7 @@ class _OperationFormScreenState extends State { Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: TextField( + autofocus: true, decoration: InputDecoration( hintText: 'Cerca per nome, telefono o email...', prefixIcon: const Icon(Icons.search), @@ -140,8 +180,8 @@ class _OperationFormScreenState extends State { ), ), onChanged: (query) { - // Evento di ricerca (usa debouncer nel cubit!) - // context.read().searchCustomers(query); + currentSearchQuery = query; + context.read().searchCustomers(query); }, ), ), @@ -154,8 +194,37 @@ class _OperationFormScreenState extends State { ), icon: const Icon(Icons.person_add), label: const Text('Crea Nuovo Cliente'), - onPressed: () { - // Apri form nuovo cliente... + onPressed: () async { + final OperationsCubit operationsCubit = context + .read(); + + // APRIAMO LA DIALOG RAPIDA CON LA MAGIA DEL BLOC PROVIDER + final newCustomer = await showDialog( + context: context, + builder: (dialogContext) { + return BlocProvider.value( + value: context.read(), + 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 { // Ripuliamo SOLO i testi liberi (il Cubit gestisce già i suoi reset) _referenceController.clear(); _noteController.clear(); - _customSubtypeController.clear(); + _freeTextSubtypeController.clear(); } else if (state.status == OperationsStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -403,12 +472,7 @@ class _OperationFormScreenState extends State { onSelected: (selected) { if (selected) { // Diciamo al Cubit di cambiare tipo e spianare i campi dipendenti - context.read().updateOperationFields( - type: type, - clearProvider: true, - clearSubtype: true, - clearExpiration: true, - ); + context.read().setTypeWithSmartDefault(type); } }, ); @@ -423,73 +487,138 @@ class _OperationFormScreenState extends State { ListTile( title: const Text('Seleziona Gestore'), subtitle: Text( - currentOp?.providerId ?? 'Nessun gestore selezionato', - ), // Adatta se hai displayName + (currentOp?.providerDisplayName != null && + 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), shape: RoundedRectangleBorder( side: BorderSide(color: theme.dividerColor), borderRadius: BorderRadius.circular(8), ), onTap: () { - // TODO: Modale o Dropdown Provider + _showProviderModal(currentType); }, ), const SizedBox(height: 16), - // SOTTO-TIPO (Reattivo) - if (['Energy', 'Fin', 'Entertainment'].contains(currentType)) ...[ - DropdownButtonFormField( + // 1. SCENARIO ENERGY (Dropdown Fisso) + if (currentType == 'Energy') ...[ + DropdownButtonFormField( initialValue: - null, // Sostituisci con currentOp?.subtype quando lo aggiungi - decoration: const InputDecoration( - labelText: 'Dettaglio (es. Luce, Gas...)', - ), + (currentOp?.subtype != null && currentOp!.subtype!.isNotEmpty) + ? currentOp.subtype + : null, + decoration: const InputDecoration(labelText: 'Dettaglio Fornitura'), items: [ 'Luce', 'Gas', 'Dual', ].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), onChanged: (val) { - // context.read().updateOperationFields(subtype: val); + if (val != null) { + context.read().updateOperationFields( + subtype: val, + ); + } }, ), const SizedBox(height: 16), ], - // SOTTO-TIPO CUSTOM (Reattivo) - if (currentType == 'Custom') ...[ + // 2. SCENARIO FIN (Ricerca Modello/Prodotto) + 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( - controller: _customSubtypeController, - decoration: const InputDecoration( - labelText: 'Specifica il servizio (es. Monopattino)', + controller: _freeTextSubtypeController, + decoration: InputDecoration( + labelText: currentType == 'Entertainment' + ? 'Piattaforma (es. Netflix, DAZN, Spotify...)' + : 'Specifica il servizio (es. Monopattino)', ), ), const SizedBox(height: 16), ], - // SCADENZA (Reattivo) + // SCADENZA (Reattivo per tipi complessi) if ([ 'Energy', 'Fin', 'Entertainment', 'Custom', ].contains(currentType)) ...[ + const SizedBox(height: 8), + + // --- I CHIPS RAPIDI --- + _buildDurationQuickPicks(currentOp), + + const SizedBox(height: 16), + + // --- IL SELETTORE MANUALE --- ListTile( - title: const Text('Data di Scadenza'), + title: const Text('Data di Scadenza Effettiva'), subtitle: Text( - currentOp?.expirationDate?.toLocal().toString().split(' ')[0] ?? - 'Nessuna scadenza', + currentOp?.expirationDate != null + ? "${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( - side: BorderSide(color: theme.dividerColor), - borderRadius: BorderRadius.circular(8), + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: Colors.blue, width: 0.5), ), onTap: () async { - final operationsCubit = context.read(); + final OperationsCubit operationsCubit = context + .read(); final date = await showDatePicker( context: context, - initialDate: DateTime.now().add(const Duration(days: 365)), + initialDate: + currentOp?.expirationDate ?? + DateTime.now().add(const Duration(days: 365)), firstDate: DateTime.now(), lastDate: DateTime.now().add(const Duration(days: 3650)), ); @@ -545,6 +674,50 @@ class _OperationFormScreenState extends State { ); } + 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().updateOperationFields( + expirationDate: newDate, + ); + }, + ), + ); + }).toList(), + ), + ), + ], + ); + } + Widget _buildNotesSection({required bool isDesktop}) { final title = _buildSectionTitle('Note Interne'); final noteField = TextFormField( @@ -626,4 +799,247 @@ class _OperationFormScreenState extends State { ), ); } + + 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( + // <--- 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() + .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().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(); + + // 1. Recuperiamo la lista dei brand (adatta questo in base a dove tieni i brand nel tuo stato) + final existingBrands = context + .read() + .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(), + 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( + // <--- 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().updateOperationFields( + modelId: + 'id_del_modello_$index', // deviceModel.id + // Assicurati di avere questo campo in _updateOperationFields nel Cubit! + // modelDisplayName: deviceModel.nameWithBrand, + ); + Navigator.pop(modalContext); + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } } diff --git a/lib/main.dart b/lib/main.dart index 9d86a5b..93198f7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -49,7 +49,7 @@ void main() async { // Cubit delle feature BlocProvider(create: (_) => StoreCubit()), BlocProvider(create: (_) => CustomersCubit()), - BlocProvider(create: (_) => ProductCubit()), + BlocProvider(create: (_) => ProductsCubit()), BlocProvider(create: (_) => StaffCubit()), BlocProvider(create: (_) => OperationsCubit()), BlocProvider(create: (_) => ProvidersCubit()), -- 2.43.0 From 4559db620d1fc55a83072d672b71bb5d63e23172 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sun, 3 May 2026 10:30:09 +0200 Subject: [PATCH 13/18] rinominati i campi in inglese per diminuire confusione Co-authored-by: Copilot --- .../company/models/company_model.dart | 128 +++++++++--------- .../company/ui/create_company_screen.dart | 16 +-- lib/features/home/ui/home_screen.dart | 4 +- .../providers/ui/provider_form_sheet.dart | 2 +- .../master_data/staff/ui/staff_screen.dart | 4 +- .../master_data/store/models/store_model.dart | 80 +++++------ .../store/ui/create_store_screen.dart | 20 +-- .../master_data/store/ui/store_card.dart | 8 +- .../master_data/store/ui/store_form.dart | 20 +-- .../onboarding/blocs/onboarding_cubit.dart | 2 +- .../onboarding/ui/store_onboarding_form.dart | 10 +- .../data/operations_repository.dart | 6 +- .../operations/ui/operation_form_screen.dart | 12 +- 13 files changed, 156 insertions(+), 156 deletions(-) diff --git a/lib/features/company/models/company_model.dart b/lib/features/company/models/company_model.dart index 97e542a..3e270ab 100644 --- a/lib/features/company/models/company_model.dart +++ b/lib/features/company/models/company_model.dart @@ -45,14 +45,14 @@ class CompanyModel extends Equatable { final String userId; // Nel DB è user_id (chiave esterna su auth.users) // Dati Anagrafici e Fatturazione - final String ragioneSociale; - final String indirizzo; - final String cap; - final String citta; - final String provincia; - final String partitaIva; - final String codiceFiscale; - final String codiceUnivoco; + final String name; + final String address; + final String zipCode; + final String city; + final String province; + final String vatId; + final String fiscalCode; + final String sdi; final String companyLogo; // Stato Pagamenti (Ibride: manuale + Stripe) @@ -70,14 +70,14 @@ class CompanyModel extends Equatable { this.id, this.createdAt, required this.userId, - required this.ragioneSociale, - required this.indirizzo, - required this.cap, - required this.citta, - required this.provincia, - required this.partitaIva, - required this.codiceFiscale, - required this.codiceUnivoco, + required this.name, + required this.address, + required this.zipCode, + required this.city, + required this.province, + required this.vatId, + required this.fiscalCode, + required this.sdi, this.companyLogo = '', this.isPaid = false, this.paymentExpiration, @@ -92,14 +92,14 @@ class CompanyModel extends Equatable { String? id, DateTime? createdAt, String? userId, - String? ragioneSociale, - String? indirizzo, - String? cap, - String? citta, - String? provincia, - String? partitaIva, - String? codiceFiscale, - String? codiceUnivoco, + String? name, + String? address, + String? zipCode, + String? city, + String? province, + String? vatId, + String? fiscalCode, + String? sdi, String? companyLogo, bool? isPaid, DateTime? paymentExpiration, @@ -113,14 +113,14 @@ class CompanyModel extends Equatable { id: id ?? this.id, createdAt: createdAt ?? this.createdAt, userId: userId ?? this.userId, - ragioneSociale: ragioneSociale ?? this.ragioneSociale, - indirizzo: indirizzo ?? this.indirizzo, - cap: cap ?? this.cap, - citta: citta ?? this.citta, - provincia: provincia ?? this.provincia, - partitaIva: partitaIva ?? this.partitaIva, - codiceFiscale: codiceFiscale ?? this.codiceFiscale, - codiceUnivoco: codiceUnivoco ?? this.codiceUnivoco, + name: name ?? this.name, + address: address ?? this.address, + zipCode: zipCode ?? this.zipCode, + city: city ?? this.city, + province: province ?? this.province, + vatId: vatId ?? this.vatId, + fiscalCode: fiscalCode ?? this.fiscalCode, + sdi: sdi ?? this.sdi, companyLogo: companyLogo ?? this.companyLogo, isPaid: isPaid ?? this.isPaid, paymentExpiration: paymentExpiration ?? this.paymentExpiration, @@ -137,14 +137,14 @@ class CompanyModel extends Equatable { id: null, createdAt: null, userId: '', - ragioneSociale: '', - indirizzo: '', - cap: '', - citta: '', - provincia: '', - partitaIva: '', - codiceFiscale: '', - codiceUnivoco: '', + name: '', + address: '', + zipCode: '', + city: '', + province: '', + vatId: '', + fiscalCode: '', + sdi: '', ); } @@ -155,14 +155,14 @@ class CompanyModel extends Equatable { ? DateTime.tryParse(map['created_at']) : null, userId: map['user_id'] ?? '', - ragioneSociale: map['ragione_sociale'] ?? '', - indirizzo: map['indirizzo'] ?? '', - cap: map['cap'] ?? '', - citta: map['citta'] ?? '', - provincia: map['provincia'] ?? '', - partitaIva: map['partita_iva'] ?? '', - codiceFiscale: map['codice_fiscale'] ?? '', - codiceUnivoco: map['codice_univoco'] ?? '', + name: map['name'] ?? '', + address: map['address'] ?? '', + zipCode: map['zip_code'] ?? '', + city: map['city'] ?? '', + province: map['province'] ?? '', + vatId: map['vat_id'] ?? '', + fiscalCode: map['fiscal_code'] ?? '', + sdi: map['sdi'] ?? '', companyLogo: map['company_logo'] ?? '', isPaid: map['is_paid'] ?? false, paymentExpiration: map['payment_expiration'] != null @@ -185,14 +185,14 @@ class CompanyModel extends Equatable { if (id != null) 'id': id, // created_at è gestito dal DB di default, di solito non si passa nell'insert 'user_id': userId, - 'ragione_sociale': ragioneSociale, - 'indirizzo': indirizzo, - 'cap': cap, - 'citta': citta, - 'provincia': provincia, - 'partita_iva': partitaIva, - 'codice_fiscale': codiceFiscale, - 'codice_univoco': codiceUnivoco, + 'name': name, + 'address': address, + 'zip_code': zipCode, + 'city': city, + 'province': province, + 'vat_id': vatId, + 'fiscal_code': fiscalCode, + 'sdi': sdi, 'company_logo': companyLogo, 'is_paid': isPaid, if (paymentExpiration != null) @@ -213,14 +213,14 @@ class CompanyModel extends Equatable { id, createdAt, userId, - ragioneSociale, - indirizzo, - cap, - citta, - provincia, - partitaIva, - codiceFiscale, - codiceUnivoco, + name, + address, + zipCode, + city, + province, + vatId, + fiscalCode, + sdi, companyLogo, isPaid, paymentExpiration, diff --git a/lib/features/company/ui/create_company_screen.dart b/lib/features/company/ui/create_company_screen.dart index c5447b3..07ead76 100644 --- a/lib/features/company/ui/create_company_screen.dart +++ b/lib/features/company/ui/create_company_screen.dart @@ -50,14 +50,14 @@ class _CreateCompanyScreenState extends State { 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(), + name: _ragioneSocialeController.text.trim(), + address: _indirizzoController.text.trim(), + zipCode: _capController.text.trim(), + city: _cittaController.text.trim(), + province: _provinciaController.text.trim(), + vatId: _pIvaController.text.trim(), + fiscalCode: _cfController.text.trim(), + sdi: _univocoController.text.trim().toUpperCase(), // Gli altri campi hanno i default nel modello ); diff --git a/lib/features/home/ui/home_screen.dart b/lib/features/home/ui/home_screen.dart index 1c498b2..40bef0a 100644 --- a/lib/features/home/ui/home_screen.dart +++ b/lib/features/home/ui/home_screen.dart @@ -149,7 +149,7 @@ class HomeScreen extends StatelessWidget { Icon(Icons.storefront, size: 16, color: context.primary), const SizedBox(width: 8), Text( - currentStore?.nome ?? context.l10n.homeNoStoreFound, + currentStore?.name ?? context.l10n.homeNoStoreFound, style: TextStyle( fontWeight: FontWeight.w600, color: context.primary, @@ -352,7 +352,7 @@ class HomeScreen extends StatelessWidget { : theme.iconTheme.color, ), title: Text( - store.nome, + store.name, style: TextStyle( fontWeight: isSelected ? FontWeight.bold 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 37d7f6c..fbf1602 100644 --- a/lib/features/master_data/providers/ui/provider_form_sheet.dart +++ b/lib/features/master_data/providers/ui/provider_form_sheet.dart @@ -172,7 +172,7 @@ class _ProviderFormSheetState extends State { store.id, ); return CheckboxListTile( - title: Text(store.nome), + title: Text(store.name), value: isAssociated, onChanged: (val) { setState(() { diff --git a/lib/features/master_data/staff/ui/staff_screen.dart b/lib/features/master_data/staff/ui/staff_screen.dart index 1213672..ccf77ee 100644 --- a/lib/features/master_data/staff/ui/staff_screen.dart +++ b/lib/features/master_data/staff/ui/staff_screen.dart @@ -126,7 +126,7 @@ class _StaffScreenState extends State { initialValue: _selectedStoreId, decoration: const InputDecoration(labelText: "Filtra per Negozio"), items: state.stores - .map((s) => DropdownMenuItem(value: s.id, child: Text(s.nome))) + .map((s) => DropdownMenuItem(value: s.id, child: Text(s.name))) .toList(), onChanged: (id) { setState(() => _selectedStoreId = id); @@ -355,7 +355,7 @@ class _StaffScreenState extends State { store.id, ); return FilterChip( - label: Text(store.nome), + label: Text(store.name), selected: isSelected, onSelected: (selected) { setModalState(() { diff --git a/lib/features/master_data/store/models/store_model.dart b/lib/features/master_data/store/models/store_model.dart index f30e0c1..988d256 100644 --- a/lib/features/master_data/store/models/store_model.dart +++ b/lib/features/master_data/store/models/store_model.dart @@ -4,30 +4,30 @@ import 'package:flux/features/master_data/staff/models/staff_member_model.dart'; class StoreModel extends Equatable { final String? id; - final String nome; + final String name; final String companyId; final bool isActive; final bool isPaid; final DateTime? paymentExpiration; - final String indirizzo; - final String cap; - final String comune; - final String provincia; + final String address; + final String zipCode; + final String city; + final String province; final List associatedProviders; // Provider associati final List associatedStaffMembers; // Membri dello staff associati const StoreModel({ this.id, - required this.nome, + required this.name, required this.companyId, this.isActive = true, this.isPaid = false, this.paymentExpiration, - required this.indirizzo, - required this.cap, - required this.comune, - required this.provincia, + required this.address, + required this.zipCode, + required this.city, + required this.province, this.associatedProviders = const [], this.associatedStaffMembers = const [], }); @@ -36,15 +36,15 @@ class StoreModel extends Equatable { @override List get props => [ id, - nome, + name, companyId, isActive, isPaid, paymentExpiration, - indirizzo, - cap, - comune, - provincia, + address, + zipCode, + city, + province, associatedProviders, associatedStaffMembers, ]; @@ -52,29 +52,29 @@ class StoreModel extends Equatable { // Il mitico copyWith per creare nuove istanze modificando solo ciò che serve StoreModel copyWith({ String? id, - String? nome, + String? name, String? companyId, bool? isActive, bool? isPaid, DateTime? paymentExpiration, - String? indirizzo, - String? cap, - String? comune, - String? provincia, + String? address, + String? zipCode, + String? city, + String? province, List? associatedProviders, List? associatedStaffMembers, }) { return StoreModel( id: id ?? this.id, - nome: nome ?? this.nome, + name: name ?? this.name, 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, + address: address ?? this.address, + zipCode: zipCode ?? this.zipCode, + city: city ?? this.city, + province: province ?? this.province, associatedProviders: associatedProviders ?? this.associatedProviders, associatedStaffMembers: associatedStaffMembers ?? this.associatedStaffMembers, @@ -83,12 +83,12 @@ class StoreModel extends Equatable { factory StoreModel.empty() { return const StoreModel( - nome: '', + name: '', companyId: '', - indirizzo: '', - cap: '', - comune: '', - provincia: '', + address: '', + zipCode: '', + city: '', + province: '', ); } @@ -118,17 +118,17 @@ class StoreModel extends Equatable { } return StoreModel( id: map['id'] as String, - nome: map['nome'], + name: map['name'], companyId: map['company_id'] as String, isActive: map['is_active'] ?? true, isPaid: map['is_paid'] ?? false, paymentExpiration: map['payment_expiration'] != null ? DateTime.parse(map['payment_expiration']) : null, - indirizzo: map['indirizzo'], - cap: map['cap'], - comune: map['comune'], - provincia: map['provincia'], + address: map['address'], + zipCode: map['zip_code'], + city: map['city'], + province: map['province'], associatedProviders: providers, associatedStaffMembers: staffMembers, ); @@ -137,16 +137,16 @@ class StoreModel extends Equatable { Map toMap() { return { if (id != null) 'id': id, - 'nome': nome, + 'name': name, 'company_id': companyId, 'is_active': isActive, 'is_paid': isPaid, if (paymentExpiration != null) 'payment_expiration': paymentExpiration!.toIso8601String(), - 'indirizzo': indirizzo, - 'cap': cap, - 'comune': comune, - 'provincia': provincia, + 'address': address, + 'zip_code': zipCode, + 'city': city, + 'province': province, }; } } diff --git a/lib/features/master_data/store/ui/create_store_screen.dart b/lib/features/master_data/store/ui/create_store_screen.dart index 7ea5786..918ecc7 100644 --- a/lib/features/master_data/store/ui/create_store_screen.dart +++ b/lib/features/master_data/store/ui/create_store_screen.dart @@ -37,14 +37,14 @@ class _CreateStoreScreenState extends State { final company = context.read().state.company; if (company != null) { setState(() { - _indirizzoController.text = company.indirizzo; - _capController.text = company.cap; + _indirizzoController.text = company.address; + _capController.text = company.zipCode; _comuneController.text = - company.citta; // Nel DB company è 'citta', store è 'comune' - _provinciaController.text = company.provincia; + company.city; // Nel DB company è 'citta', store è 'comune' + _provinciaController.text = company.province; // Suggeriamo anche un nome se vuoto if (_nomeController.text.isEmpty) { - _nomeController.text = '${company.ragioneSociale} - Sede'; + _nomeController.text = '${company.name} - Sede'; } }); @@ -68,12 +68,12 @@ class _CreateStoreScreenState extends State { } final store = StoreModel( - nome: _nomeController.text.trim(), + name: _nomeController.text.trim(), companyId: company.id!, - indirizzo: _indirizzoController.text.trim(), - cap: _capController.text.trim(), - comune: _comuneController.text.trim(), - provincia: _provinciaController.text.trim().toUpperCase(), + address: _indirizzoController.text.trim(), + zipCode: _capController.text.trim(), + city: _comuneController.text.trim(), + province: _provinciaController.text.trim().toUpperCase(), ); context.read().createStore(store); diff --git a/lib/features/master_data/store/ui/store_card.dart b/lib/features/master_data/store/ui/store_card.dart index 910c3c3..a585254 100644 --- a/lib/features/master_data/store/ui/store_card.dart +++ b/lib/features/master_data/store/ui/store_card.dart @@ -53,11 +53,11 @@ class _StoreCardState extends State { color: widget.store.isActive ? context.accent : Colors.grey, ), title: Text( - widget.store.nome, + widget.store.name, style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: Text( - "${widget.store.comune} (${widget.store.provincia}) - ${widget.store.indirizzo}", + "${widget.store.city} (${widget.store.province}) - ${widget.store.address}", ), trailing: Switch( value: widget.store.isActive, @@ -129,7 +129,7 @@ class _StoreCardState extends State { mainAxisSize: MainAxisSize.min, children: [ Text( - "Personale di ${store.nome}", + "Personale di ${store.name}", style: context.titleLarge, ), const SizedBox(height: 16), @@ -184,7 +184,7 @@ class _StoreCardState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text("Providers di ${store.nome}", style: context.titleLarge), + Text("Providers di ${store.name}", style: context.titleLarge), const SizedBox(height: 16), ...state.allProviders.map((provider) { final isAssociated = _tempAssociatedProviders.any( diff --git a/lib/features/master_data/store/ui/store_form.dart b/lib/features/master_data/store/ui/store_form.dart index 1795169..131009f 100644 --- a/lib/features/master_data/store/ui/store_form.dart +++ b/lib/features/master_data/store/ui/store_form.dart @@ -24,11 +24,11 @@ class _StoreFormState extends State { void initState() { super.initState(); if (widget.store != null) { - nomeController.text = widget.store!.nome; - indirizzoController.text = widget.store!.indirizzo; - capController.text = widget.store!.cap; - comuneController.text = widget.store!.comune; - provinciaController.text = widget.store!.provincia; + nomeController.text = widget.store!.name; + indirizzoController.text = widget.store!.address; + capController.text = widget.store!.zipCode; + comuneController.text = widget.store!.city; + provinciaController.text = widget.store!.province; } } @@ -124,11 +124,11 @@ class _StoreFormState extends State { id: widget .store ?.id, // Se nullo, Supabase ne crea uno nuovo - nome: nomeController.text, - indirizzo: indirizzoController.text, - cap: capController.text, - comune: comuneController.text, - provincia: provinciaController.text, + name: nomeController.text, + address: indirizzoController.text, + zipCode: capController.text, + city: comuneController.text, + province: provinciaController.text, companyId: context .read() .state diff --git a/lib/features/onboarding/blocs/onboarding_cubit.dart b/lib/features/onboarding/blocs/onboarding_cubit.dart index d00cb33..e6a4df8 100644 --- a/lib/features/onboarding/blocs/onboarding_cubit.dart +++ b/lib/features/onboarding/blocs/onboarding_cubit.dart @@ -25,7 +25,7 @@ class OnboardingCubit extends Cubit { Future saveCompany(String companyName) async { emit(state.copyWith(isLoading: true)); final company = CompanyModel.empty().copyWith( - ragioneSociale: companyName, + name: companyName, userId: GetIt.I().auth.currentUser!.id, subscriptionTier: SubscriptionTier.pro, subscriptionStatus: SubscriptionStatus.trialing, diff --git a/lib/features/onboarding/ui/store_onboarding_form.dart b/lib/features/onboarding/ui/store_onboarding_form.dart index 2408abf..e30fadc 100644 --- a/lib/features/onboarding/ui/store_onboarding_form.dart +++ b/lib/features/onboarding/ui/store_onboarding_form.dart @@ -135,12 +135,12 @@ class _StoreOnboardingFormState extends State { if (_formKey.currentState!.validate()) { // MIRACOLO DELLA FACTORY EMPTY! final newStore = StoreModel.empty().copyWith( - nome: _nameCtrl.text.trim(), - indirizzo: _addressCtrl.text.trim(), - comune: _cityCtrl.text.trim(), - cap: _zipCodeCtrl.text.trim(), + name: _nameCtrl.text.trim(), + address: _addressCtrl.text.trim(), + city: _cityCtrl.text.trim(), + zipCode: _zipCodeCtrl.text.trim(), // Formattiamo in maiuscolo qui, al momento del salvataggio! - provincia: _provinceCtrl.text.trim().toUpperCase(), + province: _provinceCtrl.text.trim().toUpperCase(), ); context.read().saveStore(newStore); } diff --git a/lib/features/operations/data/operations_repository.dart b/lib/features/operations/data/operations_repository.dart index e846ca5..bafa985 100644 --- a/lib/features/operations/data/operations_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -23,7 +23,7 @@ class OperationsRepository { staff_member(name), provider(name), model(name_with_brand), - attachments(*) + attachment(*) ''') .eq('id', id) .single(); @@ -52,7 +52,7 @@ class OperationsRepository { provider(name), model(name_with_brand), staff_member(name), - attachments(*) + attachment(*) ''') .eq('company_id', companyId); @@ -122,7 +122,7 @@ class OperationsRepository { provider(name), model(name_with_brand), customer(name), - attachments(*) + attachment(*) ''') .eq('id', newId) .single(); diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index dd48807..bbdf1e7 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -1021,12 +1021,12 @@ class _OperationFormScreenState extends State { ), ), onTap: () { - context.read().updateOperationFields( - modelId: - 'id_del_modello_$index', // deviceModel.id - // Assicurati di avere questo campo in _updateOperationFields nel Cubit! - // modelDisplayName: deviceModel.nameWithBrand, - ); + context + .read() + .updateOperationFields( + modelId: deviceModel.id, + modelDisplayName: deviceModel.nameWithBrand, + ); Navigator.pop(modalContext); }, ); -- 2.43.0 From 6bb65e8296cd03e2150b485f9a3558fe56ffc109 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sun, 3 May 2026 12:05:47 +0200 Subject: [PATCH 14/18] refactor Co-authored-by: Copilot --- lib/core/data/core_repository.dart | 2 +- .../providers/data/provider_repository.dart | 2 +- .../providers/models/provider_model.dart | 112 +-- .../providers/ui/provider_form_sheet.dart | 74 +- .../ui/providers_master_data_screen.dart | 15 +- .../store/data/store_repository.dart | 2 +- .../master_data/store/ui/store_card.dart | 2 +- .../operations/blocs/operations_cubit.dart | 14 +- .../data/operations_repository.dart | 45 +- .../operations/models/operation_model.dart | 48 +- .../operations/ui/operation_form_screen.dart | 696 +----------------- .../ui/widgets/customer_section.dart | 222 ++++++ .../ui/widgets/details_section.dart | 413 +++++++++++ 13 files changed, 832 insertions(+), 815 deletions(-) create mode 100644 lib/features/operations/ui/widgets/customer_section.dart create mode 100644 lib/features/operations/ui/widgets/details_section.dart diff --git a/lib/core/data/core_repository.dart b/lib/core/data/core_repository.dart index 6aeac53..2998a5c 100644 --- a/lib/core/data/core_repository.dart +++ b/lib/core/data/core_repository.dart @@ -50,7 +50,7 @@ class CoreRepository { .select() .eq('company_id', companyId) .eq('is_active', true) // Buona pratica - .order('nome'); // O come si chiama il campo nome + .order('name'); // O come si chiama il campo nome return (response as List).map((s) => StoreModel.fromMap(s)).toList(); } 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 ef5e271..54b377d 100644 --- a/lib/features/master_data/providers/data/provider_repository.dart +++ b/lib/features/master_data/providers/data/provider_repository.dart @@ -51,7 +51,7 @@ class ProviderRepository { ) ''') .eq('company_id', companyId) - .order('nome'); + .order('name'); return (response as List).map((m) => ProviderModel.fromMap(m)).toList(); } catch (e) { diff --git a/lib/features/master_data/providers/models/provider_model.dart b/lib/features/master_data/providers/models/provider_model.dart index 1ab1844..5027f30 100644 --- a/lib/features/master_data/providers/models/provider_model.dart +++ b/lib/features/master_data/providers/models/provider_model.dart @@ -3,30 +3,30 @@ import 'package:flux/features/master_data/store/models/store_model.dart'; class ProviderModel extends Equatable { final String? id; - final String nome; - final bool telefoniaFissa; - final bool telefoniaMobile; - final bool energia; - final bool assicurazioni; - final bool intrattenimento; - final bool finanziamenti; + final String name; + final bool landline; + final bool mobile; + final bool energy; + final bool insurance; + final bool entertainment; + final bool financing; final bool telepass; - final bool altro; + final bool other; final bool isActive; final String companyId; final List associatedStores; const ProviderModel({ this.id, - required this.nome, - required this.telefoniaFissa, - required this.telefoniaMobile, - required this.energia, - required this.assicurazioni, - required this.intrattenimento, - required this.finanziamenti, + required this.name, + required this.landline, + required this.mobile, + required this.energy, + required this.insurance, + required this.entertainment, + required this.financing, required this.telepass, - required this.altro, + required this.other, required this.isActive, required this.companyId, this.associatedStores = const [], @@ -46,15 +46,15 @@ class ProviderModel extends Equatable { } return ProviderModel( id: map['id'], - nome: map['nome'], - telefoniaFissa: map['telefonia_fissa'] ?? false, - telefoniaMobile: map['telefonia_mobile'] ?? false, - energia: map['energia'] ?? false, - assicurazioni: map['assicurazioni'] ?? false, - intrattenimento: map['intrattenimento'] ?? false, - finanziamenti: map['finanziamenti'] ?? false, + name: map['name'], + landline: map['landline'] ?? false, + mobile: map['mobile'] ?? false, + energy: map['energy'] ?? false, + insurance: map['insurance'] ?? false, + entertainment: map['entertainment'] ?? false, + financing: map['financing'] ?? false, telepass: map['telepass'] ?? false, - altro: map['altro'] ?? false, + other: map['other'] ?? false, isActive: map['is_active'] ?? true, companyId: map['company_id'], associatedStores: stores, @@ -63,15 +63,15 @@ class ProviderModel extends Equatable { Map toMap() { final map = { - 'nome': nome, - 'telefonia_fissa': telefoniaFissa, - 'telefonia_mobile': telefoniaMobile, - 'energia': energia, - 'assicurazioni': assicurazioni, - 'intrattenimento': intrattenimento, - 'finanziamenti': finanziamenti, + 'name': name, + 'landline': landline, + 'mobile': mobile, + 'energy': energy, + 'insurance': insurance, + 'entertainment': entertainment, + 'financing': financing, 'telepass': telepass, - 'altro': altro, + 'other': other, 'is_active': isActive, 'company_id': companyId, }; @@ -86,15 +86,15 @@ class ProviderModel extends Equatable { @override List get props => [ id, - nome, - telefoniaFissa, - telefoniaMobile, - energia, - assicurazioni, - intrattenimento, - finanziamenti, + name, + landline, + mobile, + energy, + insurance, + entertainment, + financing, telepass, - altro, + other, isActive, companyId, associatedStores, @@ -102,30 +102,30 @@ class ProviderModel extends Equatable { ProviderModel copyWith({ String? id, - String? nome, - bool? telefoniaFissa, - bool? telefoniaMobile, - bool? energia, - bool? assicurazioni, - bool? intrattenimento, - bool? finanziamenti, + String? name, + bool? landline, + bool? mobile, + bool? energy, + bool? insurance, + bool? entertainment, + bool? financing, bool? telepass, - bool? altro, + bool? other, bool? isActive, String? companyId, List? associatedStores, }) { return ProviderModel( id: id ?? this.id, - nome: nome ?? this.nome, - telefoniaFissa: telefoniaFissa ?? this.telefoniaFissa, - telefoniaMobile: telefoniaMobile ?? this.telefoniaMobile, - energia: energia ?? this.energia, - assicurazioni: assicurazioni ?? this.assicurazioni, - intrattenimento: intrattenimento ?? this.intrattenimento, - finanziamenti: finanziamenti ?? this.finanziamenti, + name: name ?? this.name, + landline: landline ?? this.landline, + mobile: mobile ?? this.mobile, + energy: energy ?? this.energy, + insurance: insurance ?? this.insurance, + entertainment: entertainment ?? this.entertainment, + financing: financing ?? this.financing, telepass: telepass ?? this.telepass, - altro: altro ?? this.altro, + other: other ?? this.other, isActive: isActive ?? this.isActive, companyId: companyId ?? this.companyId, associatedStores: associatedStores ?? this.associatedStores, 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 fbf1602..0fead8a 100644 --- a/lib/features/master_data/providers/ui/provider_form_sheet.dart +++ b/lib/features/master_data/providers/ui/provider_form_sheet.dart @@ -15,14 +15,14 @@ class ProviderFormSheet extends StatefulWidget { class _ProviderFormSheetState extends State { late TextEditingController _nameController; - late bool _telefoniaFissa; - late bool _telefoniaMobile; - late bool _energia; - late bool _assicurazioni; - late bool _intrattenimento; - late bool _finanziamenti; + late bool _landline; + late bool _mobile; + late bool _energy; + late bool _insurance; + late bool _entertainment; + late bool _financing; late bool _telepass; - late bool _altro; + late bool _other; late bool _isActive; final List _tempSelectedStoreIds = []; // Per gestire la selezione temporanea dei negozi @@ -34,15 +34,15 @@ class _ProviderFormSheetState extends State { for (final store in p?.associatedStores ?? []) { _tempSelectedStoreIds.add(store.id!); } - _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; - _finanziamenti = p?.finanziamenti ?? false; + _nameController = TextEditingController(text: p?.name ?? ''); + _landline = p?.landline ?? false; + _mobile = p?.mobile ?? false; + _energy = p?.energy ?? false; + _insurance = p?.insurance ?? false; + _entertainment = p?.entertainment ?? false; + _financing = p?.financing ?? false; _telepass = p?.telepass ?? false; - _altro = p?.altro ?? false; + _other = p?.other ?? false; _isActive = p?.isActive ?? true; } @@ -59,15 +59,15 @@ class _ProviderFormSheetState extends State { final cubit = context.read(); 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, - finanziamenti: _finanziamenti, + name: _nameController.text.trim(), + landline: _landline, + mobile: _mobile, + energy: _energy, + insurance: _insurance, + entertainment: _entertainment, + financing: _financing, telepass: _telepass, - altro: _altro, + other: _other, isActive: _isActive, companyId: '', // Verrà ignorato dal toMap e gestito dal Cubit/SessionBloc se hai messo la logica lì @@ -113,33 +113,33 @@ class _ProviderFormSheetState extends State { ), _buildSwitch( "Energia (Luce/Gas)", - _energia, - (v) => setState(() => _energia = v), + _energy, + (v) => setState(() => _energy = v), ), _buildSwitch( "Telefonia Fissa", - _telefoniaFissa, - (v) => setState(() => _telefoniaFissa = v), + _landline, + (v) => setState(() => _landline = v), ), _buildSwitch( "Telefonia Mobile", - _telefoniaMobile, - (v) => setState(() => _telefoniaMobile = v), + _mobile, + (v) => setState(() => _mobile = v), ), _buildSwitch( "Assicurazioni", - _assicurazioni, - (v) => setState(() => _assicurazioni = v), + _insurance, + (v) => setState(() => _insurance = v), ), _buildSwitch( "Intrattenimento", - _intrattenimento, - (v) => setState(() => _intrattenimento = v), + _entertainment, + (v) => setState(() => _entertainment = v), ), _buildSwitch( "Finanziamenti", - _finanziamenti, - (v) => setState(() => _finanziamenti = v), + _financing, + (v) => setState(() => _financing = v), ), _buildSwitch( "Telepass", @@ -148,8 +148,8 @@ class _ProviderFormSheetState extends State { ), _buildSwitch( "Altro/Accessori", - _altro, - (v) => setState(() => _altro = v), + _other, + (v) => setState(() => _other = v), ), const Divider(), _buildSwitch( 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 8d4a259..465f4da 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 @@ -93,7 +93,7 @@ class _ProvidersMasterDataScreenState extends State { ), ), title: Text( - provider.nome, + provider.name, style: const TextStyle(fontWeight: FontWeight.bold), ), subtitle: _buildCardSubtitle( @@ -141,14 +141,13 @@ class _ProvidersMasterDataScreenState extends State { 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.finanziamenti) _smallTag("💰 Fin", Colors.purple), + if (p.landline || p.mobile) _smallTag("📞 Tel", Colors.blue), + if (p.energy) _smallTag("⚡ Energy", Colors.orange), + if (p.insurance) _smallTag("🛡️ Assic", Colors.teal), + if (p.entertainment) _smallTag("📺 Ent", Colors.red), + if (p.financing) _smallTag("💰 Fin", Colors.purple), if (p.telepass) _smallTag("🏎️ Telepass", Colors.yellow), - if (p.altro) _smallTag("📦 Altro", Colors.grey), + if (p.other) _smallTag("📦 Altro", Colors.grey), ], ); } diff --git a/lib/features/master_data/store/data/store_repository.dart b/lib/features/master_data/store/data/store_repository.dart index 01e52c1..0d64d91 100644 --- a/lib/features/master_data/store/data/store_repository.dart +++ b/lib/features/master_data/store/data/store_repository.dart @@ -98,7 +98,7 @@ class StoreRepository { ) ''') .eq('company_id', companyId) - .order('nome'); + .order('name'); return (response as List).map((m) => StoreModel.fromMap(m)).toList(); } catch (e) { diff --git a/lib/features/master_data/store/ui/store_card.dart b/lib/features/master_data/store/ui/store_card.dart index a585254..1c2d8b4 100644 --- a/lib/features/master_data/store/ui/store_card.dart +++ b/lib/features/master_data/store/ui/store_card.dart @@ -191,7 +191,7 @@ class _StoreCardState extends State { (p) => p.id == provider.id, ); return CheckboxListTile( - title: Text(provider.nome), + title: Text(provider.name), value: isAssociated, onChanged: (selected) { if (selected == true) { diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index 1b4b0e9..be48feb 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -165,7 +165,7 @@ class OperationsCubit extends Cubit { ); final updatedOperation = await _repository.saveFullOperation( - operationToSave, + operation: operationToSave, ); emit( @@ -233,6 +233,16 @@ class OperationsCubit extends Cubit { // Creiamo il modello aggiornato // ATTENZIONE: adatta questa logica in base a come è scritto il tuo copyWith! + int? newQuantity; + if (clearQuantity) { + newQuantity = 1; + } + if (quantity != null && quantity <= 0) { + newQuantity = 0; + } + if (quantity != null && quantity > 0) { + newQuantity = quantity; + } final updated = current.copyWith( customerId: customerId, customerDisplayName: customerDisplayName, @@ -242,7 +252,7 @@ class OperationsCubit extends Cubit { providerDisplayName: clearProvider ? null : (providerDisplayName ?? current.providerDisplayName), - quantity: clearQuantity ? 1 : (quantity ?? current.quantity), + quantity: newQuantity, type: clearType ? null : (type ?? current.type), subtype: clearSubtype ? null : (subtype ?? current.subtype), expirationDate: clearExpiration diff --git a/lib/features/operations/data/operations_repository.dart b/lib/features/operations/data/operations_repository.dart index bafa985..2d38fb8 100644 --- a/lib/features/operations/data/operations_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -99,38 +99,35 @@ class OperationsRepository { } // --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) --- - Future saveFullOperation(OperationModel operation) async { + Future saveFullOperation({ + required OperationModel operation, + }) async { try { - // 1. Upsert del record principale - final operationData = await _supabase + // 1. Salvataggio classico dell'operazione corrente + final response = await _supabase .from('operation') .upsert(operation.toMap()) - .select() + .select( + '*, provider(*), model(*), store(*), staff_member(*), customer(*), attachment(*)', + ) .single(); - final String newId = operationData['id']; + final savedOperation = OperationModel.fromMap(response); - // 5. GRAN FINALE: RECUPERO DEL MODELLO COMPLETO E AGGIORNATO - // Interroghiamo Supabase per farci restituire la pratica con TUTTI gli ID generati - // (inclusi quelli della tabella operation_file appena inseriti) - final updatedOperationData = await _supabase - .from('operation') - .select(''' - *, - staff_member(name), - store(name), - provider(name), - model(name_with_brand), - customer(name), - attachment(*) - ''') - .eq('id', newId) - .single(); + // 2. ALLINEAMENTO BATCH SEMPRE ATTIVO! + if (operation.batchUuid.isNotEmpty) { + await _supabase + .from('operation') + .update({'note': operation.note}) // Spalmiamo la nota attuale + .eq( + 'batch_uuid', + operation.batchUuid, + ); // Su tutte le pratiche di questo scontrino + } - return OperationModel.fromMap(updatedOperationData); + return savedOperation; } catch (e) { - // Qui potresti aggiungere una logica di "rollback manuale" se necessario - throw Exception('$e'); + throw Exception("Errore nel salvataggio dell'operazione: $e"); } } diff --git a/lib/features/operations/models/operation_model.dart b/lib/features/operations/models/operation_model.dart index ef2c0b6..57efa90 100644 --- a/lib/features/operations/models/operation_model.dart +++ b/lib/features/operations/models/operation_model.dart @@ -169,35 +169,49 @@ class OperationModel extends Equatable { factory OperationModel.fromMap(Map map) { return OperationModel( - id: map['id'], + id: map['id'] as String?, createdAt: map['created_at'] != null ? DateTime.parse(map['created_at']) : null, type: map['type'] as String? ?? '', subtype: map['sub_type'] as String?, - providerId: map['provider_id'] as String? ?? '', - providerDisplayName: "${map['provider']['name']}".myFormat(), - modelId: map['model_id'] as String? ?? '', - modelDisplayName: "${map['model']['name_with_brand'] ?? ''}".myFormat(), - description: map['description'] as String? ?? '', + + // I campi relazionali nullabili restano rigorosamente null! + providerId: map['provider_id'] as String?, + // MAGIA ANTI-CRASH: Usiamo ?['chiave'] per non far esplodere i join vuoti + providerDisplayName: (map['provider']?['name'] as String?)?.myFormat(), + + modelId: map['model_id'] as String?, + modelDisplayName: (map['model']?['name_with_brand'] as String?) + ?.myFormat(), + + description: map['description'] as String?, expirationDate: map['expiration_date'] != null ? DateTime.parse(map['expiration_date']) : null, note: map['note'] as String? ?? '', - showInDashboard: map['show_in_dashboard'] as bool, - batchUuid: map['batch_uuid'] as String, + showInDashboard: map['show_in_dashboard'] as bool? ?? true, + batchUuid: map['batch_uuid'] as String? ?? '', companyId: map['company_id'] as String, - storeId: map['store_id'] as String? ?? '', - storeDisplayName: "${map['store']['name']}".myFormat(), + + storeId: + map['store_id'] as String? ?? + '', // Questo è non-nullable nella tua classe + storeDisplayName: (map['store']?['name'] as String?)?.myFormat(), + quantity: map['quantity'] is int ? map['quantity'] - : int.tryParse(map['quantity']?.toString() ?? '0') ?? 0, - staffId: map['staff_id'] as String? ?? '', - staffDisplayName: "${map['staff_member']['name'] ?? ''}".myFormat(), - lastCampaignId: map['last_campaign_id'] as String? ?? '', - status: OperationStatus.fromString(map['status']), - customerId: map['customer_id'] as String? ?? '', - customerDisplayName: "${map['customer']['name'] ?? ''}".myFormat(), + : int.tryParse(map['quantity']?.toString() ?? '1') ?? 1, + + staffId: map['staff_id'] as String?, + staffDisplayName: (map['staff_member']?['name'] as String?)?.myFormat(), + + lastCampaignId: map['last_campaign_id'] as String?, + status: OperationStatus.fromString(map['status'] ?? 'draft'), + + customerId: map['customer_id'] as String?, + customerDisplayName: (map['customer']?['name'] as String?)?.myFormat(), + attachments: (map['attachment'] as List?) ?.map((x) => AttachmentModel.fromMap(x)) diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index bbdf1e7..ca9b24b 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -1,12 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.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/models/operation_model.dart'; +import 'package:flux/features/operations/ui/widgets/customer_section.dart'; +import 'package:flux/features/operations/ui/widgets/details_section.dart'; // ASSICURATI DEL PATH // import 'package:flux/features/attachments/ui/operation_files_section.dart'; class OperationFormScreen extends StatefulWidget { @@ -26,7 +23,6 @@ class OperationFormScreen extends StatefulWidget { class _OperationFormScreenState extends State { final _formKey = GlobalKey(); - // TEXT CONTROLLERS (Unici detentori di stato locale per evitare lag) final _referenceController = TextEditingController(); final _noteController = TextEditingController(); final _freeTextSubtypeController = TextEditingController(); @@ -43,40 +39,11 @@ class _OperationFormScreenState extends State { '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; @override void initState() { super.initState(); - // Inizializziamo il form nel Cubit context.read().initOperationForm( existingOperation: widget.existingOperation, operationId: widget.operationId, @@ -91,7 +58,6 @@ class _OperationFormScreenState extends State { super.dispose(); } - // Sincronizza SOLO i testi liberi quando il Cubit ha caricato da DB void _syncTextControllers(OperationModel model) { if (_referenceController.text.isEmpty && model.reference.isNotEmpty) { _referenceController.text = model.reference; @@ -107,23 +73,20 @@ class _OperationFormScreenState extends State { _isInitialized = true; } - // --- LOGICA DI SALVATAGGIO --- void _saveOperation({required bool keepAdding}) { if (_formKey.currentState!.validate()) { final cubit = context.read(); final currentOperation = cubit.state.currentOperation!; - // 1. "Travasiamo" i testi liberi dai controller al Modello prima di salvare final operationToSave = currentOperation.copyWith( reference: _referenceController.text, note: _noteController.text, - // subtype: currentOperation.type == 'Custom' ? _customSubtypeController.text : currentOperation.subtype, // <-- Scommenta quando aggiungi subtype + subtype: ['Entertainment', 'Custom'].contains(currentOperation.type) + ? _freeTextSubtypeController.text + : currentOperation.subtype, ); - // 2. Aggiorniamo il Cubit con i testi cubit.initOperationForm(existingOperation: operationToSave); - - // 3. Salviamo! cubit.saveCurrentOperation( targetStatus: OperationStatus.ok, shouldPop: !keepAdding, @@ -131,165 +94,6 @@ class _OperationFormScreenState extends State { } } - // --- MODALE SELEZIONE CLIENTE --- - void _showCustomerModal() { - String currentSearchQuery = ''; - - showModalBottomSheet( - context: context, - isScrollControlled: true, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), - ), - builder: (modalContext) { - return DraggableScrollableSheet( - initialChildSize: 0.8, - minChildSize: 0.5, - maxChildSize: 0.95, - expand: false, - builder: (_, scrollController) { - return Column( - children: [ - // Header - Padding( - padding: const EdgeInsets.all(16.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Seleziona Cliente', - style: Theme.of(context).textTheme.titleLarge, - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(modalContext), - ), - ], - ), - ), - // Barra di Ricerca - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: TextField( - autofocus: true, - decoration: InputDecoration( - hintText: 'Cerca per nome, telefono o email...', - prefixIcon: const Icon(Icons.search), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - onChanged: (query) { - currentSearchQuery = query; - context.read().searchCustomers(query); - }, - ), - ), - // Pulsante Nuovo Cliente - Padding( - padding: const EdgeInsets.all(16.0), - child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - minimumSize: const Size.fromHeight(48), - ), - icon: const Icon(Icons.person_add), - label: const Text('Crea Nuovo Cliente'), - onPressed: () async { - final OperationsCubit operationsCubit = context - .read(); - - // APRIAMO LA DIALOG RAPIDA CON LA MAGIA DEL BLOC PROVIDER - final newCustomer = await showDialog( - context: context, - builder: (dialogContext) { - return BlocProvider.value( - value: context.read(), - 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); - } - } - }, - ), - ), - const Divider(), - // Lista Clienti dal Bloc - Expanded( - child: BlocBuilder( - builder: (context, state) { - if (state.status == CustomersStatus.loading) { - return const Center(child: CircularProgressIndicator()); - } - if (state.customers.isEmpty) { - return const Center( - child: Text( - 'Nessun cliente trovato.', - style: TextStyle(color: Colors.grey), - ), - ); - } - - return ListView.builder( - controller: scrollController, - itemCount: state.customers.length, - itemBuilder: (context, index) { - final customer = state.customers[index]; - return ListTile( - leading: CircleAvatar( - child: Text( - customer.name.substring(0, 1).toUpperCase(), - ), - ), - title: Text( - customer.name, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ), - subtitle: Text( - '${customer.phoneNumber} • ${customer.email}', - ), - onTap: () { - // Aggiorniamo il form tramite il Cubit delle operazioni - context - .read() - .updateOperationFields( - customerId: customer.id, // customer.id - customerDisplayName: - customer.name, // customer.name - ); - Navigator.pop(modalContext); - }, - ); - }, - ); - }, - ), - ), - ], - ); - }, - ); - }, - ); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -299,7 +103,6 @@ class _OperationFormScreenState extends State { previous.status != current.status || previous.currentOperation?.id != current.currentOperation?.id, listener: (context, state) { - // Sincronizzazione iniziale if (state.status == OperationsStatus.ready && state.currentOperation != null && !_isInitialized) { @@ -315,9 +118,6 @@ class _OperationFormScreenState extends State { content: Text('Servizio aggiunto! Inserisci il prossimo.'), ), ); - // Ripuliamo SOLO i testi liberi (il Cubit gestisce già i suoi reset) - _referenceController.clear(); - _noteController.clear(); _freeTextSubtypeController.clear(); } else if (state.status == OperationsStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( @@ -329,7 +129,6 @@ class _OperationFormScreenState extends State { } }, builder: (context, state) { - // Loader iniziale if (!_isInitialized && (widget.operationId != null || widget.existingOperation != null) && state.status == OperationsStatus.loading) { @@ -351,9 +150,7 @@ class _OperationFormScreenState extends State { child: LayoutBuilder( builder: (context, constraints) { final isDesktop = constraints.maxWidth > 900; - if (isDesktop) { - // --- LAYOUT DESKTOP --- return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -375,7 +172,6 @@ class _OperationFormScreenState extends State { ], ); } else { - // --- LAYOUT MOBILE --- return SingleChildScrollView( padding: const EdgeInsets.all(16.0), child: Column( @@ -392,7 +188,6 @@ class _OperationFormScreenState extends State { }, ), ), - // --- LA CASSA --- bottomNavigationBar: SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), @@ -438,8 +233,6 @@ class _OperationFormScreenState extends State { ); } - // --- COSTRUTTORI UI COMPONENTI --- - Widget _buildMainFormContent(ThemeData theme, OperationsState state) { final currentOp = state.currentOperation; final currentType = currentOp?.type ?? 'AL'; @@ -447,9 +240,8 @@ class _OperationFormScreenState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // --- BLOCCO 1: CONTESTO --- _buildSectionTitle('Cliente & Riferimento'), - _buildCustomerSelector(currentOp), + CustomerSection(currentOp: currentOp), const SizedBox(height: 16), TextFormField( controller: _referenceController, @@ -460,7 +252,6 @@ class _OperationFormScreenState extends State { ), const Divider(height: 32), - // --- BLOCCO 2: TIPO DI OPERAZIONE --- _buildSectionTitle('Cosa stiamo facendo?'), Wrap( spacing: 8.0, @@ -471,7 +262,6 @@ class _OperationFormScreenState extends State { selected: currentType == type, onSelected: (selected) { if (selected) { - // Diciamo al Cubit di cambiare tipo e spianare i campi dipendenti context.read().setTypeWithSmartDefault(type); } }, @@ -480,155 +270,13 @@ class _OperationFormScreenState extends State { ), const Divider(height: 32), - // --- BLOCCO 3: DETTAGLI REATTIVI --- _buildSectionTitle('Dettagli Servizio'), - - // PROVIDER (Mostrato quasi sempre) - ListTile( - title: const Text('Seleziona Gestore'), - subtitle: Text( - (currentOp?.providerDisplayName != null && - 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), - shape: RoundedRectangleBorder( - side: BorderSide(color: theme.dividerColor), - borderRadius: BorderRadius.circular(8), - ), - onTap: () { - _showProviderModal(currentType); - }, + DetailsSection( + currentOp: currentOp, + currentType: currentType, + freeTextSubtypeController: _freeTextSubtypeController, + durationQuickPicks: _buildDurationQuickPicks(currentOp), ), - const SizedBox(height: 16), - - // 1. SCENARIO ENERGY (Dropdown Fisso) - if (currentType == 'Energy') ...[ - DropdownButtonFormField( - initialValue: - (currentOp?.subtype != null && currentOp!.subtype!.isNotEmpty) - ? currentOp.subtype - : null, - decoration: const InputDecoration(labelText: 'Dettaglio Fornitura'), - items: [ - 'Luce', - 'Gas', - 'Dual', - ].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), - onChanged: (val) { - if (val != null) { - context.read().updateOperationFields( - subtype: val, - ); - } - }, - ), - const SizedBox(height: 16), - ], - - // 2. SCENARIO FIN (Ricerca Modello/Prodotto) - 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( - controller: _freeTextSubtypeController, - decoration: InputDecoration( - labelText: currentType == 'Entertainment' - ? 'Piattaforma (es. Netflix, DAZN, Spotify...)' - : 'Specifica il servizio (es. Monopattino)', - ), - ), - const SizedBox(height: 16), - ], - - // SCADENZA (Reattivo per tipi complessi) - if ([ - 'Energy', - 'Fin', - 'Entertainment', - 'Custom', - ].contains(currentType)) ...[ - const SizedBox(height: 8), - - // --- I CHIPS RAPIDI --- - _buildDurationQuickPicks(currentOp), - - const SizedBox(height: 16), - - // --- IL SELETTORE MANUALE --- - ListTile( - title: const Text('Data di Scadenza Effettiva'), - subtitle: Text( - currentOp?.expirationDate != null - ? "${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_month, color: Colors.blue), - tileColor: Colors.blue.withValues(alpha: 0.05), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: const BorderSide(color: Colors.blue, width: 0.5), - ), - onTap: () async { - final OperationsCubit operationsCubit = context - .read(); - final date = await showDatePicker( - context: context, - initialDate: - currentOp?.expirationDate ?? - DateTime.now().add(const Duration(days: 365)), - firstDate: DateTime.now(), - lastDate: DateTime.now().add(const Duration(days: 3650)), - ); - if (date != null) { - operationsCubit.updateOperationFields(expirationDate: date); - } - }, - ), - const SizedBox(height: 16), - ], // QUANTITÀ Row( @@ -662,7 +310,6 @@ class _OperationFormScreenState extends State { ), const Divider(height: 32), - // --- BLOCCO 5: ALLEGATI --- _buildSectionTitle('Documenti & Foto'), const Center( child: Text( @@ -676,7 +323,6 @@ class _OperationFormScreenState extends State { Widget _buildDurationQuickPicks(OperationModel? currentOp) { final durations = [3, 6, 12, 24, 30, 36, 48]; - return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -700,13 +346,12 @@ class _OperationFormScreenState extends State { backgroundColor: Colors.blue.withValues(alpha: 0.05), onPressed: () { final now = DateTime.now(); - final newDate = DateTime( - now.year, - now.month + months, - now.day, - ); context.read().updateOperationFields( - expirationDate: newDate, + expirationDate: DateTime( + now.year, + now.month + months, + now.day, + ), ); }, ), @@ -733,22 +378,19 @@ class _OperationFormScreenState extends State { border: OutlineInputBorder(), ), ); - - if (isDesktop) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - title, - const SizedBox(height: 8), - Expanded(child: noteField), - ], - ); - } else { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [title, const SizedBox(height: 8), noteField], - ); - } + return isDesktop + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title, + const SizedBox(height: 8), + Expanded(child: noteField), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [title, const SizedBox(height: 8), noteField], + ); } Widget _buildSectionTitle(String title) { @@ -762,284 +404,4 @@ class _OperationFormScreenState extends State { ), ); } - - Widget _buildCustomerSelector(OperationModel? currentOp) { - final hasCustomer = - currentOp?.customerId != null && currentOp!.customerId!.isNotEmpty; - - return InkWell( - onTap: _showCustomerModal, - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - border: Border.all(color: Theme.of(context).colorScheme.primary), - borderRadius: BorderRadius.circular(8), - color: Theme.of( - context, - ).colorScheme.primaryContainer.withValues(alpha: 0.2), - ), - child: Row( - children: [ - const Icon(Icons.person), - const SizedBox(width: 12), - Expanded( - child: Text( - hasCustomer - ? currentOp.customerDisplayName ?? '' - : 'Seleziona Cliente *', - style: TextStyle( - fontWeight: hasCustomer ? FontWeight.bold : FontWeight.normal, - color: hasCustomer ? null : Colors.grey, - ), - ), - ), - const Icon(Icons.search), - ], - ), - ), - ); - } - - 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( - // <--- 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() - .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().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(); - - // 1. Recuperiamo la lista dei brand (adatta questo in base a dove tieni i brand nel tuo stato) - final existingBrands = context - .read() - .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(), - 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( - // <--- 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() - .updateOperationFields( - modelId: deviceModel.id, - modelDisplayName: deviceModel.nameWithBrand, - ); - Navigator.pop(modalContext); - }, - ); - }, - ); - }, - ), - ), - ], - ); - }, - ); - }, - ); - } } diff --git a/lib/features/operations/ui/widgets/customer_section.dart b/lib/features/operations/ui/widgets/customer_section.dart new file mode 100644 index 0000000..88ec939 --- /dev/null +++ b/lib/features/operations/ui/widgets/customer_section.dart @@ -0,0 +1,222 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/features/customers/blocs/customers_cubit.dart'; +import 'package:flux/features/customers/ui/quick_customer_dialog.dart'; +import 'package:flux/features/operations/blocs/operations_cubit.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; + +class CustomerSection extends StatelessWidget { + final OperationModel? currentOp; + const CustomerSection({super.key, required this.currentOp}); + + @override + Widget build(BuildContext context) { + final hasCustomer = + currentOp?.customerId != null && currentOp!.customerId!.isNotEmpty; + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Text( + 'Cliente', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + InkWell( + onTap: () => _showCustomerModal(context), // Passiamo il context! + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all(color: theme.colorScheme.primary), + borderRadius: BorderRadius.circular(8), + color: theme.colorScheme.primaryContainer.withValues(alpha: 0.2), + ), + child: Row( + children: [ + const Icon(Icons.person), + const SizedBox(width: 12), + Expanded( + child: Text( + hasCustomer + ? currentOp!.customerDisplayName! + : 'Seleziona Cliente *', + style: TextStyle( + fontWeight: hasCustomer + ? FontWeight.bold + : FontWeight.normal, + color: hasCustomer ? null : Colors.grey, + ), + ), + ), + const Icon(Icons.search), + ], + ), + ), + ), + ], + ); + } + + // --- MODALE SELEZIONE CLIENTE --- + void _showCustomerModal(BuildContext context) { + String currentSearchQuery = ''; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (modalContext) { + return DraggableScrollableSheet( + initialChildSize: 0.8, + minChildSize: 0.5, + maxChildSize: 0.95, + expand: false, + builder: (_, scrollController) { + return Column( + children: [ + // Header + Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Seleziona Cliente', + style: Theme.of(context).textTheme.titleLarge, + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(modalContext), + ), + ], + ), + ), + // Barra di Ricerca + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: TextField( + autofocus: true, + decoration: InputDecoration( + hintText: 'Cerca per nome, telefono o email...', + prefixIcon: const Icon(Icons.search), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + onChanged: (query) { + currentSearchQuery = query; + context.read().searchCustomers(query); + }, + ), + ), + // Pulsante Nuovo Cliente + Padding( + padding: const EdgeInsets.all(16.0), + child: ElevatedButton.icon( + style: ElevatedButton.styleFrom( + minimumSize: const Size.fromHeight(48), + ), + icon: const Icon(Icons.person_add), + label: const Text('Crea Nuovo Cliente'), + onPressed: () async { + final OperationsCubit operationsCubit = context + .read(); + + // APRIAMO LA DIALOG RAPIDA CON LA MAGIA DEL BLOC PROVIDER + final newCustomer = await showDialog( + context: context, + builder: (dialogContext) { + return BlocProvider.value( + value: context.read(), + 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); + } + } + }, + ), + ), + const Divider(), + // Lista Clienti dal Bloc + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state.status == CustomersStatus.loading) { + return const Center(child: CircularProgressIndicator()); + } + if (state.customers.isEmpty) { + return const Center( + child: Text( + 'Nessun cliente trovato.', + style: TextStyle(color: Colors.grey), + ), + ); + } + + return ListView.builder( + controller: scrollController, + itemCount: state.customers.length, + itemBuilder: (context, index) { + final customer = state.customers[index]; + return ListTile( + leading: CircleAvatar( + child: Text( + customer.name.substring(0, 1).toUpperCase(), + ), + ), + title: Text( + customer.name, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text( + '${customer.phoneNumber} • ${customer.email}', + ), + onTap: () { + // Aggiorniamo il form tramite il Cubit delle operazioni + context + .read() + .updateOperationFields( + customerId: customer.id, // customer.id + customerDisplayName: + customer.name, // customer.name + ); + Navigator.pop(modalContext); + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } +} diff --git a/lib/features/operations/ui/widgets/details_section.dart b/lib/features/operations/ui/widgets/details_section.dart new file mode 100644 index 0000000..343db76 --- /dev/null +++ b/lib/features/operations/ui/widgets/details_section.dart @@ -0,0 +1,413 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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/models/operation_model.dart'; + +class DetailsSection extends StatelessWidget { + final OperationModel? currentOp; + final String currentType; + final TextEditingController freeTextSubtypeController; + final Widget durationQuickPicks; + + const DetailsSection({ + super.key, + required this.currentOp, + required this.currentType, + required this.freeTextSubtypeController, + required this.durationQuickPicks, + }); + + bool _doesProviderMatchOperationType(dynamic provider, String operationType) { + if (operationType == 'Custom') return true; + switch (operationType) { + case 'AL': + case 'MNP': + return provider.mobile == true; + case 'NIP': + return provider.landline == true; + case 'UNICA': + return provider.landline == true || provider.mobile == true; + case 'Energy': + return provider.energy == true; + case 'Fin': + return provider.financing == true; + case 'Entertainment': + return provider.entertainment == true; + case 'TELEPASS': + return provider.telepass == true; + default: + return true; + } + } + + void _showProviderModal(BuildContext context, String operationType) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + builder: (modalContext) { + return DraggableScrollableSheet( + initialChildSize: 0.5, + 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( + builder: (context, state) { + if (state.isLoading) { + return const Center(child: CircularProgressIndicator()); + } + + final allProviders = state.activeProviders; + final filteredProviders = allProviders + .where( + (p) => _doesProviderMatchOperationType( + p, + operationType, + ), + ) + .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.name, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + onTap: () { + context + .read() + .updateOperationFields( + providerId: provider.id, + providerDisplayName: provider.name, + ); + Navigator.pop(modalContext); + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } + + void _showModelModal(BuildContext context) { + 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().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 = context.read(); + final existingBrands = context + .read() + .state + .brands; + + final newModel = await showDialog( + context: context, + builder: (dialogContext) { + return BlocProvider.value( + value: context.read(), + child: QuickProductDialog( + existingBrands: existingBrands, + ), + ); + }, + ); + + if (newModel != null) { + operationsCubit.updateOperationFields( + modelId: newModel.id, + modelDisplayName: newModel.nameWithBrand, + ); + if (context.mounted) Navigator.pop(modalContext); + } + }, + ), + ), + const Divider(), + Expanded( + child: BlocBuilder( + builder: (context, state) { + return ListView.builder( + controller: scrollController, + itemCount: 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() + .updateOperationFields( + modelId: deviceModel.id, + modelDisplayName: deviceModel.nameWithBrand, + ); + Navigator.pop(modalContext); + }, + ); + }, + ); + }, + ), + ), + ], + ); + }, + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // PROVIDER (Mostrato quasi sempre) + ListTile( + title: const Text('Seleziona Gestore'), + subtitle: Text( + (currentOp?.providerDisplayName != null && + 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), + shape: RoundedRectangleBorder( + side: BorderSide(color: theme.dividerColor), + borderRadius: BorderRadius.circular(8), + ), + onTap: () => _showProviderModal(context, currentType), + ), + const SizedBox(height: 16), + + // 1. SCENARIO ENERGY (Dropdown Fisso) + if (currentType == 'Energy') ...[ + DropdownButtonFormField( + initialValue: + (currentOp?.subtype != null && currentOp!.subtype!.isNotEmpty) + ? currentOp!.subtype + : null, + decoration: const InputDecoration(labelText: 'Dettaglio Fornitura'), + items: [ + 'Luce', + 'Gas', + 'Dual', + ].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), + onChanged: (val) { + if (val != null) { + context.read().updateOperationFields( + subtype: val, + ); + } + }, + ), + const SizedBox(height: 16), + ], + + // 2. SCENARIO FIN (Ricerca Modello/Prodotto) + 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(context), + ), + const SizedBox(height: 16), + ], + + // 3. SCENARIO ENTERTAINMENT O CUSTOM (Testo libero) + if (currentType == 'Entertainment' || currentType == 'Custom') ...[ + TextFormField( + controller: freeTextSubtypeController, + decoration: InputDecoration( + labelText: currentType == 'Entertainment' + ? 'Piattaforma (es. Netflix, DAZN, Spotify...)' + : 'Specifica il servizio (es. Monopattino)', + ), + ), + const SizedBox(height: 16), + ], + + // SCADENZA (Reattivo per tipi complessi) + if ([ + 'Energy', + 'Fin', + 'Entertainment', + 'Custom', + ].contains(currentType)) ...[ + const SizedBox(height: 8), + durationQuickPicks, // Passiamo i chips dall'esterno + const SizedBox(height: 16), + ListTile( + title: const Text('Data di Scadenza Effettiva'), + subtitle: Text( + currentOp?.expirationDate != null + ? "${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_month, color: Colors.blue), + tileColor: Colors.blue.withValues(alpha: 0.05), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: Colors.blue, width: 0.5), + ), + onTap: () async { + final date = await showDatePicker( + context: context, + initialDate: + currentOp?.expirationDate ?? + DateTime.now().add(const Duration(days: 365)), + firstDate: DateTime.now(), + lastDate: DateTime.now().add(const Duration(days: 3650)), + ); + if (date != null && context.mounted) { + context.read().updateOperationFields( + expirationDate: date, + ); + } + }, + ), + const SizedBox(height: 16), + ], + ], + ); + } +} -- 2.43.0 From eb66a707cc89dbd7c78c64509a6a74e59a006c35 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sun, 3 May 2026 12:44:51 +0200 Subject: [PATCH 15/18] s Co-authored-by: Copilot --- .../operations/blocs/operations_cubit.dart | 6 + .../operations/ui/operation_form_screen.dart | 25 +++- .../operations/ui/widgets/staff_section.dart | 136 ++++++++++++++++++ 3 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 lib/features/operations/ui/widgets/staff_section.dart diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index be48feb..b148b8a 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -89,6 +89,8 @@ class OperationsCubit extends Cubit { void initOperationForm({ OperationModel? existingOperation, String? operationId, + String? staffId, + String? staffDisplayName, }) async { if (existingOperation != null) { emit( @@ -219,6 +221,8 @@ class OperationsCubit extends Cubit { int? quantity, String? modelId, String? modelDisplayName, + String? staffId, + String? staffDisplayName, // Aggiungiamo questi flag per forzare la pulizia dei campi quando cambi tipo bool clearProvider = false, bool clearType = false, @@ -262,6 +266,8 @@ class OperationsCubit extends Cubit { modelDisplayName: clearModel ? null : (modelDisplayName ?? current.modelDisplayName), + staffId: staffId ?? current.staffId, + staffDisplayName: staffDisplayName ?? current.staffDisplayName, ); emit(state.copyWith(currentOperation: updated)); diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index ca9b24b..ed6f233 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_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/ui/widgets/customer_section.dart'; -import 'package:flux/features/operations/ui/widgets/details_section.dart'; // ASSICURATI DEL PATH +import 'package:flux/features/operations/ui/widgets/details_section.dart'; +import 'package:flux/features/operations/ui/widgets/staff_section.dart'; +import 'package:get_it/get_it.dart'; // ASSICURATI DEL PATH // import 'package:flux/features/attachments/ui/operation_files_section.dart'; class OperationFormScreen extends StatefulWidget { @@ -44,10 +47,26 @@ class _OperationFormScreenState extends State { @override void initState() { super.initState(); - context.read().initOperationForm( + final cubit = context.read(); + final currentLoggedStaff = GetIt.I + .get() + .state + .currentStaffMember!; + + // 1. Diciamo al Cubit di prepararsi + cubit.initOperationForm( existingOperation: widget.existingOperation, operationId: widget.operationId, + staffId: currentLoggedStaff.id, + staffDisplayName: currentLoggedStaff.name, ); + + // 2. IL TRUCCO MAGICO: + // Se abbiamo passato existingOperation, il Cubit si è appena aggiornato. + // Lo stato è già pronto, quindi sincronizziamo i controller SUBITO! + if (cubit.state.currentOperation != null) { + _syncTextControllers(cubit.state.currentOperation!); + } } @override @@ -240,6 +259,8 @@ class _OperationFormScreenState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + StaffSection(currentOp: currentOp), + const Divider(height: 50), _buildSectionTitle('Cliente & Riferimento'), CustomerSection(currentOp: currentOp), const SizedBox(height: 16), diff --git a/lib/features/operations/ui/widgets/staff_section.dart b/lib/features/operations/ui/widgets/staff_section.dart new file mode 100644 index 0000000..7c41e07 --- /dev/null +++ b/lib/features/operations/ui/widgets/staff_section.dart @@ -0,0 +1,136 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/features/operations/blocs/operations_cubit.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +// IMPORTA IL TUO CUBIT DELLO STAFF +// import 'package:flux/features/staff/blocs/staff_cubit.dart'; + +class StaffSection extends StatelessWidget { + final OperationModel? currentOp; + + const StaffSection({super.key, required this.currentOp}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final selectedStaffId = currentOp?.staffId; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Text( + 'Operatore', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + + // Ascoltiamo il Cubit che contiene la lista dei membri dello staff del negozio + // BlocBuilder( + // builder: (context, state) { + // final staffMembers = state.storeStaff; + + // Sostituisci questo blocco 'simulato' con i dati veri del tuo BlocBuilder + Builder( + builder: (context) { + // Dati finti per farti vedere la UI, piallali quando attacchi il BlocBuilder! + final staffMembers = [ + {'id': '1', 'name': 'Tu (Admin)'}, + {'id': '2', 'name': 'Marco'}, + {'id': '3', 'name': 'Giulia'}, + ]; + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: staffMembers.map((staff) { + final staffId = staff['id'] as String; + final staffName = staff['name'] as String; + + final isSelected = staffId == selectedStaffId; + + return GestureDetector( + onTap: () { + // Aggiorniamo la form con un solo tap! + context.read().updateOperationFields( + staffId: staffId, + staffDisplayName: staffName, + ); + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + margin: const EdgeInsets.only(right: 12.0), + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 10.0, + ), + decoration: BoxDecoration( + color: isSelected + ? theme.colorScheme.primary + : theme.colorScheme.surface, + borderRadius: BorderRadius.circular(30), + border: Border.all( + color: isSelected + ? theme.colorScheme.primary + : theme.dividerColor, + width: 1.5, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: theme.colorScheme.primary.withValues( + alpha: 0.3, + ), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ] + : null, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + radius: 12, + backgroundColor: isSelected + ? Colors.white + : theme.colorScheme.primaryContainer, + child: Text( + staffName.substring(0, 1).toUpperCase(), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: isSelected + ? theme.colorScheme.primary + : theme.colorScheme.onPrimaryContainer, + ), + ), + ), + const SizedBox(width: 8), + Text( + staffName, + style: TextStyle( + fontWeight: isSelected + ? FontWeight.bold + : FontWeight.w500, + color: isSelected + ? Colors.white + : theme.colorScheme.onSurface, + ), + ), + ], + ), + ), + ); + }).toList(), + ), + ); + }, + ), + ], + ); + } +} -- 2.43.0 From 212f33ff516842565771bc3f0c28665bc04af5b4 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Sun, 3 May 2026 13:03:50 +0200 Subject: [PATCH 16/18] aggiunta staff section a OperationFormScreen Co-authored-by: Copilot --- lib/core/routes/app_router.dart | 22 +++++++++- .../master_data/staff/blocs/staff_cubit.dart | 2 +- .../master_data/staff/blocs/staff_state.dart | 5 +++ .../operations/ui/widgets/staff_section.dart | 43 +++++++++---------- 4 files changed, 47 insertions(+), 25 deletions(-) diff --git a/lib/core/routes/app_router.dart b/lib/core/routes/app_router.dart index 1ea8bba..1ac0980 100644 --- a/lib/core/routes/app_router.dart +++ b/lib/core/routes/app_router.dart @@ -19,6 +19,7 @@ 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/providers/blocs/provider_cubit.dart'; import 'package:flux/features/master_data/providers/ui/providers_master_data_screen.dart'; +import 'package:flux/features/master_data/staff/blocs/staff_cubit.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/onboarding/blocs/onboarding_cubit.dart'; @@ -179,12 +180,19 @@ class AppRouter { builder: (context, state) { final existingOperation = state.extra as OperationModel?; final operationId = state.uri.queryParameters['operationId']; + final currentStoreId = GetIt.I + .get() + .state + .currentStore! + .id!; context.read().loadCustomers(); context.read().loadActiveProvidersForStore( - GetIt.I.get().state.currentStore!.id!, + currentStoreId, ); context.read().loadModels(); context.read().loadBrands(); + context.read().loadStaffForStore(currentStoreId); + return BlocProvider( create: (context) => OperationFilesBloc( operationId: operationId ?? existingOperation?.id, @@ -202,6 +210,18 @@ class AppRouter { final operationId = state.pathParameters['id']!; final operationName = state.uri.queryParameters['name'] ?? 'Pratica'; + final currentStoreId = GetIt.I + .get() + .state + .currentStore! + .id!; + context.read().loadCustomers(); + context.read().loadActiveProvidersForStore( + currentStoreId, + ); + context.read().loadModels(); + context.read().loadBrands(); + context.read().loadStaffForStore(currentStoreId); return BlocProvider( create: (context) => OperationFilesBloc(operationId: operationId), child: OperationMobileUploadScreen( diff --git a/lib/features/master_data/staff/blocs/staff_cubit.dart b/lib/features/master_data/staff/blocs/staff_cubit.dart index 8cd77f5..d231167 100644 --- a/lib/features/master_data/staff/blocs/staff_cubit.dart +++ b/lib/features/master_data/staff/blocs/staff_cubit.dart @@ -56,7 +56,7 @@ class StaffCubit extends Cubit { state.staffByStore, ); newMap[storeId] = staffInStore; - emit(state.copyWith(staffByStore: newMap)); + emit(state.copyWith(staffByStore: newMap, storeStaff: staffInStore)); } catch (e) { emit(state.copyWith(status: StaffStatus.error, error: e.toString())); } diff --git a/lib/features/master_data/staff/blocs/staff_state.dart b/lib/features/master_data/staff/blocs/staff_state.dart index 2a3b7d4..f3d0723 100644 --- a/lib/features/master_data/staff/blocs/staff_state.dart +++ b/lib/features/master_data/staff/blocs/staff_state.dart @@ -7,6 +7,7 @@ class StaffState extends Equatable { final List allStaff; final Map> storesByStaff; final Map> staffByStore; + final List storeStaff; final String? error; const StaffState({ @@ -14,6 +15,7 @@ class StaffState extends Equatable { this.allStaff = const [], this.storesByStaff = const {}, this.staffByStore = const {}, + this.storeStaff = const [], this.error, }); @@ -22,6 +24,7 @@ class StaffState extends Equatable { List? allStaff, Map>? storesByStaff, Map>? staffByStore, + List? storeStaff, String? error, }) { return StaffState( @@ -29,6 +32,7 @@ class StaffState extends Equatable { allStaff: allStaff ?? this.allStaff, storesByStaff: storesByStaff ?? this.storesByStaff, staffByStore: staffByStore ?? this.staffByStore, + storeStaff: storeStaff ?? this.storeStaff, error: error, ); } @@ -39,6 +43,7 @@ class StaffState extends Equatable { allStaff, storesByStaff, staffByStore, + storeStaff, error, ]; } diff --git a/lib/features/operations/ui/widgets/staff_section.dart b/lib/features/operations/ui/widgets/staff_section.dart index 7c41e07..00967b1 100644 --- a/lib/features/operations/ui/widgets/staff_section.dart +++ b/lib/features/operations/ui/widgets/staff_section.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flux/core/blocs/session/session_cubit.dart'; +import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:get_it/get_it.dart'; // IMPORTA IL TUO CUBIT DELLO STAFF // import 'package:flux/features/staff/blocs/staff_cubit.dart'; @@ -13,7 +16,9 @@ class StaffSection extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final selectedStaffId = currentOp?.staffId; + final selectedStaffId = + currentOp?.staffId ?? + GetIt.I.get().state.currentStaffMember?.id; return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -27,37 +32,27 @@ class StaffSection extends StatelessWidget { ), ), ), - - // Ascoltiamo il Cubit che contiene la lista dei membri dello staff del negozio - // BlocBuilder( - // builder: (context, state) { - // final staffMembers = state.storeStaff; - - // Sostituisci questo blocco 'simulato' con i dati veri del tuo BlocBuilder - Builder( - builder: (context) { + BlocBuilder( + builder: (context, state) { // Dati finti per farti vedere la UI, piallali quando attacchi il BlocBuilder! - final staffMembers = [ - {'id': '1', 'name': 'Tu (Admin)'}, - {'id': '2', 'name': 'Marco'}, - {'id': '3', 'name': 'Giulia'}, - ]; + final staffMembers = state.storeStaff; + final currentLoggedStaffMember = GetIt.I + .get() + .state + .currentStaffMember; return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: staffMembers.map((staff) { - final staffId = staff['id'] as String; - final staffName = staff['name'] as String; - - final isSelected = staffId == selectedStaffId; + final isSelected = staff.id == selectedStaffId; return GestureDetector( onTap: () { // Aggiorniamo la form con un solo tap! context.read().updateOperationFields( - staffId: staffId, - staffDisplayName: staffName, + staffId: staff.id, + staffDisplayName: staff.name, ); }, child: AnimatedContainer( @@ -99,7 +94,7 @@ class StaffSection extends StatelessWidget { ? Colors.white : theme.colorScheme.primaryContainer, child: Text( - staffName.substring(0, 1).toUpperCase(), + staff.name.substring(0, 1).toUpperCase(), style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, @@ -111,7 +106,9 @@ class StaffSection extends StatelessWidget { ), const SizedBox(width: 8), Text( - staffName, + staff == currentLoggedStaffMember + ? 'Tu (${staff.name})' + : staff.name, style: TextStyle( fontWeight: isSelected ? FontWeight.bold -- 2.43.0 From 68b075f0b140aa4eec3a313c26c076115f89a695 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Mon, 4 May 2026 12:50:00 +0200 Subject: [PATCH 17/18] pr? Co-authored-by: Copilot --- .../data/attachments_repository.dart | 23 + .../attachments/models/attachment_model.dart | 6 +- .../ui/attachment_viewer_screen.dart | 220 +++++ .../attachments/ui/quick_rename_dialog.dart | 85 ++ .../customers/data/customer_repository.dart | 2 +- .../blocs/operation_files_bloc.dart | 63 ++ .../blocs/operation_files_events.dart | 23 + .../operations/blocs/operations_cubit.dart | 5 + .../data/operations_repository.dart | 26 +- .../operations/ui/operation_form_screen.dart | 71 +- .../ui/widgets/details_section.dart | 12 +- .../ui/widgets/operation_files_section.dart | 762 ++++++++++++++++++ lib/main.dart | 4 + pubspec.lock | 56 ++ pubspec.yaml | 2 + 15 files changed, 1345 insertions(+), 15 deletions(-) create mode 100644 lib/features/attachments/data/attachments_repository.dart create mode 100644 lib/features/attachments/ui/attachment_viewer_screen.dart create mode 100644 lib/features/attachments/ui/quick_rename_dialog.dart create mode 100644 lib/features/operations/ui/widgets/operation_files_section.dart diff --git a/lib/features/attachments/data/attachments_repository.dart b/lib/features/attachments/data/attachments_repository.dart new file mode 100644 index 0000000..a7760b3 --- /dev/null +++ b/lib/features/attachments/data/attachments_repository.dart @@ -0,0 +1,23 @@ +import 'dart:typed_data'; + +import 'package:supabase_flutter/supabase_flutter.dart'; + +class AttachmentsRepository { + final _supabase = Supabase.instance.client; + + /// Scarica i byte di un file direttamente da Supabase Storage + Future downloadAttachmentBytes(String storagePath) async { + try { + // ATTENZIONE: Sostituisci 'attachments' con il nome VERO del tuo bucket su Supabase! + // Se il tuo storagePath contiene già il nome del bucket all'inizio, + // assicurati di passargli solo il percorso interno. + final Uint8List bytes = await _supabase.storage + .from('attachments') // <--- NOME DEL TUO BUCKET + .download(storagePath); + + return bytes; + } catch (e) { + throw Exception("Impossibile scaricare il documento dal cloud: $e"); + } + } +} diff --git a/lib/features/attachments/models/attachment_model.dart b/lib/features/attachments/models/attachment_model.dart index 35ad3b2..8e61b3e 100644 --- a/lib/features/attachments/models/attachment_model.dart +++ b/lib/features/attachments/models/attachment_model.dart @@ -9,7 +9,7 @@ class AttachmentModel extends Equatable { final String? operationId; final String name; final String extension; - final String storagePath; + final String? storagePath; final int fileSize; final Uint8List? localBytes; final String companyId; @@ -21,7 +21,7 @@ class AttachmentModel extends Equatable { this.operationId, required this.name, required this.extension, - required this.storagePath, + this.storagePath, required this.fileSize, this.localBytes, required this.companyId, @@ -88,7 +88,7 @@ class AttachmentModel extends Equatable { operationId: map['operation_id'] as String?, name: map['name'] as String, extension: map['extension'] as String, - storagePath: map['storage_path'] as String, + storagePath: map['storage_path'] as String?, fileSize: map['file_size'] is int ? map['file_size'] : int.tryParse(map['file_size']?.toString() ?? '0') ?? 0, diff --git a/lib/features/attachments/ui/attachment_viewer_screen.dart b/lib/features/attachments/ui/attachment_viewer_screen.dart new file mode 100644 index 0000000..b1a9347 --- /dev/null +++ b/lib/features/attachments/ui/attachment_viewer_screen.dart @@ -0,0 +1,220 @@ +import 'dart:typed_data'; +import 'package:flutter/material.dart'; +import 'package:flux/core/utils/functions.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; +import 'package:pdfx/pdfx.dart'; +import 'package:internet_file/internet_file.dart'; + +class AttachmentViewerScreen extends StatefulWidget { + final AttachmentModel attachment; + final Function(String newName)? onRename; + final VoidCallback? onDelete; + + const AttachmentViewerScreen({ + super.key, + required this.attachment, + this.onRename, + this.onDelete, + }); + + @override + State createState() => _AttachmentViewerScreenState(); +} + +class _AttachmentViewerScreenState extends State { + PdfControllerPinch? _pdfController; + bool _isLoading = true; + String? _errorMessage; + Uint8List? _fileBytes; + late String _fileName; + + bool get isPdf => widget.attachment.extension.toLowerCase() == 'pdf'; + + @override + void initState() { + super.initState(); + _fileName = widget.attachment.name; + _loadFile(); + } + + Future _loadFile() async { + try { + // 1. Capiamo da dove prendere i dati + if (widget.attachment.localBytes != null) { + _fileBytes = widget.attachment.localBytes; + } else if (widget.attachment.storagePath != null && + widget.attachment.storagePath!.isNotEmpty) { + final signedUrl = await getSignedUrl(widget.attachment.storagePath!); + _fileBytes = await InternetFile.get(signedUrl); + } else { + throw Exception("Nessun documento trovato o byte mancanti."); + } + + // 2. Se è PDF, inizializziamo il controller + if (isPdf && _fileBytes != null) { + _pdfController = PdfControllerPinch( + document: PdfDocument.openData(_fileBytes!), + ); + } + + if (mounted) setState(() => _isLoading = false); + } catch (e) { + if (mounted) { + setState(() { + _isLoading = false; + _errorMessage = e.toString(); + }); + } + } + } + + @override + void dispose() { + _pdfController?.dispose(); + super.dispose(); + } + + void _showRenameDialog() { + final ctrl = TextEditingController(text: _fileName); + ctrl.selection = TextSelection( + baseOffset: 0, + extentOffset: ctrl.text.length, + ); + final focusNode = FocusNode(); + + showDialog( + context: context, + builder: (context) { + WidgetsBinding.instance.addPostFrameCallback( + (_) => focusNode.requestFocus(), + ); + return AlertDialog( + title: const Text('Rinomina File'), + content: TextField( + controller: ctrl, + focusNode: focusNode, + decoration: InputDecoration( + labelText: 'Nuovo nome', + suffixText: '.${widget.attachment.extension}', + ), + onSubmitted: (val) { + Navigator.pop(context); + if (val.trim().isNotEmpty && widget.onRename != null) { + setState(() { + _fileName = val.trim(); + }); + widget.onRename!(val.trim()); + } + }, + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context), + child: const Text('Annulla'), + ), + ElevatedButton( + onPressed: () { + Navigator.pop(context); + if (ctrl.text.trim().isNotEmpty && widget.onRename != null) { + setState(() { + _fileName = ctrl.text.trim(); + }); + widget.onRename!(ctrl.text.trim()); + } + }, + child: const Text('Salva'), + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.black87, // Sfondo scuro per i viewer è il top + appBar: AppBar( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + title: Text(_fileName, style: const TextStyle(fontSize: 16)), + actions: [ + if (widget.onRename != null) + IconButton( + icon: const Icon(Icons.edit), + tooltip: 'Rinomina', + onPressed: _showRenameDialog, + ), + if (widget.onDelete != null) + IconButton( + icon: const Icon(Icons.delete, color: Colors.redAccent), + tooltip: 'Elimina', + onPressed: () { + // Chiediamo conferma + showDialog( + context: context, + builder: (c) => AlertDialog( + title: const Text('Eliminare file?'), + content: const Text( + 'Sei sicuro di voler eliminare questo allegato?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(c), + child: const Text('Annulla'), + ), + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + onPressed: () { + Navigator.pop(c); // Chiude dialog + widget.onDelete!(); // Lancia eliminazione + Navigator.pop(context); // Chiude il viewer + }, + child: const Text('Elimina'), + ), + ], + ), + ); + }, + ), + ], + ), + body: _buildBody(), + ); + } + + Widget _buildBody() { + if (_isLoading) { + return const Center( + child: CircularProgressIndicator(color: Colors.white), + ); + } + if (_errorMessage != null) { + return Center( + child: Text( + 'Errore: $_errorMessage', + style: const TextStyle(color: Colors.redAccent), + ), + ); + } + if (_fileBytes == null) { + return const Center( + child: Text( + 'File non disponibile', + style: TextStyle(color: Colors.white), + ), + ); + } + + if (isPdf && _pdfController != null) { + return PdfViewPinch(controller: _pdfController!); + } else { + return InteractiveViewer( + maxScale: 5.0, + child: Center(child: Image.memory(_fileBytes!)), + ); + } + } +} diff --git a/lib/features/attachments/ui/quick_rename_dialog.dart b/lib/features/attachments/ui/quick_rename_dialog.dart new file mode 100644 index 0000000..22e1da3 --- /dev/null +++ b/lib/features/attachments/ui/quick_rename_dialog.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; + +class QuickRenameDialog extends StatefulWidget { + final String suggestedName; + final Widget previewWidget; // Può essere Image.memory o un'icona PDF + + const QuickRenameDialog({ + super.key, + required this.suggestedName, + required this.previewWidget, + }); + + @override + State createState() => _QuickRenameDialogState(); +} + +class _QuickRenameDialogState extends State { + late TextEditingController _nameCtrl; + final FocusNode _focusNode = FocusNode(); + + @override + void initState() { + super.initState(); + _nameCtrl = TextEditingController(text: widget.suggestedName); + + // MAGIA UX: Selezioniamo tutto il testo di default appena si apre! + _nameCtrl.selection = TextSelection( + baseOffset: 0, + extentOffset: widget.suggestedName.length, + ); + + // Richiediamo il focus appena il widget è costruito + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode.requestFocus(); + }); + } + + @override + void dispose() { + _nameCtrl.dispose(); + _focusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: const Text('Rinomina per Export'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Anteprima del documento (limitiamo l'altezza) + Container( + height: 200, + width: double.infinity, + decoration: BoxDecoration(border: Border.all(color: Colors.grey)), + child: widget.previewWidget, + ), + const SizedBox(height: 16), + TextField( + controller: _nameCtrl, + focusNode: _focusNode, + decoration: const InputDecoration( + labelText: 'Nome del file', + suffixText: '.pdf', // Facciamo capire che sarà un PDF + border: OutlineInputBorder(), + ), + // MAGIA UX 2: Se preme invio sulla tastiera, salva e chiude! + onSubmitted: (value) => Navigator.of(context).pop(value), + ), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), // Ritorna null + child: const Text('Salta'), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(_nameCtrl.text), + child: const Text('Esporta (Invio)'), + ), + ], + ); + } +} diff --git a/lib/features/customers/data/customer_repository.dart b/lib/features/customers/data/customer_repository.dart index f7ffc8f..43c31ff 100644 --- a/lib/features/customers/data/customer_repository.dart +++ b/lib/features/customers/data/customer_repository.dart @@ -168,7 +168,7 @@ class CustomerRepository { for (var file in files) { if (file.operationId == null) { idsToDelete.add(file.id!); - storagePathsToDelete.add(file.storagePath); + storagePathsToDelete.add(file.storagePath!); } else { idsToEdit.add(file.id!); } diff --git a/lib/features/operations/blocs/operation_files_bloc.dart b/lib/features/operations/blocs/operation_files_bloc.dart index 34aca84..7a48387 100644 --- a/lib/features/operations/blocs/operation_files_bloc.dart +++ b/lib/features/operations/blocs/operation_files_bloc.dart @@ -31,6 +31,10 @@ class OperationFilesBloc on(_onDeleteOperationFiles); on(_onToggleOperationFileSelection); on(_onLinkFilesToCustomer); + on(_onRenameOperationFile); + on(_onDeleteSpecificOperationFiles); + on(_onSelectAllOperationFiles); + on(_onClearOperationFileSelection); // Se il BLoC nasce con un ID, accendiamo subito lo stream! if (operationId != null) { @@ -266,6 +270,22 @@ class OperationFilesBloc emit(state.copyWith(selectedFiles: selectedFiles)); } + void _onSelectAllOperationFiles( + SelectAllOperationFilesEvent event, + Emitter emit, + ) { + // Prendiamo TUTTI i file (locali e remoti) e li buttiamo nei selezionati + emit(state.copyWith(selectedFiles: state.allFiles)); + } + + void _onClearOperationFileSelection( + ClearOperationFileSelectionEvent event, + Emitter emit, + ) { + // Svuotiamo brutalmente la lista + emit(state.copyWith(selectedFiles: [])); + } + FutureOr _onLinkFilesToCustomer( LinkFilesToCustomerEvent event, Emitter emit, @@ -323,4 +343,47 @@ class OperationFilesBloc ); } } + + FutureOr _onRenameOperationFile( + RenameOperationFileEvent event, + Emitter emit, + ) async { + // BIVIO 1: File Locale (Bozza) + if (event.file.localBytes != null) { + final updatedLocalFiles = state.localFiles.map((f) { + if (f == event.file) { + return f.copyWith(name: event.newName); + } + return f; + }).toList(); + emit(state.copyWith(localFiles: updatedLocalFiles)); + return; + } + + // BIVIO 2: File Remoto (Salvato su DB) + emit(state.copyWith(status: OperationFilesStatus.loading)); + try { + await _repository.renameAttachment(event.file.id!, event.newName); + emit(state.copyWith(status: OperationFilesStatus.success)); + } catch (e) { + emit( + state.copyWith( + status: OperationFilesStatus.failure, + error: "Errore rinomina: $e", + ), + ); + } + } + + FutureOr _onDeleteSpecificOperationFiles( + DeleteSpecificOperationFileEvent event, + Emitter emit, + ) { + if (event.file.localBytes != null) { + final updatedLocalFiles = state.localFiles + .where((f) => f != event.file) + .toList(); + emit(state.copyWith(localFiles: updatedLocalFiles)); + } + } } diff --git a/lib/features/operations/blocs/operation_files_events.dart b/lib/features/operations/blocs/operation_files_events.dart index 44a3b5e..f80dfce 100644 --- a/lib/features/operations/blocs/operation_files_events.dart +++ b/lib/features/operations/blocs/operation_files_events.dart @@ -56,3 +56,26 @@ class ToggleOperationFileSelectionEvent extends OperationFilesEvent { final AttachmentModel file; const ToggleOperationFileSelectionEvent(this.file); } + +class RenameOperationFileEvent extends OperationFilesEvent { + final AttachmentModel file; + final String newName; + + const RenameOperationFileEvent(this.file, this.newName); + + @override + List get props => [file, newName]; +} + +class DeleteSpecificOperationFileEvent extends OperationFilesEvent { + final AttachmentModel file; + + const DeleteSpecificOperationFileEvent(this.file); + + @override + List get props => [file]; +} + +class SelectAllOperationFilesEvent extends OperationFilesEvent {} + +class ClearOperationFileSelectionEvent extends OperationFilesEvent {} diff --git a/lib/features/operations/blocs/operations_cubit.dart b/lib/features/operations/blocs/operations_cubit.dart index b148b8a..da3df30 100644 --- a/lib/features/operations/blocs/operations_cubit.dart +++ b/lib/features/operations/blocs/operations_cubit.dart @@ -217,6 +217,7 @@ class OperationsCubit extends Cubit { String? providerId, String? providerDisplayName, String? subtype, + String? description, DateTime? expirationDate, int? quantity, String? modelId, @@ -227,6 +228,7 @@ class OperationsCubit extends Cubit { bool clearProvider = false, bool clearType = false, bool clearSubtype = false, + bool clearDescription = false, bool clearExpiration = false, bool clearQuantity = false, bool clearModel = false, @@ -258,6 +260,9 @@ class OperationsCubit extends Cubit { : (providerDisplayName ?? current.providerDisplayName), quantity: newQuantity, type: clearType ? null : (type ?? current.type), + description: clearDescription + ? null + : (description ?? current.description), subtype: clearSubtype ? null : (subtype ?? current.subtype), expirationDate: clearExpiration ? null diff --git a/lib/features/operations/data/operations_repository.dart b/lib/features/operations/data/operations_repository.dart index 2d38fb8..f0cd46a 100644 --- a/lib/features/operations/data/operations_repository.dart +++ b/lib/features/operations/data/operations_repository.dart @@ -247,6 +247,30 @@ class OperationsRepository { .eq('id', file.id!); } + Future renameAttachment(String id, String newName) async { + try { + await _supabase.from('attachment').update({'name': newName}).eq('id', id); + } catch (e) { + throw '$e'; + } + } + + Future deleteSpecificOperationFile(AttachmentModel file) async { + try { + if (file.customerId == null) { + await _supabase.from('attachment').delete().eq('id', file.id!); + await _supabase.storage.from('documents').remove([file.storagePath!]); + } else { + await _supabase + .from('attachment') + .update({'operation_id': null}) + .eq('id', file.id!); + } + } catch (e) { + throw '$e'; + } + } + Future deleteOperationFiles(List files) async { if (files.isEmpty) return; // 1. Prepariamo le liste di ID e di Percorsi @@ -256,7 +280,7 @@ class OperationsRepository { for (var file in files) { if (file.customerId == null) { idsToDelete.add(file.id!); - storagePathsToDelete.add(file.storagePath); + storagePathsToDelete.add(file.storagePath!); } else { idsToEdit.add(file.id!); } diff --git a/lib/features/operations/ui/operation_form_screen.dart b/lib/features/operations/ui/operation_form_screen.dart index ed6f233..40f7193 100644 --- a/lib/features/operations/ui/operation_form_screen.dart +++ b/lib/features/operations/ui/operation_form_screen.dart @@ -5,6 +5,7 @@ import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/ui/widgets/customer_section.dart'; import 'package:flux/features/operations/ui/widgets/details_section.dart'; +import 'package:flux/features/operations/ui/widgets/operation_files_section.dart'; import 'package:flux/features/operations/ui/widgets/staff_section.dart'; import 'package:get_it/get_it.dart'; // ASSICURATI DEL PATH // import 'package:flux/features/attachments/ui/operation_files_section.dart'; @@ -29,6 +30,7 @@ class _OperationFormScreenState extends State { final _referenceController = TextEditingController(); final _noteController = TextEditingController(); final _freeTextSubtypeController = TextEditingController(); + final _freeTextDescriptionController = TextEditingController(); final List _availableTypes = [ 'AL', @@ -89,6 +91,11 @@ class _OperationFormScreenState extends State { model.subtype!.isNotEmpty) { _freeTextSubtypeController.text = model.subtype!; } + if (_freeTextDescriptionController.text.isEmpty && + model.description != null && + model.description!.isNotEmpty) { + _freeTextDescriptionController.text = model.description!; + } _isInitialized = true; } @@ -103,6 +110,9 @@ class _OperationFormScreenState extends State { subtype: ['Entertainment', 'Custom'].contains(currentOperation.type) ? _freeTextSubtypeController.text : currentOperation.subtype, + description: ['Energy', 'Custom'].contains(currentOperation.type) + ? _freeTextDescriptionController.text + : currentOperation.description, ); cubit.initOperationForm(existingOperation: operationToSave); @@ -138,6 +148,7 @@ class _OperationFormScreenState extends State { ), ); _freeTextSubtypeController.clear(); + _freeTextDescriptionController.clear(); } else if (state.status == OperationsStatus.failure) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -168,8 +179,51 @@ class _OperationFormScreenState extends State { key: _formKey, child: LayoutBuilder( builder: (context, constraints) { + final isUltraWide = constraints.maxWidth > 1400; final isDesktop = constraints.maxWidth > 900; - if (isDesktop) { + if (isUltraWide) { + // --- LAYOUT 3 COLONNE (Schermi giganti) --- + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. FORM PRINCIPALE (40%) + Expanded( + flex: 4, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + // Attenzione: devi togliere la sezione file dal _buildMainFormContent! + child: _buildMainFormContent( + theme, + state, + showFiles: false, + ), + ), + ), + VerticalDivider(width: 1, color: theme.dividerColor), + + // 2. NOTE (30%) + Expanded( + flex: 3, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: _buildNotesSection(isDesktop: true), + ), + ), + VerticalDivider(width: 1, color: theme.dividerColor), + + // 3. FILE (30%) + Expanded( + flex: 3, + child: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: OperationFilesSection( + currentOp: state.currentOperation!, + ), + ), + ), + ], + ); + } else if (isDesktop) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -252,7 +306,11 @@ class _OperationFormScreenState extends State { ); } - Widget _buildMainFormContent(ThemeData theme, OperationsState state) { + Widget _buildMainFormContent( + ThemeData theme, + OperationsState state, { + bool showFiles = true, + }) { final currentOp = state.currentOperation; final currentType = currentOp?.type ?? 'AL'; @@ -296,6 +354,7 @@ class _OperationFormScreenState extends State { currentOp: currentOp, currentType: currentType, freeTextSubtypeController: _freeTextSubtypeController, + freeTextDescriptionController: _freeTextDescriptionController, durationQuickPicks: _buildDurationQuickPicks(currentOp), ), @@ -331,13 +390,7 @@ class _OperationFormScreenState extends State { ), const Divider(height: 32), - _buildSectionTitle('Documenti & Foto'), - const Center( - child: Text( - "Widget File in arrivo...", - style: TextStyle(color: Colors.grey), - ), - ), + if (showFiles) ...[OperationFilesSection(currentOp: currentOp!)], ], ); } diff --git a/lib/features/operations/ui/widgets/details_section.dart b/lib/features/operations/ui/widgets/details_section.dart index 343db76..9361d37 100644 --- a/lib/features/operations/ui/widgets/details_section.dart +++ b/lib/features/operations/ui/widgets/details_section.dart @@ -10,6 +10,7 @@ class DetailsSection extends StatelessWidget { final OperationModel? currentOp; final String currentType; final TextEditingController freeTextSubtypeController; + final TextEditingController freeTextDescriptionController; final Widget durationQuickPicks; const DetailsSection({ @@ -17,6 +18,7 @@ class DetailsSection extends StatelessWidget { required this.currentOp, required this.currentType, required this.freeTextSubtypeController, + required this.freeTextDescriptionController, required this.durationQuickPicks, }); @@ -309,7 +311,6 @@ class DetailsSection extends StatelessWidget { items: [ 'Luce', 'Gas', - 'Dual', ].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(), onChanged: (val) { if (val != null) { @@ -320,6 +321,15 @@ class DetailsSection extends StatelessWidget { }, ), const SizedBox(height: 16), + TextFormField( + controller: freeTextDescriptionController, + decoration: InputDecoration( + labelText: currentType == 'Energy' + ? 'Offerta scelta' + : 'Nome del servizio/offerta', + ), + ), + const SizedBox(height: 16), ], // 2. SCENARIO FIN (Ricerca Modello/Prodotto) diff --git a/lib/features/operations/ui/widgets/operation_files_section.dart b/lib/features/operations/ui/widgets/operation_files_section.dart new file mode 100644 index 0000000..670725d --- /dev/null +++ b/lib/features/operations/ui/widgets/operation_files_section.dart @@ -0,0 +1,762 @@ +import 'dart:io'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flux/features/attachments/data/attachments_repository.dart'; +import 'package:flux/features/attachments/ui/attachment_viewer_screen.dart'; +import 'package:flux/features/attachments/ui/quick_rename_dialog.dart'; +import 'package:flux/features/operations/blocs/operation_files_bloc.dart'; +import 'package:get_it/get_it.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:flux/features/operations/models/operation_model.dart'; +import 'package:flux/features/attachments/models/attachment_model.dart'; +import 'package:pdf/widgets.dart' as pw; +import 'package:pdf/pdf.dart' as p; // Se ti serve formattazione core +import 'package:pdfx/pdfx.dart' as px; // Isoliamo pdfx + +class _ExportItem { + final Uint8List bytes; + final String sourceName; + final bool isMultiPage; + final int pageIndex; + + _ExportItem({ + required this.bytes, + required this.sourceName, + required this.isMultiPage, + required this.pageIndex, + }); +} + +class OperationFilesSection extends StatefulWidget { + final OperationModel currentOp; + + const OperationFilesSection({super.key, required this.currentOp}); + + @override + State createState() => _OperationFilesSectionState(); +} + +class _OperationFilesSectionState extends State { + String? _exportDirectory; + double _maxMbLimit = 1.0; + + @override + void initState() { + super.initState(); + _loadExportDirectory(); + } + + // --- GESTIONE CARTELLA CITRIX (SOLO DESKTOP) --- + Future _loadExportDirectory() async { + if (kIsWeb) return; + final prefs = await SharedPreferences.getInstance(); + setState(() { + _exportDirectory = prefs.getString('citrix_export_path'); + }); + } + + Future _selectExportDirectory() async { + final String? selectedDirectory = await FilePicker.getDirectoryPath( + dialogTitle: 'Seleziona la cartella di esportazione per TIM/Citrix', + ); + + if (selectedDirectory != null) { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('citrix_export_path', selectedDirectory); + setState(() { + _exportDirectory = selectedDirectory; + }); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Cartella Export impostata: $selectedDirectory'), + ), + ); + } + } + } + + // --- SELEZIONE FILE DAL PC/TELEFONO --- + Future _pickFiles() async { + final result = await FilePicker.pickFiles( + allowMultiple: true, + type: FileType.custom, + allowedExtensions: ['jpg', 'jpeg', 'png', 'pdf'], + withData: true, + ); + + if (result != null && mounted) { + // MAGIA: Passiamo direttamente la lista di PlatformFile al tuo BLoC! + context.read().add( + AddOperationFilesEvent(result.files), + ); + } + } + + // --- APERTURA VIEWER --- + void _openFile(AttachmentModel file) { + // 1. Catturiamo il BLoC dalla pagina corrente prima di navigare + final operationFilesBloc = context.read(); + Navigator.push( + context, + MaterialPageRoute( + builder: (viewerContext) => BlocProvider.value( + value: operationFilesBloc, + child: AttachmentViewerScreen( + attachment: file, + onRename: (newName) { + // Spara l'evento al BLoC e lui farà il resto! + operationFilesBloc.add(RenameOperationFileEvent(file, newName)); + }, + onDelete: () { + operationFilesBloc.add(DeleteSpecificOperationFileEvent(file)); + }, + ), + ), + ), + ); + } + + Future _exportMergedPdf(List selectedFiles) async { + if (!kIsWeb && (_exportDirectory == null || _exportDirectory!.isEmpty)) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Imposta prima la cartella Citrix!')), + ); + return; + } + + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const Center(child: CircularProgressIndicator()), + ); + + try { + // 1. "FLATTEN" DI TUTTO (Stessa magia di prima) + List allPagesAsImages = []; + final repository = GetIt.I.get(); + + for (var file in selectedFiles) { + Uint8List? fileBytes; + + if (file.localBytes != null) { + fileBytes = file.localBytes; + } else if (file.storagePath != null && file.storagePath!.isNotEmpty) { + fileBytes = await repository.downloadAttachmentBytes( + file.storagePath!, + ); + } + + if (fileBytes == null) continue; + + if (file.extension == 'pdf') { + final document = await px.PdfDocument.openData(fileBytes); + for (int i = 1; i <= document.pagesCount; i++) { + final page = await document.getPage(i); + final pageImage = await page.render( + width: page.width * 2, + height: page.height * 2, + format: px.PdfPageImageFormat.jpeg, + ); + if (pageImage != null) { + allPagesAsImages.add(pageImage.bytes); + } + await page.close(); + } + await document.close(); + } else { + // È un'immagine + allPagesAsImages.add(fileBytes); + } + } + + if (mounted) Navigator.pop(context); // Togliamo il loading + + // Se per qualche motivo la lista è vuota, usciamo + if (allPagesAsImages.isEmpty) return; + + // 2. LOGICA DEL NOME SUGGERITO + String suggestedName; + if (selectedFiles.length == 1) { + // Se c'è un solo file (es. ho selezionato 3 foto ma poi ho deselezionato le altre) + suggestedName = selectedFiles.first.name; + } else { + // Se sono più file uniti + suggestedName = '${widget.currentOp.customerDisplayName}_Unito'; + } + + if (!mounted) return; + + // 3. DIALOG DI CONFERMA (Mostriamo la PRIMA pagina come anteprima per fargli capire cos'è) + final finalName = await showDialog( + context: context, + builder: (_) => QuickRenameDialog( + suggestedName: suggestedName, + previewWidget: Image.memory( + allPagesAsImages.first, + fit: BoxFit.contain, + ), + ), + ); + + if (finalName == null || finalName.isEmpty) return; // Ha annullato + + // 4. CREAZIONE DEL PDF UNICO (IL MERGE VERO E PROPRIO) + final pdf = pw.Document(); + + // Cicliamo su tutte le immagini estratte e creiamo una pagina per ognuna + for (var imageBytes in allPagesAsImages) { + final pdfImage = pw.MemoryImage(imageBytes); + + pdf.addPage( + pw.Page( + margin: pw.EdgeInsets.zero, + build: (pw.Context context) { + return pw.Center(child: pw.Image(pdfImage)); + }, + ), + ); + } + + final mergedPdfBytes = await pdf.save(); + + // 5. SALVATAGGIO SUL DISCO + if (kIsWeb) { + // Trigger download web + } else { + final fileToSave = File('$_exportDirectory/$finalName.pdf'); + await fileToSave.writeAsBytes(mergedPdfBytes); + } + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('PDF Multi-pagina creato e salvato con successo!'), + ), + ); + } + } catch (e) { + if (mounted) { + // Se il loading è ancora aperto, lo chiudiamo + if (Navigator.canPop(context)) Navigator.pop(context); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Errore durante l\'unione: $e'))); + } + } + } + + Future _exportSplitPdfs(List selectedFiles) async { + if (!kIsWeb && (_exportDirectory == null || _exportDirectory!.isEmpty)) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Imposta prima la cartella Citrix!')), + ); + return; + } + + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => const Center(child: CircularProgressIndicator()), + ); + + try { + // 1. "FLATTEN" INTELLIGENTE (Ora usiamo la nostra classe _ExportItem) + List<_ExportItem> itemsToExport = []; + final repository = GetIt.I.get(); + + for (var file in selectedFiles) { + Uint8List? fileBytes; + + if (file.localBytes != null) { + fileBytes = file.localBytes; + } else if (file.storagePath != null && file.storagePath!.isNotEmpty) { + fileBytes = await repository.downloadAttachmentBytes( + file.storagePath!, + ); + } + + if (fileBytes == null) continue; + + // Recuperiamo il nome che l'utente ha (magari) già impostato + final baseName = file.name ?? 'Documento'; + + if (file.extension == 'pdf') { + final document = await px.PdfDocument.openData(fileBytes); + final isMulti = + document.pagesCount > 1; // Controlliamo se è multipagina! + + for (int i = 1; i <= document.pagesCount; i++) { + final page = await document.getPage(i); + + final pageImage = await page.render( + width: page.width * 2, + height: page.height * 2, + format: px.PdfPageImageFormat.jpeg, + ); + + if (pageImage != null) { + // Salviamo l'immagine CON il suo contesto storico + itemsToExport.add( + _ExportItem( + bytes: pageImage.bytes, + sourceName: baseName, + isMultiPage: isMulti, + pageIndex: i, + ), + ); + } + await page.close(); + } + await document.close(); + } else { + // SE È UN'IMMAGINE, la salviamo come singola pagina + itemsToExport.add( + _ExportItem( + bytes: fileBytes, + sourceName: baseName, + isMultiPage: false, + pageIndex: 1, + ), + ); + } + } + + if (mounted) Navigator.pop(context); + + // 2. IL CICLO UX + for (var item in itemsToExport) { + if (!mounted) return; + + // LA TUA MAGIA UX SUI NOMI: + // Se è singolo (foto o PDF da 1 pag) -> Usa il nome originale nudo e crudo! + // Se è multipagina -> Usa il nome originale + il numero di pagina + String suggestedName = item.sourceName; + if (item.isMultiPage) { + suggestedName = '${item.sourceName}_Pag_${item.pageIndex}'; + } + + final finalName = await showDialog( + context: context, + builder: (_) => QuickRenameDialog( + suggestedName: suggestedName, + previewWidget: Image.memory(item.bytes, fit: BoxFit.contain), + ), + ); + + if (finalName == null || finalName.isEmpty) continue; + + // CREAZIONE DEL PDF SINGOLO + final pdf = pw.Document(); + final pdfImage = pw.MemoryImage(item.bytes); // Usiamo item.bytes! + + pdf.addPage( + pw.Page( + margin: pw.EdgeInsets.zero, + build: (pw.Context context) { + return pw.Center(child: pw.Image(pdfImage)); + }, + ), + ); + + final singlePdfBytes = await pdf.save(); + + if (kIsWeb) { + // Trigger download web + } else { + final fileToSave = File('$_exportDirectory/$finalName.pdf'); + await fileToSave.writeAsBytes(singlePdfBytes); + } + } + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Esportazione completata con successo!'), + ), + ); + } + } catch (e) { + if (mounted) { + Navigator.pop(context); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text('Errore: $e'))); + } + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + // USIAMO IL TUO BLOC! + return BlocBuilder( + builder: (context, state) { + final allFiles = state.allFiles; + final selectedFiles = state.selectedFiles; + final hasSelection = selectedFiles.isNotEmpty; + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 1. SETTINGS CARTELLA (Solo visibile su Desktop) + if (!kIsWeb) + Card( + color: theme.colorScheme.surfaceContainerHighest.withValues( + alpha: 0.5, + ), + elevation: 0, + margin: const EdgeInsets.only(bottom: 16), + child: ListTile( + leading: Icon( + Icons.folder_special, + color: theme.colorScheme.primary, + ), + title: const Text( + 'Cartella Export (Es. Citrix TIM)', + style: TextStyle(fontWeight: FontWeight.bold), + ), + subtitle: Text( + _exportDirectory ?? + 'Nessuna cartella selezionata. Clicca per impostare.', + style: TextStyle( + color: _exportDirectory == null + ? theme.colorScheme.error + : null, + ), + ), + trailing: const Icon(Icons.settings), + onTap: _selectExportDirectory, + ), + ), + + // 2. ACTION BAR DINAMICA + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + spacing: 8, + runSpacing: 8, + children: [ + // Bottone di Aggiunta + ElevatedButton.icon( + icon: const Icon(Icons.add_photo_alternate), + label: const Text('Aggiungi File'), + onPressed: state.status == OperationFilesStatus.uploading + ? null + : _pickFiles, + ), + const SizedBox(width: 12), + + // NUOVO: SELEZIONA / DESELEZIONA TUTTO + if (allFiles.isNotEmpty) ...[ + TextButton.icon( + icon: Icon( + selectedFiles.length == allFiles.length + ? Icons.deselect + : Icons.select_all, + ), + label: Text( + selectedFiles.length == allFiles.length + ? 'Deseleziona Tutto' + : 'Seleziona Tutto', + ), + onPressed: () { + if (selectedFiles.length == allFiles.length) { + context.read().add( + ClearOperationFileSelectionEvent(), + ); + } else { + context.read().add( + SelectAllOperationFilesEvent(), + ); + } + }, + ), + ], + const SizedBox(width: 12), + + // Loader di upload + if (state.status == OperationFilesStatus.uploading) + const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ), + + const Spacer(), + + // Azioni visibili SOLO se c'è una selezione! + if (hasSelection) ...[ + // Bottone Elimina + IconButton( + icon: const Icon(Icons.delete, color: Colors.red), + tooltip: 'Elimina selezionati', + onPressed: () { + context.read().add( + DeleteOperationFilesEvent(), + ); + }, + ), + // Bottone Associa a Cliente + if (widget.currentOp.customerId != null && + widget.currentOp.customerId!.isNotEmpty) + IconButton( + icon: const Icon(Icons.person_add, color: Colors.blue), + tooltip: 'Copia nei documenti del Cliente', + onPressed: () { + context.read().add( + LinkFilesToCustomerEvent( + customerId: widget.currentOp.customerId!, + ), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('File copiati nella scheda cliente!'), + ), + ); + }, + ), + // IL NUOVO BOTTONE ESPORTA CON MENU A TENDINA + PopupMenuButton( + tooltip: 'Opzioni di esportazione', + position: PopupMenuPosition + .under, // Opzionale: fa aprire il menu sotto al bottone + onSelected: (value) { + if (value == 'merge') { + _exportMergedPdf(selectedFiles); + } else if (value == 'split') { + _exportSplitPdfs(selectedFiles); + } + }, + itemBuilder: (BuildContext context) => + >[ + const PopupMenuItem( + value: 'merge', + child: ListTile( + leading: Icon( + Icons.merge_type, + color: Colors.blue, + ), + title: Text('Unisci in un singolo PDF'), + ), + ), + const PopupMenuItem( + value: 'split', + child: ListTile( + leading: Icon( + Icons.splitscreen, + color: Colors.orange, + ), + title: Text( + 'Dividi: un PDF per ogni pagina/foto', + ), + ), + ), + ], + // IL FIX È QUI SOTTO: AbsorbPointer + onPressed vuoto + child: AbsorbPointer( + child: FilledButton.icon( + style: FilledButton.styleFrom( + backgroundColor: Colors.red, + ), + icon: const Icon(Icons.picture_as_pdf), + label: Text('Esporta (${selectedFiles.length})'), + onPressed: () {}, // Manteniamo vivo il colore! + ), + ), + ), + ], + ], + ), + const SizedBox(height: 16), + + // 3. GRIGLIA DEI FILE + if (allFiles.isEmpty) + Container( + width: double.infinity, + padding: const EdgeInsets.all(32), + decoration: BoxDecoration( + border: Border.all( + color: theme.dividerColor, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(8), + ), + child: const Column( + children: [ + Icon(Icons.upload_file, size: 48, color: Colors.grey), + SizedBox(height: 8), + Text( + 'Nessun file allegato. Usa il pulsante per aggiungere documenti o foto.', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.grey), + ), + ], + ), + ) + else + GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 150, + crossAxisSpacing: 12, + mainAxisSpacing: 12, + childAspectRatio: 0.8, + ), + itemCount: allFiles.length, + itemBuilder: (context, index) { + final file = allFiles[index]; + final isPdf = file.extension == 'pdf'; + final isSelected = selectedFiles.contains(file); + final isLocal = + file.localBytes != + null; // Per capire se è un file in bozza + + return Stack( + children: [ + // CARD DEL FILE + InkWell( + onTap: () => _openFile(file), + onLongPress: () { + // Selezione rapida con long press! + context.read().add( + ToggleOperationFileSelectionEvent(file), + ); + }, + borderRadius: BorderRadius.circular(8), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: isSelected + ? theme.colorScheme.primary + : theme.dividerColor, + width: isSelected ? 3 : 1, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Anteprima + Expanded( + child: Container( + decoration: BoxDecoration( + color: theme + .colorScheme + .surfaceContainerHighest, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(8), + ), + ), + child: isPdf + ? const Icon( + Icons.picture_as_pdf, + size: 48, + color: Colors.red, + ) + : isLocal + ? ClipRRect( + borderRadius: + const BorderRadius.vertical( + top: Radius.circular(8), + ), + child: Image.memory( + file.localBytes!, + fit: BoxFit.cover, + ), + ) + : const Icon( + Icons.image, + size: 48, + color: Colors.blue, + ), // Da remoto metterai il tuo NetworkImage se vuoi + ), + ), + // Nome File + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + file.name, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ), + + // CHECKBOX DI SELEZIONE + Positioned( + top: 4, + right: 4, + child: InkWell( + onTap: () { + context.read().add( + ToggleOperationFileSelectionEvent(file), + ); + }, + child: Container( + decoration: BoxDecoration( + color: isSelected + ? theme.colorScheme.primary + : Colors.white.withValues(alpha: 0.8), + shape: BoxShape.circle, + border: Border.all( + color: theme.colorScheme.primary, + ), + ), + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Icon( + isSelected ? Icons.check : Icons.circle, + size: 16, + color: isSelected + ? Colors.white + : Colors.transparent, + ), + ), + ), + ), + ), + + // BADGE "IN ATTESA" (Se è locale ma la pratica è salvata) + if (isLocal) + Positioned( + top: 4, + left: 4, + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 2, + ), + decoration: BoxDecoration( + color: Colors.orange, + borderRadius: BorderRadius.circular(4), + ), + child: const Text( + 'Bozza', + style: TextStyle( + color: Colors.white, + fontSize: 10, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ); + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 93198f7..8f62822 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:flux/features/attachments/data/attachments_repository.dart'; import 'package:flux/features/auth/bloc/auth_cubit.dart'; import 'package:flux/features/operations/data/operations_repository.dart'; import 'package:flux/l10n/app_localizations.dart'; @@ -88,6 +89,9 @@ Future setupLocator() async { () => OperationsRepository(), ); getIt.registerLazySingleton(() => ProviderRepository()); + getIt.registerLazySingleton( + () => AttachmentsRepository(), + ); // NOTA: CompanyRepository l'ho tolto perché la logica della Company // ora è gestita dal CoreRepository durante l'Onboarding. diff --git a/pubspec.lock b/pubspec.lock index 3573306..399aabd 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.dev" + source: hosted + version: "4.0.9" args: dependency: transitive description: @@ -49,6 +57,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.13.1" + barcode: + dependency: transitive + description: + name: barcode + sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4" + url: "https://pub.dev" + source: hosted + version: "2.2.9" + bidi: + dependency: transitive + description: + name: bidi + sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d" + url: "https://pub.dev" + source: hosted + version: "2.0.13" bloc: dependency: transitive description: @@ -365,6 +389,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.dev" + source: hosted + version: "4.8.0" image_picker: dependency: "direct main" description: @@ -637,6 +669,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: e47a275b267873d5944ad5f5ff0dcc7ac2e36c02b3046a0ffac9b72fd362c44b + url: "https://pub.dev" + source: hosted + version: "3.12.0" pdfx: dependency: "direct main" description: @@ -733,6 +773,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.dev" + source: hosted + version: "6.5.0" postgrest: dependency: transitive description: @@ -962,6 +1010,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + universal_io: + dependency: "direct main" + description: + name: universal_io + sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 + url: "https://pub.dev" + source: hosted + version: "2.3.1" universal_platform: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 9a63d01..017d845 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,6 +29,8 @@ dependencies: shared_preferences: ^2.5.5 supabase_flutter: ^2.12.2 uuid: ^4.5.3 + pdf: ^3.12.0 + universal_io: ^2.3.1 dev_dependencies: flutter_test: -- 2.43.0 From 7876d867226c235095abc8f685a87b68514bbb11 Mon Sep 17 00:00:00 2001 From: Mark M2 Macbook Date: Mon, 4 May 2026 12:50:16 +0200 Subject: [PATCH 18/18] f --- lib/features/operations/ui/widgets/operation_files_section.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/features/operations/ui/widgets/operation_files_section.dart b/lib/features/operations/ui/widgets/operation_files_section.dart index 670725d..42b1a6c 100644 --- a/lib/features/operations/ui/widgets/operation_files_section.dart +++ b/lib/features/operations/ui/widgets/operation_files_section.dart @@ -41,7 +41,6 @@ class OperationFilesSection extends StatefulWidget { class _OperationFilesSectionState extends State { String? _exportDirectory; - double _maxMbLimit = 1.0; @override void initState() { -- 2.43.0