service #2
@@ -7,6 +7,8 @@ import 'package:flux/features/customers/ui/customer_detail_screen.dart';
|
|||||||
import 'package:flux/features/home/ui/home_screen.dart';
|
import 'package:flux/features/home/ui/home_screen.dart';
|
||||||
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
||||||
import 'package:flux/features/master_data/store/ui/create_store_screen.dart';
|
import 'package:flux/features/master_data/store/ui/create_store_screen.dart';
|
||||||
|
import 'package:flux/features/services/models/service_model.dart';
|
||||||
|
import 'package:flux/features/services/ui/service_form_screen.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
@@ -74,6 +76,15 @@ class AppRouter {
|
|||||||
name: 'products',
|
name: 'products',
|
||||||
builder: (context, state) => const ProductsScreen(),
|
builder: (context, state) => const ProductsScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/service-form',
|
||||||
|
name: 'service-form',
|
||||||
|
builder: (context, state) {
|
||||||
|
// Recuperiamo il ServiceModel se passato come extra
|
||||||
|
final service = state.extra as ServiceModel?;
|
||||||
|
return ServiceFormScreen(initialService: service);
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flux/core/blocs/session/session_bloc.dart';
|
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||||
import 'package:flux/core/theme/theme.dart';
|
import 'package:flux/core/theme/theme.dart';
|
||||||
import 'package:flux/features/master_data/master_data_hub_content.dart';
|
import 'package:flux/features/master_data/master_data_hub_content.dart';
|
||||||
|
import 'package:flux/features/services/blocs/services_cubit.dart';
|
||||||
|
import 'package:flux/features/services/ui/services_screen.dart';
|
||||||
import 'dashboard_content.dart'; // Importiamo il contenuto della dashboard
|
import 'dashboard_content.dart'; // Importiamo il contenuto della dashboard
|
||||||
|
|
||||||
class HomeScreen extends StatefulWidget {
|
class HomeScreen extends StatefulWidget {
|
||||||
@@ -16,6 +18,16 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
int _selectedIndex = 0;
|
int _selectedIndex = 0;
|
||||||
bool _extendRailway = false;
|
bool _extendRailway = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Caricamento "silenzioso" all'avvio dell'app
|
||||||
|
// Usiamo WidgetsBinding per assicurarci che il contesto sia pronto
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
context.read<ServicesCubit>().loadServices();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<SessionBloc, SessionState>(
|
return BlocBuilder<SessionBloc, SessionState>(
|
||||||
@@ -63,7 +75,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.receipt_long),
|
icon: Icon(Icons.receipt_long),
|
||||||
label: 'Operazioni',
|
label: 'Servizi',
|
||||||
),
|
),
|
||||||
BottomNavigationBarItem(
|
BottomNavigationBarItem(
|
||||||
icon: Icon(Icons.folder_shared),
|
icon: Icon(Icons.folder_shared),
|
||||||
@@ -111,7 +123,7 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
icon: Icon(Icons.receipt_long_outlined),
|
icon: Icon(Icons.receipt_long_outlined),
|
||||||
selectedIcon: Icon(Icons.receipt_long),
|
selectedIcon: Icon(Icons.receipt_long),
|
||||||
label: Text('Operazioni'),
|
label: Text('Servizi'),
|
||||||
),
|
),
|
||||||
NavigationRailDestination(
|
NavigationRailDestination(
|
||||||
icon: Icon(Icons.folder_shared_outlined),
|
icon: Icon(Icons.folder_shared_outlined),
|
||||||
@@ -148,17 +160,18 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
|
|
||||||
// Switch tra le sottopagine
|
// Switch tra le sottopagine
|
||||||
Widget _buildPageContent(int index, bool isLargeScreen) {
|
Widget _buildPageContent(int index, bool isLargeScreen) {
|
||||||
switch (index) {
|
return IndexedStack(
|
||||||
case 0:
|
index: index,
|
||||||
return DashboardContent(
|
children: [
|
||||||
|
DashboardContent(
|
||||||
isLargeScreen: isLargeScreen,
|
isLargeScreen: isLargeScreen,
|
||||||
onTabRequested: (idx) => setState(() => _selectedIndex = 2),
|
onTabRequested: (idx) => setState(() => _selectedIndex = 2),
|
||||||
);
|
),
|
||||||
case 1:
|
|
||||||
return const Center(child: Text('Operazioni'));
|
ServicesScreen(),
|
||||||
case 2:
|
|
||||||
// L'unico punto di ingresso per tutte le anagrafiche
|
// L'unico punto di ingresso per tutte le anagrafiche
|
||||||
return MasterDataHubContent(
|
MasterDataHubContent(
|
||||||
// Qui gestiamo la navigazione "interna" all'hub
|
// Qui gestiamo la navigazione "interna" all'hub
|
||||||
onOpenPage: (widget) {
|
onOpenPage: (widget) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
@@ -166,9 +179,8 @@ class _HomeScreenState extends State<HomeScreen> {
|
|||||||
MaterialPageRoute(builder: (context) => widget),
|
MaterialPageRoute(builder: (context) => widget),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
default:
|
],
|
||||||
return DashboardContent(isLargeScreen: isLargeScreen);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
113
lib/features/master_data/providers/blocs/provider_cubit.dart
Normal file
113
lib/features/master_data/providers/blocs/provider_cubit.dart
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/features/master_data/providers/data/provider_repository.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import '../models/provider_model.dart';
|
||||||
|
|
||||||
|
class ProvidersState extends Equatable {
|
||||||
|
final List<ProviderModel> allProviders; // Tutti i provider della company
|
||||||
|
final List<String>
|
||||||
|
associatedIds; // ID dei provider attivi nello store selezionato
|
||||||
|
final bool isLoading;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
const ProvidersState({
|
||||||
|
this.allProviders = const [],
|
||||||
|
this.associatedIds = const [],
|
||||||
|
this.isLoading = false,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
ProvidersState copyWith({
|
||||||
|
List<ProviderModel>? allProviders,
|
||||||
|
List<String>? associatedIds,
|
||||||
|
bool? isLoading,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return ProvidersState(
|
||||||
|
allProviders: allProviders ?? this.allProviders,
|
||||||
|
associatedIds: associatedIds ?? this.associatedIds,
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
allProviders,
|
||||||
|
associatedIds,
|
||||||
|
isLoading,
|
||||||
|
errorMessage,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProvidersCubit extends Cubit<ProvidersState> {
|
||||||
|
final ProviderRepository _repository = GetIt.I<ProviderRepository>();
|
||||||
|
|
||||||
|
ProvidersCubit() : super(const ProvidersState());
|
||||||
|
|
||||||
|
// Carica i provider della company e quelli associati a uno store specifico
|
||||||
|
Future<void> loadProviders(String companyId, String? storeId) async {
|
||||||
|
emit(state.copyWith(isLoading: true));
|
||||||
|
try {
|
||||||
|
final all = await _repository.fetchAllCompanyProviders(companyId);
|
||||||
|
List<String> associated = [];
|
||||||
|
|
||||||
|
if (storeId != null) {
|
||||||
|
associated = await _repository.fetchAssociatedProviderIds(storeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
allProviders: all,
|
||||||
|
associatedIds: associated,
|
||||||
|
isLoading: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aggiunge o rimuove l'associazione con lo store
|
||||||
|
Future<void> toggleProviderAssociation({
|
||||||
|
required String providerId,
|
||||||
|
required String storeId,
|
||||||
|
required bool isCurrentlyAssociated,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
if (isCurrentlyAssociated) {
|
||||||
|
await _repository.disassociateProviderFromStore(
|
||||||
|
providerId: providerId,
|
||||||
|
storeId: storeId,
|
||||||
|
);
|
||||||
|
// Aggiorniamo lo stato locale rimuovendo l'ID
|
||||||
|
final newIds = List<String>.from(state.associatedIds)
|
||||||
|
..remove(providerId);
|
||||||
|
emit(state.copyWith(associatedIds: newIds));
|
||||||
|
} else {
|
||||||
|
await _repository.associateProviderToStore(
|
||||||
|
providerId: providerId,
|
||||||
|
storeId: storeId,
|
||||||
|
);
|
||||||
|
// Aggiorniamo lo stato locale aggiungendo l'ID
|
||||||
|
final newIds = List<String>.from(state.associatedIds)..add(providerId);
|
||||||
|
emit(state.copyWith(associatedIds: newIds));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(errorMessage: "Errore durante l'aggiornamento: $e"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salvataggio/Update anagrafica (nuovo o modifica)
|
||||||
|
Future<void> saveProvider(ProviderModel provider) async {
|
||||||
|
emit(state.copyWith(isLoading: true));
|
||||||
|
try {
|
||||||
|
await _repository.saveProvider(provider);
|
||||||
|
// Ricarichiamo la lista per vedere le modifiche
|
||||||
|
await loadProviders(provider.companyId, null);
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(isLoading: false, errorMessage: e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import '../models/provider_model.dart';
|
||||||
|
|
||||||
|
class ProviderRepository {
|
||||||
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
// --- ASSOCIAZIONE PROVIDER <-> STORE ---
|
||||||
|
|
||||||
|
// Aggiunge un provider a un negozio (Attiva mandato)
|
||||||
|
Future<void> associateProviderToStore({
|
||||||
|
required String providerId,
|
||||||
|
required String storeId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _supabase.from('providers_in_stores').insert({
|
||||||
|
'provider_id': providerId,
|
||||||
|
'store_id': storeId,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Errore durante l\'associazione provider: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rimuove un provider da un negozio (Disattiva mandato)
|
||||||
|
Future<void> disassociateProviderFromStore({
|
||||||
|
required String providerId,
|
||||||
|
required String storeId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _supabase
|
||||||
|
.from('providers_in_stores')
|
||||||
|
.delete()
|
||||||
|
.eq('provider_id', providerId)
|
||||||
|
.eq('store_id', storeId);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Errore durante la disassociazione provider: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recupera tutti i provider di una company (per la lista generale)
|
||||||
|
Future<List<ProviderModel>> fetchAllCompanyProviders(String companyId) async {
|
||||||
|
try {
|
||||||
|
final response = await _supabase
|
||||||
|
.from('provider')
|
||||||
|
.select()
|
||||||
|
.eq('company_id', companyId);
|
||||||
|
|
||||||
|
return (response as List).map((m) => ProviderModel.fromMap(m)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Errore fetch provider: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recupera gli ID dei provider associati a uno store (utile per le checkbox)
|
||||||
|
Future<List<String>> fetchAssociatedProviderIds(String storeId) async {
|
||||||
|
try {
|
||||||
|
final response = await _supabase
|
||||||
|
.from('providers_in_stores')
|
||||||
|
.select('provider_id')
|
||||||
|
.eq('store_id', storeId);
|
||||||
|
|
||||||
|
return (response as List)
|
||||||
|
.map((item) => item['provider_id'].toString())
|
||||||
|
.toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Errore recupero ID associati: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- FUNZIONI STANDARD ---
|
||||||
|
|
||||||
|
// Questa la userai nel Form Servizi: carica solo i provider abilitati per lo store
|
||||||
|
Future<List<ProviderModel>> fetchActiveProvidersForStore(
|
||||||
|
String storeId,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
final response = await _supabase
|
||||||
|
.from('provider')
|
||||||
|
.select('*, providers_in_stores!inner(store_id)')
|
||||||
|
.eq('providers_in_stores.store_id', storeId)
|
||||||
|
.eq('is_active', true);
|
||||||
|
|
||||||
|
return (response as List).map((m) => ProviderModel.fromMap(m)).toList();
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Errore fetch provider attivi: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salva o aggiorna l'anagrafica del Provider
|
||||||
|
Future<void> saveProvider(ProviderModel provider) async {
|
||||||
|
await _supabase.from('provider').upsert(provider.toMap());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import 'package:equatable/equatable.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 altro;
|
||||||
|
final bool isActive;
|
||||||
|
final String companyId;
|
||||||
|
|
||||||
|
const ProviderModel({
|
||||||
|
required this.id,
|
||||||
|
required this.nome,
|
||||||
|
required this.telefoniaFissa,
|
||||||
|
required this.telefoniaMobile,
|
||||||
|
required this.energia,
|
||||||
|
required this.assicurazioni,
|
||||||
|
required this.intrattenimento,
|
||||||
|
required this.altro,
|
||||||
|
required this.isActive,
|
||||||
|
required this.companyId,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory ProviderModel.fromMap(Map<String, dynamic> map) {
|
||||||
|
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,
|
||||||
|
altro: map['altro'] ?? false,
|
||||||
|
isActive: map['is_active'] ?? true,
|
||||||
|
companyId: map['company_id'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toMap() {
|
||||||
|
return {
|
||||||
|
'nome': nome,
|
||||||
|
'telefonia_fissa': telefoniaFissa,
|
||||||
|
'telefonia_mobile': telefoniaMobile,
|
||||||
|
'energia': energia,
|
||||||
|
'assicurazioni': assicurazioni,
|
||||||
|
'intrattenimento': intrattenimento,
|
||||||
|
'altro': altro,
|
||||||
|
'is_active': isActive,
|
||||||
|
'company_id': companyId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
id,
|
||||||
|
nome,
|
||||||
|
telefoniaFissa,
|
||||||
|
telefoniaMobile,
|
||||||
|
energia,
|
||||||
|
assicurazioni,
|
||||||
|
intrattenimento,
|
||||||
|
altro,
|
||||||
|
isActive,
|
||||||
|
companyId,
|
||||||
|
];
|
||||||
|
|
||||||
|
ProviderModel copyWith({
|
||||||
|
String? id,
|
||||||
|
String? nome,
|
||||||
|
bool? telefoniaFissa,
|
||||||
|
bool? telefoniaMobile,
|
||||||
|
bool? energia,
|
||||||
|
bool? assicurazioni,
|
||||||
|
bool? intrattenimento,
|
||||||
|
bool? altro,
|
||||||
|
bool? isActive,
|
||||||
|
String? companyId,
|
||||||
|
}) {
|
||||||
|
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,
|
||||||
|
altro: altro ?? this.altro,
|
||||||
|
isActive: isActive ?? this.isActive,
|
||||||
|
companyId: companyId ?? this.companyId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||||
import 'package:flux/features/services/data/services_repository.dart';
|
import 'package:flux/features/services/data/services_repository.dart';
|
||||||
import 'package:flux/features/services/models/service_model.dart';
|
import 'package:flux/features/services/models/service_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
@@ -54,11 +55,14 @@ class ServicesState extends Equatable {
|
|||||||
|
|
||||||
class ServicesCubit extends Cubit<ServicesState> {
|
class ServicesCubit extends Cubit<ServicesState> {
|
||||||
final ServicesRepository _repository = GetIt.I<ServicesRepository>();
|
final ServicesRepository _repository = GetIt.I<ServicesRepository>();
|
||||||
|
final SessionBloc _sessionBloc;
|
||||||
|
|
||||||
ServicesCubit() : super(const ServicesState());
|
ServicesCubit(this._sessionBloc) : super(const ServicesState());
|
||||||
|
|
||||||
// Carica tutto il pacchetto
|
// Carica tutto il pacchetto
|
||||||
Future<void> loadServices({bool refresh = false}) async {
|
Future<void> loadServices({bool refresh = false}) async {
|
||||||
|
// Se non è un refresh e abbiamo già dati, non disturbare Supabase
|
||||||
|
if (!refresh && state.allServices.isNotEmpty) return;
|
||||||
if (state.isLoading) return;
|
if (state.isLoading) return;
|
||||||
|
|
||||||
// Se facciamo refresh, resettiamo tutto
|
// Se facciamo refresh, resettiamo tutto
|
||||||
@@ -74,6 +78,7 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final newServices = await _repository.fetchServices(
|
final newServices = await _repository.fetchServices(
|
||||||
|
companyId: _sessionBloc.state.company!.id,
|
||||||
offset: currentOffset,
|
offset: currentOffset,
|
||||||
searchTerm: state.query,
|
searchTerm: state.query,
|
||||||
dateRange: state.dateRange,
|
dateRange: state.dateRange,
|
||||||
|
|||||||
@@ -1,94 +1,106 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import '../models/service_model.dart';
|
import '../models/service_model.dart';
|
||||||
// Importa gli altri modelli se sono in file separati
|
|
||||||
|
|
||||||
class ServicesRepository {
|
class ServicesRepository {
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
// --- RECUPERO TUTTI I SERVIZI ---
|
// --- RECUPERO PAGINATO CON FILTRI E JOIN ---
|
||||||
Future<List<ServiceModel>> fetchServices({
|
Future<List<ServiceModel>> fetchServices({
|
||||||
|
required String companyId,
|
||||||
required int offset,
|
required int offset,
|
||||||
int limit = 50,
|
int limit = 50,
|
||||||
String? searchTerm,
|
String? searchTerm,
|
||||||
DateTimeRange? dateRange,
|
DateTimeRange? dateRange,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
var query = _supabase.from('service').select('''
|
// Nota: 'customer(name, surname)' serve per il display name nella card
|
||||||
*,
|
var query = _supabase
|
||||||
energy_service(*),
|
.from('service')
|
||||||
fin_service(*),
|
.select('''
|
||||||
entertainment_service(*)
|
*,
|
||||||
''');
|
customer(name, surname),
|
||||||
|
energy_service(*),
|
||||||
|
fin_service(*),
|
||||||
|
entertainment_service(*)
|
||||||
|
''')
|
||||||
|
.eq('company_id', companyId);
|
||||||
|
|
||||||
// Filtro per range di date
|
// Filtro Range Date
|
||||||
if (dateRange != null) {
|
if (dateRange != null) {
|
||||||
query = query
|
query = query
|
||||||
.gte('created_at', dateRange.start.toIso8601String())
|
.gte('created_at', dateRange.start.toIso8601String())
|
||||||
.lte('created_at', dateRange.end.toIso8601String());
|
.lte('created_at', dateRange.end.toIso8601String());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ordinamento e Paginazione
|
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.name.ilike.%$searchTerm%,customer.surname.ilike.%$searchTerm%',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final response = await query
|
final response = await query
|
||||||
.order('created_at', ascending: false)
|
.order('created_at', ascending: false)
|
||||||
.range(offset, offset + limit - 1);
|
.range(offset, offset + limit - 1);
|
||||||
|
|
||||||
final List<ServiceModel> services = (response as List)
|
return (response as List)
|
||||||
.map((map) => ServiceModel.fromMap(map))
|
.map((map) => ServiceModel.fromMap(map))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
// Filtro testuale lato client per semplicità (o potresti farlo in SQL se preferisci)
|
|
||||||
if (searchTerm != null && searchTerm.isNotEmpty) {
|
|
||||||
return services.where((s) {
|
|
||||||
// Qui cercheremo per numero pratica o note (il nome cliente lo vedremo poi con le Join)
|
|
||||||
return s.number.toLowerCase().contains(searchTerm.toLowerCase()) ||
|
|
||||||
s.note.toLowerCase().contains(searchTerm.toLowerCase());
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
return services;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Errore fetch: $e');
|
throw Exception('Errore nel caricamento servizi: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- SALVATAGGIO COMPLETO (A CASCATA) ---
|
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
|
||||||
Future<void> saveFullService(ServiceModel service) async {
|
Future<void> saveFullService(ServiceModel service) async {
|
||||||
try {
|
try {
|
||||||
// 1. Inserimento Padre
|
// 1. Inseriamo il record principale
|
||||||
|
// Se service.id è null, Supabase fa INSERT. Se c'è, fa UPDATE (grazie all'upsert o gestione manuale)
|
||||||
final serviceData = await _supabase
|
final serviceData = await _supabase
|
||||||
.from('service')
|
.from('service')
|
||||||
.insert(service.toMap())
|
.upsert(service.toMap())
|
||||||
.select()
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
final String newId = serviceData['id'];
|
final String newId = serviceData['id'];
|
||||||
|
|
||||||
// 2. Inserimento Energy (se presenti)
|
// 2. Pulizia vecchi record figli (necessaria se è una MODIFICA)
|
||||||
|
// Se stiamo modificando, cancelliamo i vecchi per reinserire i nuovi (più semplice)
|
||||||
|
if (service.id != null) {
|
||||||
|
await _supabase.from('energy_service').delete().eq('service_id', newId);
|
||||||
|
await _supabase.from('fin_service').delete().eq('service_id', newId);
|
||||||
|
await _supabase
|
||||||
|
.from('entertainment_service')
|
||||||
|
.delete()
|
||||||
|
.eq('service_id', newId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Inserimento EnergyServices
|
||||||
if (service.energyServices.isNotEmpty) {
|
if (service.energyServices.isNotEmpty) {
|
||||||
final List<Map<String, dynamic>> energyToInsert = [];
|
final List<Map<String, dynamic>> toInsert = [];
|
||||||
for (var item in service.energyServices) {
|
for (var item in service.energyServices) {
|
||||||
energyToInsert.add(item.copyWith(serviceId: newId).toMap());
|
toInsert.add(item.copyWith(serviceId: newId).toMap());
|
||||||
}
|
}
|
||||||
await _supabase.from('energy_service').insert(energyToInsert);
|
await _supabase.from('energy_service').insert(toInsert);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Inserimento Finanziamenti (se presenti)
|
// 4. Inserimento FinServices
|
||||||
if (service.finServices.isNotEmpty) {
|
if (service.finServices.isNotEmpty) {
|
||||||
final List<Map<String, dynamic>> finToInsert = [];
|
final List<Map<String, dynamic>> toInsert = [];
|
||||||
for (var item in service.finServices) {
|
for (var item in service.finServices) {
|
||||||
finToInsert.add(item.copyWith(serviceId: newId).toMap());
|
toInsert.add(item.copyWith(serviceId: newId).toMap());
|
||||||
}
|
}
|
||||||
await _supabase.from('fin_service').insert(finToInsert);
|
await _supabase.from('fin_service').insert(toInsert);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Inserimento Entertainment (se presenti)
|
// 5. Inserimento EntertainmentServices
|
||||||
if (service.entertainmentServices.isNotEmpty) {
|
if (service.entertainmentServices.isNotEmpty) {
|
||||||
final List<Map<String, dynamic>> entToInsert = [];
|
final List<Map<String, dynamic>> toInsert = [];
|
||||||
for (var item in service.entertainmentServices) {
|
for (var item in service.entertainmentServices) {
|
||||||
entToInsert.add(item.copyWith(serviceId: newId).toMap());
|
toInsert.add(item.copyWith(serviceId: newId).toMap());
|
||||||
}
|
}
|
||||||
await _supabase.from('entertainment_service').insert(entToInsert);
|
await _supabase.from('entertainment_service').insert(toInsert);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception('Errore durante il salvataggio: $e');
|
throw Exception('Errore durante il salvataggio: $e');
|
||||||
@@ -96,8 +108,6 @@ class ServicesRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- ELIMINAZIONE ---
|
// --- ELIMINAZIONE ---
|
||||||
// Grazie ai "ON DELETE CASCADE" che hai messo nell'SQL,
|
|
||||||
// cancellando il padre Supabase pialla automaticamente i figli. Top!
|
|
||||||
Future<void> deleteService(String id) async {
|
Future<void> deleteService(String id) async {
|
||||||
try {
|
try {
|
||||||
await _supabase.from('service').delete().eq('id', id);
|
await _supabase.from('service').delete().eq('id', id);
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class ServiceModel extends Equatable {
|
|||||||
final bool isBozza;
|
final bool isBozza;
|
||||||
final String note;
|
final String note;
|
||||||
final bool resultOk;
|
final bool resultOk;
|
||||||
|
final String? customerDisplayName;
|
||||||
|
|
||||||
// Telefonia
|
// Telefonia
|
||||||
final int al;
|
final int al;
|
||||||
@@ -44,6 +45,7 @@ class ServiceModel extends Equatable {
|
|||||||
this.energyServices = const [],
|
this.energyServices = const [],
|
||||||
this.finServices = const [],
|
this.finServices = const [],
|
||||||
this.entertainmentServices = const [],
|
this.entertainmentServices = const [],
|
||||||
|
this.customerDisplayName,
|
||||||
});
|
});
|
||||||
|
|
||||||
ServiceModel copyWith({
|
ServiceModel copyWith({
|
||||||
@@ -64,6 +66,7 @@ class ServiceModel extends Equatable {
|
|||||||
List<EnergyServiceModel>? energyServices,
|
List<EnergyServiceModel>? energyServices,
|
||||||
List<FinServiceModel>? finServices,
|
List<FinServiceModel>? finServices,
|
||||||
List<EntertainmentServiceModel>? entertainmentServices,
|
List<EntertainmentServiceModel>? entertainmentServices,
|
||||||
|
String? customerDisplayName,
|
||||||
}) {
|
}) {
|
||||||
return ServiceModel(
|
return ServiceModel(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -84,6 +87,7 @@ class ServiceModel extends Equatable {
|
|||||||
finServices: finServices ?? this.finServices,
|
finServices: finServices ?? this.finServices,
|
||||||
entertainmentServices:
|
entertainmentServices:
|
||||||
entertainmentServices ?? this.entertainmentServices,
|
entertainmentServices ?? this.entertainmentServices,
|
||||||
|
customerDisplayName: customerDisplayName ?? this.customerDisplayName,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,6 +110,7 @@ class ServiceModel extends Equatable {
|
|||||||
energyServices,
|
energyServices,
|
||||||
finServices,
|
finServices,
|
||||||
entertainmentServices,
|
entertainmentServices,
|
||||||
|
customerDisplayName,
|
||||||
];
|
];
|
||||||
|
|
||||||
factory ServiceModel.fromMap(Map<String, dynamic> map) {
|
factory ServiceModel.fromMap(Map<String, dynamic> map) {
|
||||||
@@ -141,6 +146,9 @@ class ServiceModel extends Equatable {
|
|||||||
?.map((x) => EntertainmentServiceModel.fromMap(x))
|
?.map((x) => EntertainmentServiceModel.fromMap(x))
|
||||||
.toList() ??
|
.toList() ??
|
||||||
const [],
|
const [],
|
||||||
|
customerDisplayName: map['customer'] != null
|
||||||
|
? "${map['customer']['name']} ${map['customer']['surname']}"
|
||||||
|
: "Cliente sconosciuto",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
150
lib/features/services/ui/service_form_screen.dart
Normal file
150
lib/features/services/ui/service_form_screen.dart
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
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/energy_service_model.dart';
|
||||||
|
import 'package:flux/features/services/models/service_model.dart';
|
||||||
|
|
||||||
|
class ServiceFormScreen extends StatefulWidget {
|
||||||
|
final ServiceModel? initialService; // Se nullo, è un nuovo inserimento
|
||||||
|
|
||||||
|
const ServiceFormScreen({super.key, this.initialService});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ServiceFormScreen> createState() => _ServiceFormScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ServiceFormScreenState extends State<ServiceFormScreen> {
|
||||||
|
late ServiceModel currentService;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Se passiamo un servizio esistente lo carichiamo, altrimenti ne creiamo uno "vuoto"
|
||||||
|
currentService =
|
||||||
|
widget.initialService ??
|
||||||
|
ServiceModel(
|
||||||
|
storeId: 'ID_NEGOZIO_QUI', // Poi lo prenderai dal profilo utente
|
||||||
|
number: '',
|
||||||
|
energyServices: const [],
|
||||||
|
finServices: const [],
|
||||||
|
entertainmentServices: const [],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metodo generico per aggiungere un servizio energia
|
||||||
|
void _addEnergy() {
|
||||||
|
setState(() {
|
||||||
|
final newList =
|
||||||
|
List<EnergyServiceModel>.from(currentService.energyServices)..add(
|
||||||
|
EnergyServiceModel(
|
||||||
|
type: EnergyType.luce, // Default
|
||||||
|
expiration: DateTime.now().add(const Duration(days: 365)),
|
||||||
|
providerId: '', // Lo sceglierà l'utente
|
||||||
|
),
|
||||||
|
);
|
||||||
|
currentService = currentService.copyWith(energyServices: newList);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(
|
||||||
|
widget.initialService == null ? "Nuova Pratica" : "Modifica",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// --- SEZIONE DATI GENERALI ---
|
||||||
|
TextField(
|
||||||
|
decoration: const InputDecoration(labelText: "Numero Pratica"),
|
||||||
|
onChanged: (v) =>
|
||||||
|
currentService = currentService.copyWith(number: v),
|
||||||
|
),
|
||||||
|
|
||||||
|
const Divider(height: 32),
|
||||||
|
|
||||||
|
// --- SEZIONE ENERGY ---
|
||||||
|
_SectionHeader(
|
||||||
|
title: "Energia (Luce/Gas)",
|
||||||
|
onAdd: _addEnergy,
|
||||||
|
icon: Icons.electric_bolt,
|
||||||
|
),
|
||||||
|
...currentService.energyServices.asMap().entries.map((entry) {
|
||||||
|
int idx = entry.key;
|
||||||
|
var item = entry.value;
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
"${item.type.name.toUpperCase()} - Scadenza: ${item.expiration.year}",
|
||||||
|
),
|
||||||
|
trailing: IconButton(
|
||||||
|
icon: const Icon(Icons.delete, color: Colors.red),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
final newList = List<EnergyServiceModel>.from(
|
||||||
|
currentService.energyServices,
|
||||||
|
)..removeAt(idx);
|
||||||
|
currentService = currentService.copyWith(
|
||||||
|
energyServices: newList,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
// --- BOTTONE SALVA ---
|
||||||
|
ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
minimumSize: const Size.fromHeight(50),
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
context.read<ServicesCubit>().addService(currentService);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: const Text("SALVA TUTTO"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SectionHeader extends StatelessWidget {
|
||||||
|
final String title;
|
||||||
|
final VoidCallback onAdd;
|
||||||
|
final IconData icon;
|
||||||
|
|
||||||
|
const _SectionHeader({
|
||||||
|
required this.title,
|
||||||
|
required this.onAdd,
|
||||||
|
required this.icon,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, color: Colors.orange),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
onPressed: onAdd,
|
||||||
|
icon: const Icon(Icons.add_circle, color: Colors.green, size: 30),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
197
lib/features/services/ui/services_screen.dart
Normal file
197
lib/features/services/ui/services_screen.dart
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
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:go_router/go_router.dart';
|
||||||
|
// Importa i tuoi modelli e cubit
|
||||||
|
|
||||||
|
class ServicesScreen extends StatefulWidget {
|
||||||
|
const ServicesScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ServicesScreen> createState() => _ServicesScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ServicesScreenState extends State<ServicesScreen> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Agganciamo il listener per la paginazione (Scroll Infinito)
|
||||||
|
_scrollController.addListener(_onScroll);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onScroll() {
|
||||||
|
if (_isBottom) {
|
||||||
|
context.read<ServicesCubit>().loadServices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool get _isBottom {
|
||||||
|
if (!_scrollController.hasClients) return false;
|
||||||
|
final maxScroll = _scrollController.position.maxScrollExtent;
|
||||||
|
final currentScroll = _scrollController.offset;
|
||||||
|
// Carica quando mancano 200px alla fine
|
||||||
|
return currentScroll >= (maxScroll * 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Gestione Servizi"),
|
||||||
|
elevation: 0,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
onPressed: () {
|
||||||
|
// Qui potrai implementare una barra di ricerca
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: BlocBuilder<ServicesCubit, ServicesState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
// 1. Stato di caricamento iniziale
|
||||||
|
if (state.isLoading && state.allServices.isEmpty) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Lista vuota
|
||||||
|
if (state.allServices.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text("Nessuna pratica trovata."),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => context.read<ServicesCubit>().loadServices(
|
||||||
|
refresh: true,
|
||||||
|
),
|
||||||
|
child: const Text("Riprova"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. La Lista (con Pull-to-refresh)
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () =>
|
||||||
|
context.read<ServicesCubit>().loadServices(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,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index >= state.allServices.length) {
|
||||||
|
return const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(16.0),
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final service = state.allServices[index];
|
||||||
|
return _buildServiceCard(context, service);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: () => context.pushNamed('service-form'), // GoRouter
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildServiceCard(BuildContext context, ServiceModel service) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: ListTile(
|
||||||
|
contentPadding: const EdgeInsets.all(12),
|
||||||
|
title: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
service.customerDisplayName ?? "Cliente sconosciuto",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 16,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (service.isBozza)
|
||||||
|
const Chip(
|
||||||
|
label: Text(
|
||||||
|
"BOZZA",
|
||||||
|
style: TextStyle(fontSize: 10, color: Colors.white),
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.orange,
|
||||||
|
visualDensity: VisualDensity.compact,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
subtitle: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"Pratica: ${service.number} • ${service.createdAt?.day}/${service.createdAt?.month}/${service.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)
|
||||||
|
_miniBadge("📞 Tel", Colors.blue),
|
||||||
|
if (service.energyServices.isNotEmpty)
|
||||||
|
_miniBadge("⚡ Energy", Colors.green),
|
||||||
|
if (service.finServices.isNotEmpty)
|
||||||
|
_miniBadge("💰 Fin", Colors.purple),
|
||||||
|
if (service.entertainmentServices.isNotEmpty)
|
||||||
|
_miniBadge("📺 Ent", Colors.red),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
onTap: () => context.pushNamed('service-form', extra: service),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:developer';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
@@ -16,6 +18,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/staff/data/staff_repository.dart';
|
||||||
import 'package:flux/features/master_data/store/bloc/store_cubit.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/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/settings/settings.dart';
|
import 'package:flux/features/settings/settings.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -59,6 +63,7 @@ Future<void> setupLocator() async {
|
|||||||
getIt.registerLazySingleton<CustomerRepository>(() => CustomerRepository());
|
getIt.registerLazySingleton<CustomerRepository>(() => CustomerRepository());
|
||||||
getIt.registerLazySingleton<ProductRepository>(() => ProductRepository());
|
getIt.registerLazySingleton<ProductRepository>(() => ProductRepository());
|
||||||
getIt.registerLazySingleton<StaffRepository>(() => StaffRepository());
|
getIt.registerLazySingleton<StaffRepository>(() => StaffRepository());
|
||||||
|
getIt.registerLazySingleton<ServicesRepository>(() => ServicesRepository());
|
||||||
}
|
}
|
||||||
|
|
||||||
class FluxApp extends StatefulWidget {
|
class FluxApp extends StatefulWidget {
|
||||||
@@ -95,6 +100,9 @@ class _FluxAppState extends State<FluxApp> {
|
|||||||
create: (_) =>
|
create: (_) =>
|
||||||
StaffCubit(context.read<SessionBloc>())..loadAllStaff(),
|
StaffCubit(context.read<SessionBloc>())..loadAllStaff(),
|
||||||
),
|
),
|
||||||
|
BlocProvider<ServicesCubit>(
|
||||||
|
create: (_) => ServicesCubit(context.read<SessionBloc>()),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: BlocBuilder<ThemeBloc, ThemeState>(
|
child: BlocBuilder<ThemeBloc, ThemeState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
|||||||
Reference in New Issue
Block a user