feat-insert-service #5
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
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/features/auth/ui/auth_screen.dart';
|
import 'package:flux/features/auth/ui/auth_screen.dart';
|
||||||
import 'package:flux/features/company/ui/create_company_screen.dart';
|
import 'package:flux/features/company/ui/create_company_screen.dart';
|
||||||
@@ -7,7 +8,10 @@ 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/blocs/services_cubit.dart';
|
||||||
|
import 'package:flux/features/services/data/services_repository.dart';
|
||||||
import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart';
|
import 'package:flux/features/services/ui/service_form_screen/service_form_screen.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
@@ -79,6 +83,13 @@ class AppRouter {
|
|||||||
path: '/service-form',
|
path: '/service-form',
|
||||||
name: 'service-form',
|
name: 'service-form',
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
// Recuperiamo il serviceId dai parametri della query (es: /service-form?serviceId=123)
|
||||||
|
final serviceId = state.uri.queryParameters['serviceId'];
|
||||||
|
if (serviceId != null) {
|
||||||
|
context.read<ServicesCubit>().initServiceForm(
|
||||||
|
serviceId: serviceId,
|
||||||
|
);
|
||||||
|
}
|
||||||
return ServiceFormScreen();
|
return ServiceFormScreen();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async'; // Serve per il Timer del debounce
|
import 'dart:async'; // Serve per il Timer del debounce
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flux/core/blocs/session/session_bloc.dart';
|
||||||
import 'package:flux/features/customers/data/customer_repository.dart';
|
import 'package:flux/features/customers/data/customer_repository.dart';
|
||||||
import 'package:flux/features/customers/models/customer_model.dart';
|
import 'package:flux/features/customers/models/customer_model.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
import 'package:get_it/get_it.dart';
|
||||||
@@ -9,6 +10,7 @@ part 'customer_state.dart';
|
|||||||
|
|
||||||
class CustomerCubit extends Cubit<CustomerState> {
|
class CustomerCubit extends Cubit<CustomerState> {
|
||||||
final CustomerRepository _repository = GetIt.I<CustomerRepository>();
|
final CustomerRepository _repository = GetIt.I<CustomerRepository>();
|
||||||
|
final SessionBloc _sessionBloc = GetIt.I<SessionBloc>();
|
||||||
|
|
||||||
// Variabile per gestire il debounce della ricerca
|
// Variabile per gestire il debounce della ricerca
|
||||||
Timer? _searchDebounce;
|
Timer? _searchDebounce;
|
||||||
@@ -16,10 +18,12 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
CustomerCubit() : super(const CustomerState());
|
CustomerCubit() : super(const CustomerState());
|
||||||
|
|
||||||
// --- LETTURA ---
|
// --- LETTURA ---
|
||||||
Future<void> loadCustomers(String companyId) async {
|
Future<void> loadCustomers() async {
|
||||||
emit(state.copyWith(status: CustomerStatus.loading));
|
emit(state.copyWith(status: CustomerStatus.loading));
|
||||||
try {
|
try {
|
||||||
final customers = await _repository.getCustomers(companyId);
|
final customers = await _repository.getCustomers(
|
||||||
|
_sessionBloc.state.company!.id,
|
||||||
|
);
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(status: CustomerStatus.success, customers: customers),
|
state.copyWith(status: CustomerStatus.success, customers: customers),
|
||||||
);
|
);
|
||||||
@@ -92,21 +96,24 @@ class CustomerCubit extends Cubit<CustomerState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- RICERCA CON DEBOUNCE ---
|
// --- RICERCA CON DEBOUNCE ---
|
||||||
void searchCustomers(String companyId, String query) {
|
void searchCustomers(String query) {
|
||||||
// 1. Se c'è già una ricerca in attesa (l'utente sta digitando veloce), la annulliamo
|
// 1. Se c'è già una ricerca in attesa (l'utente sta digitando veloce), la annulliamo
|
||||||
if (_searchDebounce?.isActive ?? false) _searchDebounce!.cancel();
|
if (_searchDebounce?.isActive ?? false) _searchDebounce!.cancel();
|
||||||
|
|
||||||
// 2. Facciamo partire un timer di 400 millisecondi
|
// 2. Facciamo partire un timer di 400 millisecondi
|
||||||
_searchDebounce = Timer(const Duration(milliseconds: 400), () async {
|
_searchDebounce = Timer(const Duration(milliseconds: 300), () async {
|
||||||
// Se cancella tutto e la query è vuota, ricarichiamo la lista base
|
// Se cancella tutto e la query è vuota, ricarichiamo la lista base
|
||||||
if (query.trim().isEmpty) {
|
if (query.trim().isEmpty) {
|
||||||
await loadCustomers(companyId);
|
await loadCustomers();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nessun "loading" state qui, per evitare sfarfallii visivi mentre si scrive
|
// Nessun "loading" state qui, per evitare sfarfallii visivi mentre si scrive
|
||||||
try {
|
try {
|
||||||
final results = await _repository.searchCustomers(companyId, query);
|
final results = await _repository.searchCustomers(
|
||||||
|
_sessionBloc.state.company!.id,
|
||||||
|
query,
|
||||||
|
);
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(status: CustomerStatus.success, customers: results),
|
state.copyWith(status: CustomerStatus.success, customers: results),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,9 +16,7 @@ class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Opzionale ma consigliato: carica i clienti recenti appena si apre la modale,
|
context.read<CustomerCubit>().loadCustomers();
|
||||||
// così l'utente non vede una schermata vuota prima di cercare.
|
|
||||||
// context.read<CustomersCubit>().loadCustomers(query: '');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -28,10 +26,7 @@ class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onSearchChanged(String query) {
|
void _onSearchChanged(String query) {
|
||||||
// Comunichiamo al Cubit dei clienti di fare la query su Supabase
|
context.read<CustomerCubit>().searchCustomers(query);
|
||||||
// (Consiglio Pro: nel Cubit, metti un "debounce" di 300ms su questa chiamata
|
|
||||||
// per non bombardare Supabase a ogni singola lettera digitata!)
|
|
||||||
// context.read<CustomersCubit>().searchCustomers(query);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -26,14 +26,14 @@ class _CustomersContentState extends State<CustomersContent> {
|
|||||||
void _loadInitialCustomers() {
|
void _loadInitialCustomers() {
|
||||||
final companyId = context.read<SessionBloc>().state.company?.id;
|
final companyId = context.read<SessionBloc>().state.company?.id;
|
||||||
if (companyId != null) {
|
if (companyId != null) {
|
||||||
context.read<CustomerCubit>().loadCustomers(companyId);
|
context.read<CustomerCubit>().loadCustomers();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSearch(String query) {
|
void _onSearch(String query) {
|
||||||
final companyId = context.read<SessionBloc>().state.company?.id;
|
final companyId = context.read<SessionBloc>().state.company?.id;
|
||||||
if (companyId != null) {
|
if (companyId != null) {
|
||||||
context.read<CustomerCubit>().searchCustomers(companyId, query);
|
context.read<CustomerCubit>().searchCustomers( query);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,26 +8,27 @@ 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/fin_service_model.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';
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
part 'services_state.dart';
|
part 'services_state.dart';
|
||||||
|
|
||||||
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 = GetIt.I<SessionBloc>();
|
final SessionBloc _sessionBloc = GetIt.I<SessionBloc>();
|
||||||
|
|
||||||
ServicesCubit() : super(const ServicesState());
|
ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial));
|
||||||
|
|
||||||
// --- CARICAMENTO E PAGINAZIONE ---
|
// --- CARICAMENTO E PAGINAZIONE ---
|
||||||
|
|
||||||
Future<void> loadServices({bool refresh = false}) async {
|
Future<void> loadServices({bool refresh = false}) async {
|
||||||
// Se stiamo già caricando, evitiamo chiamate doppie
|
// Se stiamo già caricando, evitiamo chiamate doppie
|
||||||
if (state.isLoading) return;
|
if (state.status == ServicesStatus.loading) return;
|
||||||
|
|
||||||
// Se non è un refresh e abbiamo già raggiunto la fine dei dati, ci fermiamo
|
// Se non è un refresh e abbiamo già raggiunto la fine dei dati, ci fermiamo
|
||||||
if (!refresh && state.hasReachedMax) return;
|
if (!refresh && state.hasReachedMax) return;
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
isLoading: true,
|
status: ServicesStatus.loading,
|
||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
// Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading
|
// Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading
|
||||||
allServices: refresh ? [] : state.allServices,
|
allServices: refresh ? [] : state.allServices,
|
||||||
@@ -56,7 +57,7 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
|
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
isLoading: false,
|
status: ServicesStatus.ready,
|
||||||
allServices: refresh
|
allServices: refresh
|
||||||
? newServices
|
? newServices
|
||||||
: [...state.allServices, ...newServices],
|
: [...state.allServices, ...newServices],
|
||||||
@@ -66,7 +67,7 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
isLoading: false,
|
status: ServicesStatus.failure,
|
||||||
errorMessage: "Errore nel caricamento servizi: $e",
|
errorMessage: "Errore nel caricamento servizi: $e",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -95,9 +96,28 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
// --- GESTIONE BOZZA (DRAFT) ---
|
// --- GESTIONE BOZZA (DRAFT) ---
|
||||||
|
|
||||||
/// Inizializza un nuovo servizio o ne carica uno esistente per la modifica
|
/// Inizializza un nuovo servizio o ne carica uno esistente per la modifica
|
||||||
void initServiceForm(ServiceModel? existingService) {
|
void initServiceForm({
|
||||||
|
ServiceModel? existingService,
|
||||||
|
String? serviceId,
|
||||||
|
}) async {
|
||||||
if (existingService != null) {
|
if (existingService != null) {
|
||||||
emit(state.copyWith(currentService: existingService));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
currentService: existingService,
|
||||||
|
status: ServicesStatus.ready,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (serviceId != null) {
|
||||||
|
ServiceModel? serviceModel = state.allServices.firstWhereOrNull(
|
||||||
|
(s) => s.id == serviceId,
|
||||||
|
);
|
||||||
|
serviceModel ??= await _repository.fetchServiceById(serviceId);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
currentService: serviceModel,
|
||||||
|
status: ServicesStatus.ready,
|
||||||
|
),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Crea un template vuoto con lo store di default (se disponibile)
|
// Crea un template vuoto con lo store di default (se disponibile)
|
||||||
emit(
|
emit(
|
||||||
@@ -106,7 +126,9 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
storeId: _sessionBloc.state.selectedStore?.id ?? '',
|
storeId: _sessionBloc.state.selectedStore?.id ?? '',
|
||||||
number: '', // Sarà compilato dall'utente
|
number: '', // Sarà compilato dall'utente
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
|
companyId: _sessionBloc.state.company!.id,
|
||||||
),
|
),
|
||||||
|
status: ServicesStatus.ready,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -180,16 +202,22 @@ class ServicesCubit extends Cubit<ServicesState> {
|
|||||||
Future<void> saveCurrentService() async {
|
Future<void> saveCurrentService() async {
|
||||||
if (state.currentService == null) return;
|
if (state.currentService == null) return;
|
||||||
|
|
||||||
emit(state.copyWith(isSaving: true, errorMessage: null));
|
emit(state.copyWith(status: ServicesStatus.saving, errorMessage: null));
|
||||||
try {
|
try {
|
||||||
// Usiamo il repository corazzato che abbiamo scritto prima
|
// Usiamo il repository corazzato che abbiamo scritto prima
|
||||||
await _repository.saveFullService(state.currentService!);
|
await _repository.saveFullService(state.currentService!);
|
||||||
|
|
||||||
// Reset della bozza e ricaricamento lista
|
|
||||||
emit(state.copyWith(isSaving: false, currentService: null));
|
|
||||||
await loadServices(refresh: true);
|
await loadServices(refresh: true);
|
||||||
|
// Reset della bozza e ricaricamento lista
|
||||||
|
emit(state.copyWith(status: ServicesStatus.saved, currentService: null));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(state.copyWith(isSaving: false, errorMessage: e.toString()));
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: ServicesStatus.failure,
|
||||||
|
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
part of 'services_cubit.dart';
|
part of 'services_cubit.dart';
|
||||||
|
|
||||||
|
enum ServicesStatus { initial, loading, ready, saving, saved, success, failure }
|
||||||
|
|
||||||
class ServicesState extends Equatable {
|
class ServicesState extends Equatable {
|
||||||
|
final ServicesStatus status;
|
||||||
final List<ServiceModel> allServices;
|
final List<ServiceModel> allServices;
|
||||||
final ServiceModel? currentService; // La bozza che stiamo editando
|
final ServiceModel? currentService; // La bozza che stiamo editando
|
||||||
final bool isLoading;
|
|
||||||
final bool isSaving; // Per mostrare il caricamento solo sul tasto salva
|
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final String query;
|
final String query;
|
||||||
final DateTimeRange? dateRange;
|
final DateTimeRange? dateRange;
|
||||||
final bool hasReachedMax;
|
final bool hasReachedMax;
|
||||||
|
|
||||||
const ServicesState({
|
const ServicesState({
|
||||||
|
required this.status,
|
||||||
this.allServices = const [],
|
this.allServices = const [],
|
||||||
this.currentService,
|
this.currentService,
|
||||||
this.isLoading = false,
|
|
||||||
this.isSaving = false,
|
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
this.query = '',
|
this.query = '',
|
||||||
this.dateRange,
|
this.dateRange,
|
||||||
@@ -22,20 +22,18 @@ class ServicesState extends Equatable {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ServicesState copyWith({
|
ServicesState copyWith({
|
||||||
|
ServicesStatus? status,
|
||||||
List<ServiceModel>? allServices,
|
List<ServiceModel>? allServices,
|
||||||
ServiceModel? currentService,
|
ServiceModel? currentService,
|
||||||
bool? isLoading,
|
|
||||||
bool? isSaving,
|
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
String? query,
|
String? query,
|
||||||
DateTimeRange? dateRange,
|
DateTimeRange? dateRange,
|
||||||
bool? hasReachedMax,
|
bool? hasReachedMax,
|
||||||
}) {
|
}) {
|
||||||
return ServicesState(
|
return ServicesState(
|
||||||
|
status: status ?? this.status,
|
||||||
allServices: allServices ?? this.allServices,
|
allServices: allServices ?? this.allServices,
|
||||||
currentService: currentService ?? this.currentService,
|
currentService: currentService ?? this.currentService,
|
||||||
isLoading: isLoading ?? this.isLoading,
|
|
||||||
isSaving: isSaving ?? this.isSaving,
|
|
||||||
errorMessage: errorMessage,
|
errorMessage: errorMessage,
|
||||||
query: query ?? this.query,
|
query: query ?? this.query,
|
||||||
dateRange: dateRange ?? this.dateRange,
|
dateRange: dateRange ?? this.dateRange,
|
||||||
@@ -45,10 +43,9 @@ class ServicesState extends Equatable {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
|
status,
|
||||||
allServices,
|
allServices,
|
||||||
currentService,
|
currentService,
|
||||||
isLoading,
|
|
||||||
isSaving,
|
|
||||||
errorMessage,
|
errorMessage,
|
||||||
query,
|
query,
|
||||||
dateRange,
|
dateRange,
|
||||||
|
|||||||
@@ -5,6 +5,27 @@ import '../models/service_model.dart';
|
|||||||
class ServicesRepository {
|
class ServicesRepository {
|
||||||
final _supabase = Supabase.instance.client;
|
final _supabase = Supabase.instance.client;
|
||||||
|
|
||||||
|
// --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO ---
|
||||||
|
Future<ServiceModel> fetchServiceById(String id) async {
|
||||||
|
try {
|
||||||
|
final response = await _supabase
|
||||||
|
.from('service')
|
||||||
|
.select('''
|
||||||
|
*,
|
||||||
|
customer(nome),
|
||||||
|
energy_service(*),
|
||||||
|
fin_service(*),
|
||||||
|
entertainment_service(*)
|
||||||
|
''')
|
||||||
|
.eq('id', id)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return ServiceModel.fromMap(response);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Errore nel caricamento del servizio: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- RECUPERO PAGINATO CON FILTRI E JOIN ---
|
// --- RECUPERO PAGINATO CON FILTRI E JOIN ---
|
||||||
Future<List<ServiceModel>> fetchServices({
|
Future<List<ServiceModel>> fetchServices({
|
||||||
required String companyId,
|
required String companyId,
|
||||||
@@ -19,7 +40,7 @@ class ServicesRepository {
|
|||||||
.from('service')
|
.from('service')
|
||||||
.select('''
|
.select('''
|
||||||
*,
|
*,
|
||||||
customer(name, surname),
|
customer(nome),
|
||||||
energy_service(*),
|
energy_service(*),
|
||||||
fin_service(*),
|
fin_service(*),
|
||||||
entertainment_service(*)
|
entertainment_service(*)
|
||||||
@@ -36,7 +57,7 @@ class ServicesRepository {
|
|||||||
if (searchTerm != null && searchTerm.isNotEmpty) {
|
if (searchTerm != null && searchTerm.isNotEmpty) {
|
||||||
// Filtra sui campi della tabella principale O su quelli della tabella joinata
|
// Filtra sui campi della tabella principale O su quelli della tabella joinata
|
||||||
query = query.or(
|
query = query.or(
|
||||||
'number.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.name.ilike.%$searchTerm%,customer.surname.ilike.%$searchTerm%',
|
'number.ilike.%$searchTerm%,note.ilike.%$searchTerm%,customer.nome.ilike.%$searchTerm%',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,4 +155,36 @@ class ServicesRepository {
|
|||||||
throw Exception('Errore durante l\'eliminazione: $e');
|
throw Exception('Errore durante l\'eliminazione: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- RECUPERO TIPI CONTENUTI PIÙ FREQUENTI PER AUTOCOMPLETE ---
|
||||||
|
Future<List<String>> 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
|
||||||
|
final response = await _supabase
|
||||||
|
.from('entertainment_service')
|
||||||
|
.select('type, service!inner(store!inner(company_id))')
|
||||||
|
.eq('service.store.company_id', companyId)
|
||||||
|
.limit(100); // Prendiamo un campione
|
||||||
|
|
||||||
|
// Logica rapida per contare le occorrenze e prendere i primi 5
|
||||||
|
final Map<String, int> counts = {};
|
||||||
|
for (var item in (response as List)) {
|
||||||
|
final type = item['type'] as String;
|
||||||
|
counts[type] = (counts[type] ?? 0) + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortedKeys = counts.keys.toList()
|
||||||
|
..sort((a, b) => counts[b]!.compareTo(counts[a]!));
|
||||||
|
|
||||||
|
return sortedKeys.take(5).toList();
|
||||||
|
} catch (e) {
|
||||||
|
return [
|
||||||
|
"Netflix",
|
||||||
|
"DAZN",
|
||||||
|
"Disney+",
|
||||||
|
"Sky",
|
||||||
|
]; // Fallback se non c'è ancora storia
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flux/core/utils/string_extensions.dart';
|
||||||
import 'package:flux/features/services/models/energy_service_model.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/entertainment_service_model.dart';
|
||||||
import 'package:flux/features/services/models/fin_service_model.dart';
|
import 'package:flux/features/services/models/fin_service_model.dart';
|
||||||
@@ -14,6 +15,7 @@ class ServiceModel extends Equatable {
|
|||||||
final String note;
|
final String note;
|
||||||
final bool resultOk;
|
final bool resultOk;
|
||||||
final String? customerDisplayName;
|
final String? customerDisplayName;
|
||||||
|
final String companyId;
|
||||||
|
|
||||||
// Telefonia
|
// Telefonia
|
||||||
final int al;
|
final int al;
|
||||||
@@ -46,6 +48,7 @@ class ServiceModel extends Equatable {
|
|||||||
this.finServices = const [],
|
this.finServices = const [],
|
||||||
this.entertainmentServices = const [],
|
this.entertainmentServices = const [],
|
||||||
this.customerDisplayName,
|
this.customerDisplayName,
|
||||||
|
required this.companyId,
|
||||||
});
|
});
|
||||||
|
|
||||||
ServiceModel copyWith({
|
ServiceModel copyWith({
|
||||||
@@ -67,6 +70,7 @@ class ServiceModel extends Equatable {
|
|||||||
List<FinServiceModel>? finServices,
|
List<FinServiceModel>? finServices,
|
||||||
List<EntertainmentServiceModel>? entertainmentServices,
|
List<EntertainmentServiceModel>? entertainmentServices,
|
||||||
String? customerDisplayName,
|
String? customerDisplayName,
|
||||||
|
String? companyId,
|
||||||
}) {
|
}) {
|
||||||
return ServiceModel(
|
return ServiceModel(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -88,6 +92,7 @@ class ServiceModel extends Equatable {
|
|||||||
entertainmentServices:
|
entertainmentServices:
|
||||||
entertainmentServices ?? this.entertainmentServices,
|
entertainmentServices ?? this.entertainmentServices,
|
||||||
customerDisplayName: customerDisplayName ?? this.customerDisplayName,
|
customerDisplayName: customerDisplayName ?? this.customerDisplayName,
|
||||||
|
companyId: companyId ?? this.companyId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,6 +116,7 @@ class ServiceModel extends Equatable {
|
|||||||
finServices,
|
finServices,
|
||||||
entertainmentServices,
|
entertainmentServices,
|
||||||
customerDisplayName,
|
customerDisplayName,
|
||||||
|
companyId,
|
||||||
];
|
];
|
||||||
|
|
||||||
factory ServiceModel.fromMap(Map<String, dynamic> map) {
|
factory ServiceModel.fromMap(Map<String, dynamic> map) {
|
||||||
@@ -151,9 +157,9 @@ class ServiceModel extends Equatable {
|
|||||||
|
|
||||||
// Display name del cliente con fallback
|
// Display name del cliente con fallback
|
||||||
customerDisplayName: map['customer'] != null
|
customerDisplayName: map['customer'] != null
|
||||||
? "${map['customer']['name'] ?? ''} ${map['customer']['surname'] ?? ''}"
|
? "${map['customer']['nome'] ?? ''}".myFormat()
|
||||||
.trim()
|
|
||||||
: "Cliente non assegnato",
|
: "Cliente non assegnato",
|
||||||
|
companyId: map['company_id'] as String,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,6 +178,7 @@ class ServiceModel extends Equatable {
|
|||||||
'nip': nip,
|
'nip': nip,
|
||||||
'unica': unica,
|
'unica': unica,
|
||||||
'telepass': telepass,
|
'telepass': telepass,
|
||||||
|
'company_id': companyId,
|
||||||
// Le liste non le mettiamo qui perché vanno in tabelle diverse!
|
// Le liste non le mettiamo qui perché vanno in tabelle diverse!
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,392 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/core/blocs/session/session_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/data/services_repository.dart';
|
||||||
|
import 'package:flux/features/services/models/entertainment_service_model.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
|
class EntertainmentServiceDialog extends StatefulWidget {
|
||||||
|
final List<EntertainmentServiceModel> initialServices;
|
||||||
|
final String currentStoreId;
|
||||||
|
|
||||||
|
const EntertainmentServiceDialog({
|
||||||
|
super.key,
|
||||||
|
required this.initialServices,
|
||||||
|
required this.currentStoreId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EntertainmentServiceDialog> createState() =>
|
||||||
|
_EntertainmentServiceDialogState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EntertainmentServiceDialogState
|
||||||
|
extends State<EntertainmentServiceDialog> {
|
||||||
|
late List<EntertainmentServiceModel> _tempList;
|
||||||
|
bool _isAddingNew = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_tempList = List.from(widget.initialServices);
|
||||||
|
// Carichiamo i provider attivi per lo store corrente
|
||||||
|
context.read<ProvidersCubit>().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: (newService) => setState(() {
|
||||||
|
_tempList.add(newService);
|
||||||
|
_isAddingNew = false;
|
||||||
|
}),
|
||||||
|
onCancel: () => setState(() => _isAddingNew = false),
|
||||||
|
)
|
||||||
|
: BlocBuilder<ProvidersCubit, ProvidersState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
// Passiamo allProviders per garantire la visione dello storico
|
||||||
|
return _EntertainmentList(
|
||||||
|
services: _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<EntertainmentServiceModel> services;
|
||||||
|
final List<ProviderModel> allProviders;
|
||||||
|
final Function(int) onDelete;
|
||||||
|
final VoidCallback onAddTap;
|
||||||
|
|
||||||
|
const _EntertainmentList({
|
||||||
|
required this.services,
|
||||||
|
required this.allProviders,
|
||||||
|
required this.onDelete,
|
||||||
|
required this.onAddTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
if (services.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: services.length,
|
||||||
|
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final s = services[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,
|
||||||
|
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(EntertainmentServiceModel) 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<void> _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<ProvidersCubit, ProvidersState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final filtered = state.activeProviders
|
||||||
|
.where((p) => p.intrattenimento)
|
||||||
|
.toList();
|
||||||
|
return DropdownButtonFormField<String>(
|
||||||
|
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<List<String>>(
|
||||||
|
future: GetIt.I<ServicesRepository>().fetchTopEntertainmentTypes(
|
||||||
|
GetIt.I<SessionBloc>().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<int?>(
|
||||||
|
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(
|
||||||
|
EntertainmentServiceModel(
|
||||||
|
providerId: _selectedProviderId!,
|
||||||
|
type: _typeController.text,
|
||||||
|
constrained: _isConstrained,
|
||||||
|
constrainExpiration: _expirationDate,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text("Aggiungi"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,45 +10,67 @@ class ServiceFormScreen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return BlocListener<ServicesCubit, ServicesState>(
|
||||||
appBar: AppBar(
|
listener: (context, state) {
|
||||||
title: const Text("Nuova Pratica"),
|
if (state.status == ServicesStatus.saved) {
|
||||||
actions: [
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
_SaveButton(), // Tasto salva intelligente
|
const SnackBar(
|
||||||
],
|
content: Text("Pratica salvata con successo!"),
|
||||||
),
|
backgroundColor: Colors.green,
|
||||||
body: BlocBuilder<ServicesCubit, ServicesState>(
|
|
||||||
builder: (context, state) {
|
|
||||||
final service = state.currentService;
|
|
||||||
|
|
||||||
// Se la bozza non è ancora inizializzata, mostriamo un loader
|
|
||||||
if (service == null) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// SEZIONE 1: CLIENTE
|
|
||||||
CustomerSection(service: service),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// SEZIONE 2: INFO GENERALI (Da fare)
|
|
||||||
GeneralInfoSection(service: service),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// SEZIONE 3: I MODULI (Da fare)
|
|
||||||
ServicesGrid(service: service),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
|
|
||||||
// SEZIONE 4: ALLEGATI (Da fare)
|
|
||||||
// const _AttachmentsSection(),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
Navigator.pop(context); // Torna alla lista di pratiche
|
||||||
|
} else if (state.status == ServicesStatus.failure) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(
|
||||||
|
"Si è verificato un errore ${state.errorMessage ?? ''}",
|
||||||
|
),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Nuova Pratica"),
|
||||||
|
actions: [
|
||||||
|
_SaveButton(), // Tasto salva intelligente
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: BlocBuilder<ServicesCubit, ServicesState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
final service = state.currentService;
|
||||||
|
|
||||||
|
// Se la bozza non è ancora inizializzata, mostriamo un loader
|
||||||
|
if (service == null) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// SEZIONE 1: CLIENTE
|
||||||
|
CustomerSection(service: service),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// SEZIONE 2: INFO GENERALI
|
||||||
|
GeneralInfoSection(service: service),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// SEZIONE 3: I MODULI
|
||||||
|
ServicesGrid(service: service),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// TODO SEZIONE 4: ALLEGATI (Da fare)
|
||||||
|
// const _AttachmentsSection(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -61,7 +83,7 @@ class _SaveButton extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ServicesCubit, ServicesState>(
|
return BlocBuilder<ServicesCubit, ServicesState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.isSaving) {
|
if (state.status == ServicesStatus.saving) {
|
||||||
return const Padding(
|
return const Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
@@ -78,7 +100,6 @@ class _SaveButton extends StatelessWidget {
|
|||||||
icon: const Icon(Icons.save),
|
icon: const Icon(Icons.save),
|
||||||
tooltip: "Salva Pratica",
|
tooltip: "Salva Pratica",
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
// TODO: Aggiungere una validazione prima di salvare!
|
|
||||||
context.read<ServicesCubit>().saveCurrentService();
|
context.read<ServicesCubit>().saveCurrentService();
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ 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/blocs/product_cubit.dart';
|
||||||
import 'package:flux/features/services/blocs/services_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/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/fin_service_model.dart';
|
||||||
import 'package:flux/features/services/models/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/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/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/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/services/ui/service_form_screen/int_dialogs.dart'; // Assicurati di importare il modello
|
||||||
|
|
||||||
@@ -162,12 +164,25 @@ class ServicesGrid extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
ActionCard(
|
ActionCard(
|
||||||
label: "Contenuti",
|
label: "Intratten.",
|
||||||
count: service.entertainmentServices.length,
|
count: service.entertainmentServices.length,
|
||||||
icon: Icons.tv,
|
icon: Icons.movie_filter_outlined,
|
||||||
color: Colors.redAccent,
|
color: Colors.purple,
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
// TODO: Aprire la Dialog Contenuti complessa
|
final result =
|
||||||
|
await showDialog<List<EntertainmentServiceModel>>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => EntertainmentServiceDialog(
|
||||||
|
initialServices: service.entertainmentServices,
|
||||||
|
currentStoreId: service.storeId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result != null && context.mounted) {
|
||||||
|
context
|
||||||
|
.read<ServicesCubit>()
|
||||||
|
.updateEntertainmentServices(result);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
// Agganciamo il listener per la paginazione (Scroll Infinito)
|
// Agganciamo il listener per la paginazione (Scroll Infinito)
|
||||||
_scrollController.addListener(_onScroll);
|
_scrollController.addListener(_onScroll);
|
||||||
|
// Carichiamo i servizi iniziali
|
||||||
|
context.read<ServicesCubit>().loadServices();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScroll() {
|
void _onScroll() {
|
||||||
@@ -61,7 +63,8 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
|||||||
body: BlocBuilder<ServicesCubit, ServicesState>(
|
body: BlocBuilder<ServicesCubit, ServicesState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// 1. Stato di caricamento iniziale
|
// 1. Stato di caricamento iniziale
|
||||||
if (state.isLoading && state.allServices.isEmpty) {
|
if (state.status == ServicesStatus.loading &&
|
||||||
|
state.allServices.isEmpty) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +175,10 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
trailing: const Icon(Icons.chevron_right),
|
trailing: const Icon(Icons.chevron_right),
|
||||||
onTap: () => context.pushNamed('service-form', extra: service),
|
onTap: () => context.pushNamed(
|
||||||
|
'service-form',
|
||||||
|
queryParameters: {'serviceId': service.id},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,11 +54,12 @@ void startNewService(BuildContext context) {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
// 1. Inizializza il form nel Cubit
|
// 1. Inizializza il form nel Cubit
|
||||||
context.read<ServicesCubit>().initServiceForm(
|
context.read<ServicesCubit>().initServiceForm(
|
||||||
ServiceModel(
|
existingService: ServiceModel(
|
||||||
storeId: currentStoreId,
|
storeId: currentStoreId,
|
||||||
employeeId: member.id,
|
employeeId: member.id,
|
||||||
number: '',
|
number: '',
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
|
companyId: session.company!.id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -95,17 +95,52 @@ class _FluxAppState extends State<FluxApp> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<ThemeBloc, ThemeState>(
|
return BlocBuilder<SessionBloc, SessionState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return MaterialApp.router(
|
if (state.status == SessionStatus.unknown) {
|
||||||
title: 'FLUX Gestionale',
|
return _buildLoadingScreen();
|
||||||
debugShowCheckedModeBanner: false,
|
}
|
||||||
theme: fluxLightTheme,
|
return BlocBuilder<ThemeBloc, ThemeState>(
|
||||||
darkTheme: fluxDarkTheme,
|
builder: (context, state) {
|
||||||
themeMode: state.currentTheme.themeMode,
|
return MaterialApp.router(
|
||||||
routerConfig: _router, // Usa l'istanza mantenuta nello stato
|
title: 'FLUX Gestionale',
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
theme: fluxLightTheme,
|
||||||
|
darkTheme: fluxDarkTheme,
|
||||||
|
themeMode: state.currentTheme.themeMode,
|
||||||
|
routerConfig: _router, // Usa l'istanza mantenuta nello stato
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Una semplice schermata di caricamento coerente con il brand
|
||||||
|
Widget _buildLoadingScreen() {
|
||||||
|
return MaterialApp(
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
home: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Qui puoi mettere il tuo logo
|
||||||
|
const Icon(Icons.bolt, size: 64, color: Colors.blue),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const CircularProgressIndicator(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
"Inizializzazione sessione...",
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: collection
|
name: collection
|
||||||
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ environment:
|
|||||||
sdk: ^3.11.3
|
sdk: ^3.11.3
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
collection: ^1.19.1
|
||||||
equatable: ^2.0.8
|
equatable: ^2.0.8
|
||||||
file_picker: ^11.0.2
|
file_picker: ^11.0.2
|
||||||
flutter:
|
flutter:
|
||||||
|
|||||||
Reference in New Issue
Block a user