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 {