asd
This commit is contained in:
@@ -12,7 +12,7 @@ import 'package:flux/features/customers/models/customer_model.dart';
|
||||
import 'package:flux/features/customers/ui/customer_detail_screen.dart';
|
||||
import 'package:flux/features/customers/ui/customer_mobile_upload_screen.dart';
|
||||
import 'package:flux/features/customers/ui/customers_content.dart';
|
||||
import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.dart';
|
||||
import 'package:flux/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart';
|
||||
import 'package:flux/features/home/ui/home_screen.dart';
|
||||
import 'package:flux/features/master_data/master_data_hub_content.dart';
|
||||
import 'package:flux/features/master_data/products/ui/products_screen.dart';
|
||||
@@ -21,11 +21,11 @@ import 'package:flux/features/master_data/staff/ui/staff_screen.dart';
|
||||
import 'package:flux/features/master_data/store/ui/stores_screen.dart';
|
||||
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
|
||||
import 'package:flux/features/onboarding/ui/onboarding_screen.dart';
|
||||
import 'package:flux/features/operations/blocs/service_files_bloc.dart';
|
||||
import 'package:flux/features/operations/models/service_model.dart';
|
||||
import 'package:flux/features/operations/ui/service_form_screen/service_form_screen.dart';
|
||||
import 'package:flux/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart';
|
||||
import 'package:flux/features/operations/ui/services_screen.dart';
|
||||
import 'package:flux/features/operations/blocs/operation_files_bloc.dart';
|
||||
import 'package:flux/features/operations/models/operation_model.dart';
|
||||
import 'package:flux/features/operations/ui/operation_form_screen/operation_form_screen.dart';
|
||||
import 'package:flux/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart';
|
||||
import 'package:flux/features/operations/ui/operations_screen.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
@@ -132,7 +132,7 @@ class AppRouter {
|
||||
),
|
||||
GoRoute(
|
||||
path: '/operations',
|
||||
builder: (context, state) => const ServicesScreen(),
|
||||
builder: (context, state) => const OperationsScreen(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/customers',
|
||||
@@ -171,14 +171,15 @@ class AppRouter {
|
||||
path: '/operation-form',
|
||||
name: 'operation-form',
|
||||
builder: (context, state) {
|
||||
final existingService = state.extra as ServiceModel?;
|
||||
final serviceId = state.uri.queryParameters['serviceId'];
|
||||
final existingOperation = state.extra as OperationModel?;
|
||||
final operationId = state.uri.queryParameters['operationId'];
|
||||
return BlocProvider(
|
||||
create: (context) =>
|
||||
ServiceFilesBloc(serviceId: serviceId ?? existingService?.id),
|
||||
child: ServiceFormScreen(
|
||||
serviceId: serviceId ?? existingService?.id,
|
||||
existingService: existingService,
|
||||
create: (context) => OperationFilesBloc(
|
||||
operationId: operationId ?? existingOperation?.id,
|
||||
),
|
||||
child: OperationFormScreen(
|
||||
operationId: operationId ?? existingOperation?.id,
|
||||
existingOperation: existingOperation,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -186,13 +187,14 @@ class AppRouter {
|
||||
GoRoute(
|
||||
path: '/operation/:id/upload',
|
||||
builder: (context, state) {
|
||||
final serviceId = state.pathParameters['id']!;
|
||||
final serviceName = state.uri.queryParameters['name'] ?? 'Pratica';
|
||||
final operationId = state.pathParameters['id']!;
|
||||
final operationName =
|
||||
state.uri.queryParameters['name'] ?? 'Pratica';
|
||||
return BlocProvider(
|
||||
create: (context) => ServiceFilesBloc(serviceId: serviceId),
|
||||
child: ServiceMobileUploadScreen(
|
||||
serviceId: serviceId,
|
||||
serviceName: serviceName,
|
||||
create: (context) => OperationFilesBloc(operationId: operationId),
|
||||
child: OperationMobileUploadScreen(
|
||||
operationId: operationId,
|
||||
operationName: operationName,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// lib/ui/common/flux_text_field.dart
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/operations.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
|
||||
class FluxTextField extends StatefulWidget {
|
||||
|
||||
@@ -263,7 +263,7 @@ extension CompanyLimits on CompanyModel {
|
||||
}
|
||||
}
|
||||
|
||||
int get maxServicesPerMonth {
|
||||
int get maxOperationsPerMonth {
|
||||
switch (subscriptionTier) {
|
||||
case SubscriptionTier.free:
|
||||
return 50;
|
||||
|
||||
@@ -91,7 +91,7 @@ class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.person_add),
|
||||
onPressed: () async {
|
||||
final servicesCubit = context.read<ServicesCubit>();
|
||||
final operationsCubit = context.read<OperationsCubit>();
|
||||
// Apriamo la dialog passando la query attuale
|
||||
final CustomerModel? nuovoCliente = await showDialog(
|
||||
context: context,
|
||||
@@ -101,7 +101,7 @@ class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
|
||||
);
|
||||
|
||||
if (nuovoCliente != null) {
|
||||
servicesCubit.updateField(
|
||||
operationsCubit.updateField(
|
||||
customerId: nuovoCliente.id,
|
||||
customerDisplayName: nuovoCliente.nome,
|
||||
);
|
||||
@@ -180,7 +180,7 @@ class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
|
||||
),
|
||||
onTap: () {
|
||||
// Salviamo l'ID e il nome formattato nel form dei servizi
|
||||
context.read<ServicesCubit>().updateField(
|
||||
context.read<OperationsCubit>().updateField(
|
||||
customerId: customer.id,
|
||||
customerDisplayName: displayName,
|
||||
);
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flux/features/operations/data/operations_repository.dart';
|
||||
import 'package:flux/features/operations/models/operation_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
part '../../latest_store_operations/bloc/latest_store_operations_events.dart';
|
||||
part '../../latest_store_operations/bloc/latest_store_operations_state.dart';
|
||||
|
||||
class LatestStoreOperationsBloc
|
||||
extends Bloc<LatestStoreOperationsEvent, LatestStoreOperationsState> {
|
||||
final _repository = GetIt.I.get<OperationsRepository>();
|
||||
|
||||
LatestStoreOperationsBloc()
|
||||
: super(
|
||||
const LatestStoreOperationsState(
|
||||
status: LatestStoreOperationsStatus.initial,
|
||||
),
|
||||
) {
|
||||
on<InitLastStoreOperationsEvent>((event, emit) async {
|
||||
emit(state.copyWith(status: LatestStoreOperationsStatus.loading));
|
||||
try {
|
||||
// 1. Creiamo uno stream "intermedio" che idrata i dati
|
||||
final hydratedStream = _repository
|
||||
.getLastStoreOperationsStream(storeId: event.storeId, limit: 5)
|
||||
.asyncMap((List<OperationModel> rawOperations) async {
|
||||
// Questo gira ad ogni "scatto" dello stream di Supabase
|
||||
List<OperationModel> fullyHydratedOperations = [];
|
||||
|
||||
for (OperationModel operation in rawOperations) {
|
||||
// Peschiamo i dati completi (incluso il cliente)
|
||||
OperationModel fullOperation = await _repository
|
||||
.fetchOperationById(operation.id!);
|
||||
fullyHydratedOperations.add(fullOperation);
|
||||
}
|
||||
|
||||
// Passiamo la lista completa allo step successivo
|
||||
return fullyHydratedOperations;
|
||||
});
|
||||
|
||||
// 2. Ora passiamo lo stream idratato all'emit.forEach
|
||||
await emit.forEach(
|
||||
hydratedStream, // Usiamo lo stream modificato!
|
||||
onData: (List<OperationModel> fullyHydratedOperations) {
|
||||
// Qui ora è tutto sincrono e bellissimo
|
||||
return state.copyWith(
|
||||
operations: fullyHydratedOperations,
|
||||
status: LatestStoreOperationsStatus.success,
|
||||
);
|
||||
},
|
||||
onError: (error, stackTrace) => state.copyWith(
|
||||
status: LatestStoreOperationsStatus.failure,
|
||||
error: error.toString(),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: LatestStoreOperationsStatus.failure,
|
||||
error: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
part of 'latest_store_operations_bloc.dart';
|
||||
|
||||
sealed class LatestStoreOperationsEvent extends Equatable {
|
||||
const LatestStoreOperationsEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class InitLastStoreOperationsEvent extends LatestStoreOperationsEvent {
|
||||
final String storeId;
|
||||
|
||||
const InitLastStoreOperationsEvent(this.storeId);
|
||||
|
||||
@override
|
||||
List<Object> get props => [storeId];
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
part of 'latest_store_operations_bloc.dart';
|
||||
|
||||
enum LatestStoreOperationsStatus { initial, loading, success, failure }
|
||||
|
||||
class LatestStoreOperationsState extends Equatable {
|
||||
final LatestStoreOperationsStatus status;
|
||||
final String? error;
|
||||
final List<OperationModel> operations;
|
||||
|
||||
const LatestStoreOperationsState({
|
||||
required this.status,
|
||||
this.error,
|
||||
this.operations = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, error, operations];
|
||||
|
||||
LatestStoreOperationsState copyWith({
|
||||
LatestStoreOperationsStatus? status,
|
||||
String? error,
|
||||
List<OperationModel>? operations,
|
||||
}) {
|
||||
return LatestStoreOperationsState(
|
||||
status: status ?? this.status,
|
||||
error: error,
|
||||
operations: operations ?? this.operations,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/home/latest_store_operations/bloc/latest_store_operations_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class LatestStoreOperationsCard extends StatelessWidget {
|
||||
const LatestStoreOperationsCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentStoreId = context.read<SessionCubit>().state.currentStore?.id;
|
||||
|
||||
return BlocProvider(
|
||||
// 1. Creiamo il Bloc e facciamo partire subito la query
|
||||
create: (context) =>
|
||||
LatestStoreOperationsBloc()
|
||||
..add(InitLastStoreOperationsEvent(currentStoreId ?? '')),
|
||||
child: BlocListener<SessionCubit, SessionState>(
|
||||
// 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream!
|
||||
listenWhen: (previous, current) =>
|
||||
previous.currentStore?.id != current.currentStore?.id,
|
||||
listener: (context, state) {
|
||||
if (state.currentStore?.id != null) {
|
||||
context.read<LatestStoreOperationsBloc>().add(
|
||||
InitLastStoreOperationsEvent(state.currentStore!.id!),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: _LatestOperationsCardContent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LatestOperationsCardContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
const color = Colors.blue;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- HEADER DELLA CARD ---
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.design_services_outlined,
|
||||
color: color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => context.push('/operations'),
|
||||
child: Text(
|
||||
context.l10n.homeLatestOperations,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: context.primaryText,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// --- CORPO DELLA CARD (LA LISTA REAL-TIME) ---
|
||||
Expanded(
|
||||
child:
|
||||
BlocBuilder<
|
||||
LatestStoreOperationsBloc,
|
||||
LatestStoreOperationsState
|
||||
>(
|
||||
builder: (context, state) {
|
||||
if (state.status == LatestStoreOperationsStatus.loading ||
|
||||
state.status == LatestStoreOperationsStatus.initial) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (state.status == LatestStoreOperationsStatus.failure) {
|
||||
return Center(
|
||||
child: Text(
|
||||
"Errore di caricamento",
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.operations.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
"Nessun servizio recente.",
|
||||
style: TextStyle(
|
||||
color: context.secondaryText,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
itemCount: state.operations.length,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
height: 1,
|
||||
color: theme.dividerColor.withValues(alpha: 0.3),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final operation = state.operations[index];
|
||||
return InkWell(
|
||||
onTap: () => context.push(
|
||||
'/operation-form',
|
||||
extra: operation,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Text(
|
||||
operation.customerDisplayName ??
|
||||
'Cliente sconosciuto',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: context.primaryText,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Text(
|
||||
operation.number,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryText,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${operation.createdAt?.day}/${operation.createdAt?.month}",
|
||||
style: TextStyle(
|
||||
color: context.secondaryText,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flux/features/operations/data/services_repository.dart';
|
||||
import 'package:flux/features/operations/models/service_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
part 'latest_store_services_events.dart';
|
||||
part 'latest_store_services_state.dart';
|
||||
|
||||
class LatestStoreServicesBloc
|
||||
extends Bloc<LatestStoreServicesEvent, LatestStoreServicesState> {
|
||||
final _repository = GetIt.I.get<ServicesRepository>();
|
||||
|
||||
LatestStoreServicesBloc()
|
||||
: super(
|
||||
const LatestStoreServicesState(
|
||||
status: LatestStoreServicesStatus.initial,
|
||||
),
|
||||
) {
|
||||
on<InitLastStoreServicesEvent>((event, emit) async {
|
||||
emit(state.copyWith(status: LatestStoreServicesStatus.loading));
|
||||
try {
|
||||
// 1. Creiamo uno stream "intermedio" che idrata i dati
|
||||
final hydratedStream = _repository
|
||||
.getLastStoreServicesStream(storeId: event.storeId, limit: 5)
|
||||
.asyncMap((List<ServiceModel> rawServices) async {
|
||||
// Questo gira ad ogni "scatto" dello stream di Supabase
|
||||
List<ServiceModel> fullyHydratedServices = [];
|
||||
|
||||
for (ServiceModel operation in rawServices) {
|
||||
// Peschiamo i dati completi (incluso il cliente)
|
||||
ServiceModel fullService = await _repository.fetchServiceById(
|
||||
operation.id!,
|
||||
);
|
||||
fullyHydratedServices.add(fullService);
|
||||
}
|
||||
|
||||
// Passiamo la lista completa allo step successivo
|
||||
return fullyHydratedServices;
|
||||
});
|
||||
|
||||
// 2. Ora passiamo lo stream idratato all'emit.forEach
|
||||
await emit.forEach(
|
||||
hydratedStream, // Usiamo lo stream modificato!
|
||||
onData: (List<ServiceModel> fullyHydratedServices) {
|
||||
// Qui ora è tutto sincrono e bellissimo
|
||||
return state.copyWith(
|
||||
operations: fullyHydratedServices,
|
||||
status: LatestStoreServicesStatus.success,
|
||||
);
|
||||
},
|
||||
onError: (error, stackTrace) => state.copyWith(
|
||||
status: LatestStoreServicesStatus.failure,
|
||||
error: error.toString(),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: LatestStoreServicesStatus.failure,
|
||||
error: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
part of 'latest_store_services_bloc.dart';
|
||||
|
||||
sealed class LatestStoreServicesEvent extends Equatable {
|
||||
const LatestStoreServicesEvent();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
class InitLastStoreServicesEvent extends LatestStoreServicesEvent {
|
||||
final String storeId;
|
||||
|
||||
const InitLastStoreServicesEvent(this.storeId);
|
||||
|
||||
@override
|
||||
List<Object> get props => [storeId];
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
part of 'latest_store_services_bloc.dart';
|
||||
|
||||
enum LatestStoreServicesStatus { initial, loading, success, failure }
|
||||
|
||||
class LatestStoreServicesState extends Equatable {
|
||||
final LatestStoreServicesStatus status;
|
||||
final String? error;
|
||||
final List<ServiceModel> operations;
|
||||
|
||||
const LatestStoreServicesState({
|
||||
required this.status,
|
||||
this.error,
|
||||
this.operations = const [],
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, error, operations];
|
||||
|
||||
LatestStoreServicesState copyWith({
|
||||
LatestStoreServicesStatus? status,
|
||||
String? error,
|
||||
List<ServiceModel>? operations,
|
||||
}) {
|
||||
return LatestStoreServicesState(
|
||||
status: status ?? this.status,
|
||||
error: error,
|
||||
operations: operations ?? this.operations,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class LatestStoreServicesCard extends StatelessWidget {
|
||||
const LatestStoreServicesCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final currentStoreId = context.read<SessionCubit>().state.currentStore?.id;
|
||||
|
||||
return BlocProvider(
|
||||
// 1. Creiamo il Bloc e facciamo partire subito la query
|
||||
create: (context) =>
|
||||
LatestStoreServicesBloc()
|
||||
..add(InitLastStoreServicesEvent(currentStoreId ?? '')),
|
||||
child: BlocListener<SessionCubit, SessionState>(
|
||||
// 2. MAGIA: Se l'utente cambia negozio dalla barra in alto, riavviamo lo stream!
|
||||
listenWhen: (previous, current) =>
|
||||
previous.currentStore?.id != current.currentStore?.id,
|
||||
listener: (context, state) {
|
||||
if (state.currentStore?.id != null) {
|
||||
context.read<LatestStoreServicesBloc>().add(
|
||||
InitLastStoreServicesEvent(state.currentStore!.id!),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: _LatestServicesCardContent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LatestServicesCardContent extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
const color = Colors.blue;
|
||||
|
||||
return Card(
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: theme.dividerColor.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// --- HEADER DELLA CARD ---
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.design_services_outlined,
|
||||
color: color,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
onPressed: () => context.push('/operations'),
|
||||
child: Text(
|
||||
context.l10n.homeLatestServices,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: context.primaryText,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// --- CORPO DELLA CARD (LA LISTA REAL-TIME) ---
|
||||
Expanded(
|
||||
child: BlocBuilder<LatestStoreServicesBloc, LatestStoreServicesState>(
|
||||
builder: (context, state) {
|
||||
if (state.status == LatestStoreServicesStatus.loading ||
|
||||
state.status == LatestStoreServicesStatus.initial) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (state.status == LatestStoreServicesStatus.failure) {
|
||||
return Center(
|
||||
child: Text(
|
||||
"Errore di caricamento",
|
||||
style: TextStyle(color: theme.colorScheme.error),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (state.operations.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
"Nessun servizio recente.",
|
||||
style: TextStyle(
|
||||
color: context.secondaryText,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
itemCount: state.operations.length,
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
height: 1,
|
||||
color: theme.dividerColor.withValues(alpha: 0.3),
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final operation = state.operations[index];
|
||||
return InkWell(
|
||||
onTap: () =>
|
||||
context.push('/operation-form', extra: operation),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Text(
|
||||
operation.customerDisplayName ??
|
||||
'Cliente sconosciuto',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: context.primaryText,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: Text(
|
||||
operation.number,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryText,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${operation.createdAt?.day}/${operation.createdAt?.month}",
|
||||
style: TextStyle(
|
||||
color: context.secondaryText,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/theme/theme.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/home/latest_store_services/ui/latest_store_services_card.dart';
|
||||
import 'package:flux/features/home/latest_store_operations/ui/latest_store_operations_card.dart';
|
||||
import 'package:flux/features/home/ui/quick_actions_widget.dart';
|
||||
import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -76,10 +76,10 @@ class HomeScreen extends StatelessWidget {
|
||||
color: Colors.green,
|
||||
context: context,
|
||||
),
|
||||
LatestStoreServicesCard(),
|
||||
LatestStoreOperationsCard(),
|
||||
|
||||
_buildDashboardWidget(
|
||||
title: context.l10n.homeLatestServiceTickets,
|
||||
title: context.l10n.homeLatestOperationTickets,
|
||||
icon: Icons.support_agent_outlined,
|
||||
color: Colors.purple,
|
||||
context: context,
|
||||
@@ -181,7 +181,7 @@ class HomeScreen extends StatelessWidget {
|
||||
children: [
|
||||
QuickActionButton(
|
||||
icon: Icons.add,
|
||||
label: context.l10n.commonService,
|
||||
label: context.l10n.commonOperation,
|
||||
color: Colors.blue,
|
||||
onTap: () {
|
||||
// Entriamo nel form! Nessun parametro extra = Nuovo Servizio
|
||||
@@ -191,7 +191,7 @@ class HomeScreen extends StatelessWidget {
|
||||
const SizedBox(width: 12),
|
||||
QuickActionButton(
|
||||
icon: Icons.handyman,
|
||||
label: context.l10n.homeNewServiceTicket,
|
||||
label: context.l10n.homeNewOperationTicket,
|
||||
color: Colors.redAccent,
|
||||
onTap: () {
|
||||
// TODO: Quando avrai la rotta per la nuova assistenza
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/operations.dart'; // <-- IMPORTANTE per i formatter
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/widgets/flux_text_field.dart';
|
||||
import 'package:flux/features/master_data/store/models/store_model.dart';
|
||||
|
||||
242
lib/features/operations/blocs/operation_files_bloc.dart
Normal file
242
lib/features/operations/blocs/operation_files_bloc.dart
Normal file
@@ -0,0 +1,242 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/operations/data/operations_repository.dart';
|
||||
import 'package:flux/features/operations/models/operation_file_model.dart';
|
||||
import 'package:flux/features/operations/models/operation_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
part 'operation_files_events.dart';
|
||||
part 'operation_files_state.dart';
|
||||
|
||||
class OperationFilesBloc
|
||||
extends Bloc<OperationFilesEvent, OperationFilesState> {
|
||||
final _repository = GetIt.I.get<OperationsRepository>();
|
||||
final String? operationId;
|
||||
|
||||
OperationFilesBloc({this.operationId})
|
||||
: super(
|
||||
OperationFilesState(
|
||||
status: OperationFilesStatus.initial,
|
||||
operationId: operationId,
|
||||
),
|
||||
) {
|
||||
on<OperationsavedEvent>(_onOperationsaved);
|
||||
on<LoadOperationFilesEvent>(_onLoadOperationFiles);
|
||||
on<AddOperationFilesEvent>(_onAddOperationFiles);
|
||||
on<UploadOperationFilesEvent>(_onUploadOperationFiles);
|
||||
on<UploadMultipleOperationFilesEvent>(_onUploadMultipleOperationFiles);
|
||||
on<DeleteOperationFilesEvent>(_onDeleteOperationFiles);
|
||||
on<ToggleOperationFileSelectionEvent>(_onToggleOperationFileSelection);
|
||||
// Se il BLoC nasce con un ID, accendiamo subito lo stream!
|
||||
if (operationId != null) {
|
||||
add(LoadOperationFilesEvent(operationId: operationId));
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onOperationsaved(
|
||||
OperationsavedEvent event,
|
||||
Emitter<OperationFilesState> emit,
|
||||
) {
|
||||
// 1. Aggiorniamo l'ID nello stato
|
||||
// 2. PIALLIAMO i file locali: ormai sono partiti per Supabase!
|
||||
// Così la UI si pulisce all'istante e aspetta quelli remoti.
|
||||
emit(
|
||||
state.copyWith(
|
||||
operationId: event.operationId,
|
||||
localFiles: [], // <-- LA MAGIA ANTI-DUPLICATI
|
||||
),
|
||||
);
|
||||
|
||||
// Lanciamo il caricamento
|
||||
add(LoadOperationFilesEvent(operationId: event.operationId));
|
||||
}
|
||||
|
||||
FutureOr<void> _onLoadOperationFiles(
|
||||
LoadOperationFilesEvent event,
|
||||
Emitter<OperationFilesState> emit,
|
||||
) async {
|
||||
// Usiamo l'ID dell'evento, e se non c'è usiamo quello dello stato
|
||||
final currentId = event.operationId ?? state.operationId;
|
||||
|
||||
if (currentId != null) {
|
||||
emit(state.copyWith(status: OperationFilesStatus.loading));
|
||||
|
||||
await emit.forEach(
|
||||
_repository.getOperationFilesStream(
|
||||
currentId,
|
||||
), // <-- Usiamo l'ID corretto!
|
||||
onData: (data) => state.copyWith(
|
||||
status: OperationFilesStatus.success,
|
||||
remoteFiles: data,
|
||||
),
|
||||
onError: (error, stackTrace) => state.copyWith(
|
||||
status: OperationFilesStatus.failure,
|
||||
error: error.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onAddOperationFiles(
|
||||
AddOperationFilesEvent event,
|
||||
Emitter<OperationFilesState> emit,
|
||||
) async {
|
||||
final currentId = state.operationId;
|
||||
// BIVIO 1: PRATICA NUOVA (Nessun ID)
|
||||
if (currentId == null) {
|
||||
// Mettiamo i file nel "parcheggio" locale dello State
|
||||
final newLocalFiles = event.files.map((file) {
|
||||
return OperationFileModel(
|
||||
id: null,
|
||||
operationId: operationId ?? '',
|
||||
name: file.name.fileNameWithoutExtension(),
|
||||
extension: file.name.fileExtension(),
|
||||
storagePath: '',
|
||||
fileSize: file.size,
|
||||
localBytes: file.bytes,
|
||||
);
|
||||
}).toList();
|
||||
final List<OperationFileModel> updatedLocalFiles = [
|
||||
...state.localFiles,
|
||||
...newLocalFiles,
|
||||
];
|
||||
emit(
|
||||
state.copyWith(
|
||||
localFiles: updatedLocalFiles,
|
||||
status: OperationFilesStatus.success,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID)
|
||||
emit(state.copyWith(status: OperationFilesStatus.uploading));
|
||||
try {
|
||||
// Logica identica a quella che abbiamo fatto per i clienti
|
||||
for (var file in event.files) {
|
||||
await _repository.uploadAndRegisterOperationFile(
|
||||
operationId: operationId!,
|
||||
pickedFile: file,
|
||||
);
|
||||
}
|
||||
emit(state.copyWith(status: OperationFilesStatus.success));
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OperationFilesStatus.failure,
|
||||
error: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onUploadOperationFiles(
|
||||
UploadOperationFilesEvent event,
|
||||
Emitter<OperationFilesState> emit,
|
||||
) async {
|
||||
if (event.pickedFiles == null && event.photos == null) return;
|
||||
if (event.pickedFiles!.isEmpty && event.photos!.isEmpty) return;
|
||||
|
||||
// BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID
|
||||
emit(state.copyWith(status: OperationFilesStatus.uploading));
|
||||
try {
|
||||
// Logica identica a quella che abbiamo fatto per i clienti
|
||||
if (event.pickedFiles != null && event.pickedFiles!.isNotEmpty) {
|
||||
for (var file in event.pickedFiles!) {
|
||||
await _repository.uploadAndRegisterOperationFile(
|
||||
operationId: state.operationId!,
|
||||
pickedFile: file,
|
||||
);
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(status: OperationFilesStatus.success));
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OperationFilesStatus.failure,
|
||||
error: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onUploadMultipleOperationFiles(
|
||||
UploadMultipleOperationFilesEvent event,
|
||||
Emitter<OperationFilesState> emit,
|
||||
) async {
|
||||
if (event.files.isEmpty) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OperationFilesStatus.failure,
|
||||
error: "Nessun file selezionato",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(status: OperationFilesStatus.uploading, error: null));
|
||||
try {
|
||||
// 2. Creiamo una lista di "Promesse" (Futures) per il repository
|
||||
final List<Future<void>> uploadTasks = [];
|
||||
for (var file in event.files) {
|
||||
// Aggiungiamo il task alla lista, ma NON usiamo await qui dentro!
|
||||
uploadTasks.add(
|
||||
_repository.uploadAndRegisterOperationFile(
|
||||
operationId: state.operationId!,
|
||||
pickedFile: file,
|
||||
),
|
||||
);
|
||||
}
|
||||
// 3. ESECUZIONE PARALLELA!
|
||||
// Aspettiamo che tutti i file siano caricati contemporaneamente.
|
||||
await Future.wait(uploadTasks);
|
||||
// 4. GRAN FINALE: Tutto caricato, emettiamo il success!
|
||||
emit(state.copyWith(status: OperationFilesStatus.success));
|
||||
} catch (e) {
|
||||
// Se anche un solo file fallisce, catturiamo l'errore
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OperationFilesStatus.failure,
|
||||
error: "Errore durante l'upload multiplo: $e",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onDeleteOperationFiles(
|
||||
DeleteOperationFilesEvent event,
|
||||
Emitter<OperationFilesState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: OperationFilesStatus.loading));
|
||||
try {
|
||||
await _repository.deleteOperationFiles(state.selectedFiles);
|
||||
emit(
|
||||
state.copyWith(status: OperationFilesStatus.success, selectedFiles: []),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: OperationFilesStatus.failure,
|
||||
error: e.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onToggleOperationFileSelection(
|
||||
ToggleOperationFileSelectionEvent event,
|
||||
Emitter<OperationFilesState> emit,
|
||||
) {
|
||||
List<OperationFileModel> selectedFiles = List.from(state.selectedFiles);
|
||||
if (selectedFiles.contains(event.file)) {
|
||||
selectedFiles.remove(event.file);
|
||||
} else {
|
||||
selectedFiles.add(event.file);
|
||||
}
|
||||
emit(state.copyWith(selectedFiles: selectedFiles));
|
||||
}
|
||||
}
|
||||
56
lib/features/operations/blocs/operation_files_events.dart
Normal file
56
lib/features/operations/blocs/operation_files_events.dart
Normal file
@@ -0,0 +1,56 @@
|
||||
part of 'operation_files_bloc.dart';
|
||||
|
||||
abstract class OperationFilesEvent extends Equatable {
|
||||
const OperationFilesEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class OperationsavedEvent extends OperationFilesEvent {
|
||||
final String operationId;
|
||||
const OperationsavedEvent(this.operationId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [operationId];
|
||||
}
|
||||
|
||||
class LoadOperationFilesEvent extends OperationFilesEvent {
|
||||
final String? operationId;
|
||||
final OperationModel? operation;
|
||||
const LoadOperationFilesEvent({this.operationId, this.operation});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [operationId, operation];
|
||||
}
|
||||
|
||||
class AddOperationFilesEvent extends OperationFilesEvent {
|
||||
final List<PlatformFile> files;
|
||||
const AddOperationFilesEvent(this.files);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [files];
|
||||
}
|
||||
|
||||
class UploadOperationFilesEvent extends OperationFilesEvent {
|
||||
final List<PlatformFile>? pickedFiles;
|
||||
final List<File>? photos;
|
||||
const UploadOperationFilesEvent({this.pickedFiles, this.photos});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [pickedFiles, photos];
|
||||
}
|
||||
|
||||
class UploadMultipleOperationFilesEvent extends OperationFilesEvent {
|
||||
final List<PlatformFile> files;
|
||||
const UploadMultipleOperationFilesEvent(this.files);
|
||||
@override
|
||||
List<Object?> get props => [files];
|
||||
}
|
||||
|
||||
class DeleteOperationFilesEvent extends OperationFilesEvent {}
|
||||
|
||||
class ToggleOperationFileSelectionEvent extends OperationFilesEvent {
|
||||
final OperationFileModel file;
|
||||
const ToggleOperationFileSelectionEvent(this.file);
|
||||
}
|
||||
52
lib/features/operations/blocs/operation_files_state.dart
Normal file
52
lib/features/operations/blocs/operation_files_state.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
part of 'operation_files_bloc.dart';
|
||||
|
||||
enum OperationFilesStatus { initial, loading, uploading, success, failure }
|
||||
|
||||
class OperationFilesState extends Equatable {
|
||||
const OperationFilesState({
|
||||
this.operationId,
|
||||
required this.status,
|
||||
this.error,
|
||||
this.localFiles = const [],
|
||||
this.remoteFiles = const [],
|
||||
this.selectedFiles = const [],
|
||||
});
|
||||
|
||||
final String? operationId;
|
||||
final OperationFilesStatus status;
|
||||
final String? error;
|
||||
final List<OperationFileModel> localFiles;
|
||||
final List<OperationFileModel> remoteFiles;
|
||||
|
||||
final List<OperationFileModel> selectedFiles;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
operationId,
|
||||
status,
|
||||
error,
|
||||
localFiles,
|
||||
remoteFiles,
|
||||
selectedFiles,
|
||||
];
|
||||
|
||||
List<OperationFileModel> get allFiles => [...remoteFiles, ...localFiles];
|
||||
|
||||
OperationFilesState copyWith({
|
||||
String? operationId,
|
||||
OperationFilesStatus? status,
|
||||
String? error,
|
||||
List<OperationFileModel>? localFiles,
|
||||
List<OperationFileModel>? remoteFiles,
|
||||
List<OperationFileModel>? selectedFiles,
|
||||
}) {
|
||||
return OperationFilesState(
|
||||
operationId: operationId ?? this.operationId,
|
||||
status: status ?? this.status,
|
||||
error: error,
|
||||
localFiles: localFiles ?? this.localFiles,
|
||||
remoteFiles: remoteFiles ?? this.remoteFiles,
|
||||
selectedFiles: selectedFiles ?? this.selectedFiles,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,50 +4,51 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/operations/data/services_repository.dart';
|
||||
import 'package:flux/features/operations/models/energy_service_model.dart';
|
||||
import 'package:flux/features/operations/models/entertainment_service_model.dart';
|
||||
import 'package:flux/features/operations/models/fin_service_model.dart';
|
||||
import 'package:flux/features/operations/models/service_file_model.dart';
|
||||
import 'package:flux/features/operations/models/service_model.dart';
|
||||
import 'package:flux/features/operations/data/operations_repository.dart';
|
||||
import 'package:flux/features/operations/models/energy_operation_model.dart';
|
||||
import 'package:flux/features/operations/models/entertainment_operation_model.dart';
|
||||
import 'package:flux/features/operations/models/fin_operation_model.dart';
|
||||
import 'package:flux/features/operations/models/operation_file_model.dart';
|
||||
import 'package:flux/features/operations/models/operation_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
part 'services_state.dart';
|
||||
part 'operations_state.dart';
|
||||
|
||||
class ServicesCubit extends Cubit<ServicesState> {
|
||||
final ServicesRepository _repository = GetIt.I<ServicesRepository>();
|
||||
class OperationsCubit extends Cubit<OperationsState> {
|
||||
final OperationsRepository _repository = GetIt.I<OperationsRepository>();
|
||||
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||
|
||||
ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial));
|
||||
OperationsCubit()
|
||||
: super(const OperationsState(status: OperationsStatus.initial));
|
||||
|
||||
// --- CARICAMENTO E PAGINAZIONE ---
|
||||
|
||||
Future<void> loadServices({bool refresh = false}) async {
|
||||
Future<void> loadOperations({bool refresh = false}) async {
|
||||
// Se stiamo già caricando, evitiamo chiamate doppie
|
||||
if (state.status == ServicesStatus.loading) return;
|
||||
if (state.status == OperationsStatus.loading) return;
|
||||
|
||||
// Se non è un refresh e abbiamo già raggiunto la fine dei dati, ci fermiamo
|
||||
if (!refresh && state.hasReachedMax) return;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ServicesStatus.loading,
|
||||
status: OperationsStatus.loading,
|
||||
errorMessage: null,
|
||||
// Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading
|
||||
allServices: refresh ? [] : state.allServices,
|
||||
allOperations: refresh ? [] : state.allOperations,
|
||||
hasReachedMax: refresh ? false : state.hasReachedMax,
|
||||
),
|
||||
);
|
||||
|
||||
try {
|
||||
final currentOffset = refresh ? 0 : state.allServices.length;
|
||||
final currentOffset = refresh ? 0 : state.allOperations.length;
|
||||
final companyId = _sessionCubit.state.company?.id;
|
||||
|
||||
if (companyId == null) {
|
||||
throw Exception("Company ID non trovato nella sessione");
|
||||
}
|
||||
|
||||
final newServices = await _repository.fetchServices(
|
||||
final newOperations = await _repository.fetchOperations(
|
||||
companyId: companyId,
|
||||
offset: currentOffset,
|
||||
limit: 50,
|
||||
@@ -56,21 +57,21 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
);
|
||||
|
||||
// Se ricevi meno record del limite, significa che non ce ne sono altri sul DB
|
||||
final bool reachedMax = newServices.length < 50;
|
||||
final bool reachedMax = newOperations.length < 50;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ServicesStatus.ready,
|
||||
allServices: refresh
|
||||
? newServices
|
||||
: [...state.allServices, ...newServices],
|
||||
status: OperationsStatus.ready,
|
||||
allOperations: refresh
|
||||
? newOperations
|
||||
: [...state.allOperations, ...newOperations],
|
||||
hasReachedMax: reachedMax,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ServicesStatus.failure,
|
||||
status: OperationsStatus.failure,
|
||||
errorMessage: "Errore nel caricamento servizi: $e",
|
||||
),
|
||||
);
|
||||
@@ -87,51 +88,51 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
dateRange: range ?? state.dateRange,
|
||||
),
|
||||
);
|
||||
loadServices(refresh: true);
|
||||
loadOperations(refresh: true);
|
||||
}
|
||||
|
||||
/// Pulisce tutti i filtri
|
||||
void clearFilters() {
|
||||
emit(state.copyWith(query: '', dateRange: null));
|
||||
loadServices(refresh: true);
|
||||
loadOperations(refresh: true);
|
||||
}
|
||||
|
||||
// --- GESTIONE BOZZA (DRAFT) ---
|
||||
|
||||
/// Inizializza un nuovo servizio o ne carica uno esistente per la modifica
|
||||
void initServiceForm({
|
||||
ServiceModel? existingService,
|
||||
String? serviceId,
|
||||
void initOperationForm({
|
||||
OperationModel? existingOperation,
|
||||
String? operationId,
|
||||
}) async {
|
||||
if (existingService != null) {
|
||||
if (existingOperation != null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentService: existingService,
|
||||
status: ServicesStatus.ready,
|
||||
currentOperation: existingOperation,
|
||||
status: OperationsStatus.ready,
|
||||
),
|
||||
);
|
||||
} else if (serviceId != null) {
|
||||
ServiceModel? serviceModel = state.allServices.firstWhereOrNull(
|
||||
(s) => s.id == serviceId,
|
||||
} else if (operationId != null) {
|
||||
OperationModel? operationModel = state.allOperations.firstWhereOrNull(
|
||||
(s) => s.id == operationId,
|
||||
);
|
||||
serviceModel ??= await _repository.fetchServiceById(serviceId);
|
||||
operationModel ??= await _repository.fetchOperationById(operationId);
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentService: serviceModel,
|
||||
status: ServicesStatus.ready,
|
||||
currentOperation: operationModel,
|
||||
status: OperationsStatus.ready,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
// Crea un template vuoto con lo store di default (se disponibile)
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentService: ServiceModel(
|
||||
currentOperation: OperationModel(
|
||||
storeId: _sessionCubit.state.currentStore?.id ?? '',
|
||||
number: '', // Sarà compilato dall'utente
|
||||
createdAt: DateTime.now(),
|
||||
companyId: _sessionCubit.state.company!.id!,
|
||||
),
|
||||
status: ServicesStatus.ready,
|
||||
status: OperationsStatus.ready,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -151,9 +152,9 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
String? customerId,
|
||||
String? customerDisplayName,
|
||||
}) {
|
||||
if (state.currentService == null) return;
|
||||
if (state.currentOperation == null) return;
|
||||
|
||||
final updated = state.currentService!.copyWith(
|
||||
final updated = state.currentOperation!.copyWith(
|
||||
al: al,
|
||||
mnp: mnp,
|
||||
nip: nip,
|
||||
@@ -167,34 +168,38 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
customerDisplayName: customerDisplayName,
|
||||
);
|
||||
|
||||
emit(state.copyWith(currentService: updated));
|
||||
emit(state.copyWith(currentOperation: updated));
|
||||
}
|
||||
|
||||
// --- GESTIONE MODULI COMPLESSI ---
|
||||
|
||||
void updateEnergyServices(List<EnergyServiceModel> energyList) {
|
||||
void updateEnergyOperations(List<EnergyOperationModel> energyList) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentService: state.currentService?.copyWith(
|
||||
energyServices: energyList,
|
||||
currentOperation: state.currentOperation?.copyWith(
|
||||
energyOperations: energyList,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void updateFinServices(List<FinServiceModel> finList) {
|
||||
void updateFinOperations(List<FinOperationModel> finList) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentService: state.currentService?.copyWith(finServices: finList),
|
||||
currentOperation: state.currentOperation?.copyWith(
|
||||
finOperations: finList,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void updateEntertainmentServices(List<EntertainmentServiceModel> entList) {
|
||||
void updateEntertainmentOperations(
|
||||
List<EntertainmentOperationModel> entList,
|
||||
) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentService: state.currentService?.copyWith(
|
||||
entertainmentServices: entList,
|
||||
currentOperation: state.currentOperation?.copyWith(
|
||||
entertainmentOperations: entList,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -202,36 +207,40 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
|
||||
// --- PERSISTENZA ---
|
||||
|
||||
Future<void> saveCurrentService({
|
||||
Future<void> saveCurrentOperation({
|
||||
required bool isBozza,
|
||||
bool shouldPop = true,
|
||||
List<ServiceFileModel>? files,
|
||||
List<OperationFileModel>? files,
|
||||
}) async {
|
||||
if (state.currentService == null) return;
|
||||
if (state.currentOperation == null) return;
|
||||
|
||||
emit(state.copyWith(status: ServicesStatus.saving, errorMessage: null));
|
||||
emit(state.copyWith(status: OperationsStatus.saving, errorMessage: null));
|
||||
try {
|
||||
// 1. Aggiorniamo il flag bozza in base a quale pulsante ha premuto l'utente
|
||||
final serviceToSave = state.currentService!.copyWith(
|
||||
final operationToSave = state.currentOperation!.copyWith(
|
||||
isBozza: isBozza,
|
||||
files: files,
|
||||
);
|
||||
|
||||
// 2. Salvataggio corazzato
|
||||
final updatedService = await _repository.saveFullService(serviceToSave);
|
||||
final updatedOperation = await _repository.saveFullOperation(
|
||||
operationToSave,
|
||||
);
|
||||
|
||||
// 3. Reset e ricaricamento
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: shouldPop ? ServicesStatus.saved : ServicesStatus.savedNoPop,
|
||||
currentService: shouldPop ? null : updatedService,
|
||||
status: shouldPop
|
||||
? OperationsStatus.saved
|
||||
: OperationsStatus.savedNoPop,
|
||||
currentOperation: shouldPop ? null : updatedOperation,
|
||||
),
|
||||
);
|
||||
await loadServices(refresh: true);
|
||||
await loadOperations(refresh: true);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ServicesStatus.failure,
|
||||
status: OperationsStatus.failure,
|
||||
errorMessage: e.toString(),
|
||||
),
|
||||
);
|
||||
@@ -242,9 +251,9 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
|
||||
void addAttachments(List<PlatformFile> files) {
|
||||
final newAttachments = files.map((file) {
|
||||
return ServiceFileModel(
|
||||
return OperationFileModel(
|
||||
id: null, // Meglio null se non è su DB
|
||||
serviceId: state.currentService?.id ?? '',
|
||||
operationId: state.currentOperation?.id ?? '',
|
||||
name: file.name.fileNameWithoutExtension(),
|
||||
extension: file.name.fileExtension(),
|
||||
storagePath: '',
|
||||
@@ -255,44 +264,46 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
}).toList();
|
||||
|
||||
// Creiamo una nuova lista pulita
|
||||
final List<ServiceFileModel> updatedList = [
|
||||
...(state.currentService?.files ?? []),
|
||||
final List<OperationFileModel> updatedList = [
|
||||
...(state.currentOperation?.files ?? []),
|
||||
...newAttachments,
|
||||
];
|
||||
|
||||
// Emettiamo lo stato assicurandoci che il ServiceModel venga clonato
|
||||
if (state.currentService != null) {
|
||||
// Emettiamo lo stato assicurandoci che il OperationModel venga clonato
|
||||
if (state.currentOperation != null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentService: state.currentService!.copyWith(files: updatedList),
|
||||
currentOperation: state.currentOperation!.copyWith(
|
||||
files: updatedList,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void removeAttachment(int index) {
|
||||
if (state.currentService == null) return;
|
||||
if (state.currentOperation == null) return;
|
||||
|
||||
final updatedList = List<ServiceFileModel>.from(
|
||||
state.currentService!.files,
|
||||
final updatedList = List<OperationFileModel>.from(
|
||||
state.currentOperation!.files,
|
||||
);
|
||||
updatedList.removeAt(index);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentService: state.currentService?.copyWith(files: updatedList),
|
||||
currentOperation: state.currentOperation?.copyWith(files: updatedList),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void saveAndCopyFileToCustomer(List<ServiceFileModel> selectedFiles) async {
|
||||
final currentService = state.currentService;
|
||||
void saveAndCopyFileToCustomer(List<OperationFileModel> selectedFiles) async {
|
||||
final currentOperation = state.currentOperation;
|
||||
|
||||
// 1. Check di sicurezza: se non c'è il cliente, non sappiamo dove copiare
|
||||
if (currentService == null || currentService.customerId == null) {
|
||||
if (currentOperation == null || currentOperation.customerId == null) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ServicesStatus.failure,
|
||||
status: OperationsStatus.failure,
|
||||
errorMessage:
|
||||
"Impossibile copiare: nessun cliente associato alla pratica.",
|
||||
),
|
||||
@@ -300,19 +311,21 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(status: ServicesStatus.loading));
|
||||
emit(state.copyWith(status: OperationsStatus.loading));
|
||||
|
||||
try {
|
||||
// 2. SALVATAGGIO CORAZZATO
|
||||
// Chiamiamo il repo e otteniamo la pratica con TUTTI i file ora dotati di ID e storagePath
|
||||
final updatedService = await _repository.saveFullService(currentService);
|
||||
final updatedOperation = await _repository.saveFullOperation(
|
||||
currentOperation,
|
||||
);
|
||||
|
||||
// 3. COPIA RELAZIONALE
|
||||
// Per ogni file che l'utente ha selezionato nella UI, cerchiamo la sua versione
|
||||
// "ufficiale" (quella con lo storagePath) nel modello appena tornato dal DB.
|
||||
for (var selectedFile in selectedFiles) {
|
||||
// Cerchiamo il match nel modello aggiornato
|
||||
final persistedFile = updatedService.files.firstWhere(
|
||||
final persistedFile = updatedOperation.files.firstWhere(
|
||||
(f) =>
|
||||
f.name == selectedFile.name &&
|
||||
f.extension == selectedFile.extension,
|
||||
@@ -324,7 +337,7 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
// Creiamo il link nel database del cliente
|
||||
await _repository.copyFileToCustomer(
|
||||
file: persistedFile,
|
||||
customerId: currentService.customerId!,
|
||||
customerId: currentOperation.customerId!,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -332,14 +345,14 @@ class ServicesCubit extends Cubit<ServicesState> {
|
||||
// Aggiorniamo il Cubit con il servizio salvato così la UI mostra i file come "Remoti"
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ServicesStatus.success,
|
||||
currentService: updatedService,
|
||||
status: OperationsStatus.success,
|
||||
currentOperation: updatedOperation,
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ServicesStatus.failure,
|
||||
status: OperationsStatus.failure,
|
||||
errorMessage: "Errore durante il salvataggio e copia: $e",
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
part of 'operations_cubit.dart';
|
||||
|
||||
enum ServicesStatus {
|
||||
enum OperationsStatus {
|
||||
initial,
|
||||
loading,
|
||||
ready,
|
||||
@@ -11,20 +11,20 @@ enum ServicesStatus {
|
||||
failure,
|
||||
}
|
||||
|
||||
class ServicesState extends Equatable {
|
||||
final ServicesStatus status;
|
||||
final List<ServiceModel> allServices;
|
||||
final ServiceModel? currentService; // La bozza che stiamo editando
|
||||
class OperationsState extends Equatable {
|
||||
final OperationsStatus status;
|
||||
final List<OperationModel> allOperations;
|
||||
final OperationModel? currentOperation; // La bozza che stiamo editando
|
||||
final String? errorMessage;
|
||||
final String query;
|
||||
final DateTimeRange? dateRange;
|
||||
final bool hasReachedMax;
|
||||
final bool isSavingDraft;
|
||||
|
||||
const ServicesState({
|
||||
const OperationsState({
|
||||
required this.status,
|
||||
this.allServices = const [],
|
||||
this.currentService,
|
||||
this.allOperations = const [],
|
||||
this.currentOperation,
|
||||
this.errorMessage,
|
||||
this.query = '',
|
||||
this.dateRange,
|
||||
@@ -32,20 +32,20 @@ class ServicesState extends Equatable {
|
||||
this.isSavingDraft = false,
|
||||
});
|
||||
|
||||
ServicesState copyWith({
|
||||
ServicesStatus? status,
|
||||
List<ServiceModel>? allServices,
|
||||
ServiceModel? currentService,
|
||||
OperationsState copyWith({
|
||||
OperationsStatus? status,
|
||||
List<OperationModel>? allOperations,
|
||||
OperationModel? currentOperation,
|
||||
String? errorMessage,
|
||||
String? query,
|
||||
DateTimeRange? dateRange,
|
||||
bool? hasReachedMax,
|
||||
bool? isSavingDraft,
|
||||
}) {
|
||||
return ServicesState(
|
||||
return OperationsState(
|
||||
status: status ?? this.status,
|
||||
allServices: allServices ?? this.allServices,
|
||||
currentService: currentService ?? this.currentService,
|
||||
allOperations: allOperations ?? this.allOperations,
|
||||
currentOperation: currentOperation ?? this.currentOperation,
|
||||
errorMessage: errorMessage,
|
||||
query: query ?? this.query,
|
||||
dateRange: dateRange ?? this.dateRange,
|
||||
@@ -57,8 +57,8 @@ class ServicesState extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
status,
|
||||
allServices,
|
||||
currentService,
|
||||
allOperations,
|
||||
currentOperation,
|
||||
errorMessage,
|
||||
query,
|
||||
dateRange,
|
||||
@@ -1,232 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/operations/data/services_repository.dart';
|
||||
import 'package:flux/features/operations/models/service_file_model.dart';
|
||||
import 'package:flux/features/operations/models/service_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
part 'service_files_events.dart';
|
||||
part 'service_files_state.dart';
|
||||
|
||||
class ServiceFilesBloc extends Bloc<ServiceFilesEvent, ServiceFilesState> {
|
||||
final _repository = GetIt.I.get<ServicesRepository>();
|
||||
final String? serviceId;
|
||||
|
||||
ServiceFilesBloc({this.serviceId})
|
||||
: super(
|
||||
ServiceFilesState(
|
||||
status: ServiceFilesStatus.initial,
|
||||
serviceId: serviceId,
|
||||
),
|
||||
) {
|
||||
on<ServiceSavedEvent>(_onServiceSaved);
|
||||
on<LoadServiceFilesEvent>(_onLoadServiceFiles);
|
||||
on<AddServiceFilesEvent>(_onAddServiceFiles);
|
||||
on<UploadServiceFilesEvent>(_onUploadServiceFiles);
|
||||
on<UploadMultipleServiceFilesEvent>(_onUploadMultipleServiceFiles);
|
||||
on<DeleteServiceFilesEvent>(_onDeleteServiceFiles);
|
||||
on<ToggleServiceFileSelectionEvent>(_onToggleServiceFileSelection);
|
||||
// Se il BLoC nasce con un ID, accendiamo subito lo stream!
|
||||
if (serviceId != null) {
|
||||
add(LoadServiceFilesEvent(serviceId: serviceId));
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onServiceSaved(
|
||||
ServiceSavedEvent event,
|
||||
Emitter<ServiceFilesState> emit,
|
||||
) {
|
||||
// 1. Aggiorniamo l'ID nello stato
|
||||
// 2. PIALLIAMO i file locali: ormai sono partiti per Supabase!
|
||||
// Così la UI si pulisce all'istante e aspetta quelli remoti.
|
||||
emit(
|
||||
state.copyWith(
|
||||
serviceId: event.serviceId,
|
||||
localFiles: [], // <-- LA MAGIA ANTI-DUPLICATI
|
||||
),
|
||||
);
|
||||
|
||||
// Lanciamo il caricamento
|
||||
add(LoadServiceFilesEvent(serviceId: event.serviceId));
|
||||
}
|
||||
|
||||
FutureOr<void> _onLoadServiceFiles(
|
||||
LoadServiceFilesEvent event,
|
||||
Emitter<ServiceFilesState> emit,
|
||||
) async {
|
||||
// Usiamo l'ID dell'evento, e se non c'è usiamo quello dello stato
|
||||
final currentId = event.serviceId ?? state.serviceId;
|
||||
|
||||
if (currentId != null) {
|
||||
emit(state.copyWith(status: ServiceFilesStatus.loading));
|
||||
|
||||
await emit.forEach(
|
||||
_repository.getServiceFilesStream(
|
||||
currentId,
|
||||
), // <-- Usiamo l'ID corretto!
|
||||
onData: (data) => state.copyWith(
|
||||
status: ServiceFilesStatus.success,
|
||||
remoteFiles: data,
|
||||
),
|
||||
onError: (error, stackTrace) => state.copyWith(
|
||||
status: ServiceFilesStatus.failure,
|
||||
error: error.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _onAddServiceFiles(
|
||||
AddServiceFilesEvent event,
|
||||
Emitter<ServiceFilesState> emit,
|
||||
) async {
|
||||
final currentId = state.serviceId;
|
||||
// BIVIO 1: PRATICA NUOVA (Nessun ID)
|
||||
if (currentId == null) {
|
||||
// Mettiamo i file nel "parcheggio" locale dello State
|
||||
final newLocalFiles = event.files.map((file) {
|
||||
return ServiceFileModel(
|
||||
id: null,
|
||||
serviceId: serviceId ?? '',
|
||||
name: file.name.fileNameWithoutExtension(),
|
||||
extension: file.name.fileExtension(),
|
||||
storagePath: '',
|
||||
fileSize: file.size,
|
||||
localBytes: file.bytes,
|
||||
);
|
||||
}).toList();
|
||||
final List<ServiceFileModel> updatedLocalFiles = [
|
||||
...state.localFiles,
|
||||
...newLocalFiles,
|
||||
];
|
||||
emit(
|
||||
state.copyWith(
|
||||
localFiles: updatedLocalFiles,
|
||||
status: ServiceFilesStatus.success,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID)
|
||||
emit(state.copyWith(status: ServiceFilesStatus.uploading));
|
||||
try {
|
||||
// Logica identica a quella che abbiamo fatto per i clienti
|
||||
for (var file in event.files) {
|
||||
await _repository.uploadAndRegisterServiceFile(
|
||||
serviceId: serviceId!,
|
||||
pickedFile: file,
|
||||
);
|
||||
}
|
||||
emit(state.copyWith(status: ServiceFilesStatus.success));
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(status: ServiceFilesStatus.failure, error: e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onUploadServiceFiles(
|
||||
UploadServiceFilesEvent event,
|
||||
Emitter<ServiceFilesState> emit,
|
||||
) async {
|
||||
if (event.pickedFiles == null && event.photos == null) return;
|
||||
if (event.pickedFiles!.isEmpty && event.photos!.isEmpty) return;
|
||||
|
||||
// BIVIO 2: PRATICA ESISTENTE (Abbiamo l'ID
|
||||
emit(state.copyWith(status: ServiceFilesStatus.uploading));
|
||||
try {
|
||||
// Logica identica a quella che abbiamo fatto per i clienti
|
||||
if (event.pickedFiles != null && event.pickedFiles!.isNotEmpty) {
|
||||
for (var file in event.pickedFiles!) {
|
||||
await _repository.uploadAndRegisterServiceFile(
|
||||
serviceId: state.serviceId!,
|
||||
pickedFile: file,
|
||||
);
|
||||
}
|
||||
}
|
||||
emit(state.copyWith(status: ServiceFilesStatus.success));
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(status: ServiceFilesStatus.failure, error: e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onUploadMultipleServiceFiles(
|
||||
UploadMultipleServiceFilesEvent event,
|
||||
Emitter<ServiceFilesState> emit,
|
||||
) async {
|
||||
if (event.files.isEmpty) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ServiceFilesStatus.failure,
|
||||
error: "Nessun file selezionato",
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(status: ServiceFilesStatus.uploading, error: null));
|
||||
try {
|
||||
// 2. Creiamo una lista di "Promesse" (Futures) per il repository
|
||||
final List<Future<void>> uploadTasks = [];
|
||||
for (var file in event.files) {
|
||||
// Aggiungiamo il task alla lista, ma NON usiamo await qui dentro!
|
||||
uploadTasks.add(
|
||||
_repository.uploadAndRegisterServiceFile(
|
||||
serviceId: state.serviceId!,
|
||||
pickedFile: file,
|
||||
),
|
||||
);
|
||||
}
|
||||
// 3. ESECUZIONE PARALLELA!
|
||||
// Aspettiamo che tutti i file siano caricati contemporaneamente.
|
||||
await Future.wait(uploadTasks);
|
||||
// 4. GRAN FINALE: Tutto caricato, emettiamo il success!
|
||||
emit(state.copyWith(status: ServiceFilesStatus.success));
|
||||
} catch (e) {
|
||||
// Se anche un solo file fallisce, catturiamo l'errore
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: ServiceFilesStatus.failure,
|
||||
error: "Errore durante l'upload multiplo: $e",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onDeleteServiceFiles(
|
||||
DeleteServiceFilesEvent event,
|
||||
Emitter<ServiceFilesState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(status: ServiceFilesStatus.loading));
|
||||
try {
|
||||
await _repository.deleteServiceFiles(state.selectedFiles);
|
||||
emit(
|
||||
state.copyWith(status: ServiceFilesStatus.success, selectedFiles: []),
|
||||
);
|
||||
} catch (e) {
|
||||
emit(
|
||||
state.copyWith(status: ServiceFilesStatus.failure, error: e.toString()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FutureOr<void> _onToggleServiceFileSelection(
|
||||
ToggleServiceFileSelectionEvent event,
|
||||
Emitter<ServiceFilesState> emit,
|
||||
) {
|
||||
List<ServiceFileModel> selectedFiles = List.from(state.selectedFiles);
|
||||
if (selectedFiles.contains(event.file)) {
|
||||
selectedFiles.remove(event.file);
|
||||
} else {
|
||||
selectedFiles.add(event.file);
|
||||
}
|
||||
emit(state.copyWith(selectedFiles: selectedFiles));
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
part of 'service_files_bloc.dart';
|
||||
|
||||
abstract class ServiceFilesEvent extends Equatable {
|
||||
const ServiceFilesEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class ServiceSavedEvent extends ServiceFilesEvent {
|
||||
final String serviceId;
|
||||
const ServiceSavedEvent(this.serviceId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [serviceId];
|
||||
}
|
||||
|
||||
class LoadServiceFilesEvent extends ServiceFilesEvent {
|
||||
final String? serviceId;
|
||||
final ServiceModel? operation;
|
||||
const LoadServiceFilesEvent({this.serviceId, this.operation});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [serviceId, operation];
|
||||
}
|
||||
|
||||
class AddServiceFilesEvent extends ServiceFilesEvent {
|
||||
final List<PlatformFile> files;
|
||||
const AddServiceFilesEvent(this.files);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [files];
|
||||
}
|
||||
|
||||
class UploadServiceFilesEvent extends ServiceFilesEvent {
|
||||
final List<PlatformFile>? pickedFiles;
|
||||
final List<File>? photos;
|
||||
const UploadServiceFilesEvent({this.pickedFiles, this.photos});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [pickedFiles, photos];
|
||||
}
|
||||
|
||||
class UploadMultipleServiceFilesEvent extends ServiceFilesEvent {
|
||||
final List<PlatformFile> files;
|
||||
const UploadMultipleServiceFilesEvent(this.files);
|
||||
@override
|
||||
List<Object?> get props => [files];
|
||||
}
|
||||
|
||||
class DeleteServiceFilesEvent extends ServiceFilesEvent {}
|
||||
|
||||
class ToggleServiceFileSelectionEvent extends ServiceFilesEvent {
|
||||
final ServiceFileModel file;
|
||||
const ToggleServiceFileSelectionEvent(this.file);
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
part of 'service_files_bloc.dart';
|
||||
|
||||
enum ServiceFilesStatus { initial, loading, uploading, success, failure }
|
||||
|
||||
class ServiceFilesState extends Equatable {
|
||||
const ServiceFilesState({
|
||||
this.serviceId,
|
||||
required this.status,
|
||||
this.error,
|
||||
this.localFiles = const [],
|
||||
this.remoteFiles = const [],
|
||||
this.selectedFiles = const [],
|
||||
});
|
||||
|
||||
final String? serviceId;
|
||||
final ServiceFilesStatus status;
|
||||
final String? error;
|
||||
final List<ServiceFileModel> localFiles;
|
||||
final List<ServiceFileModel> remoteFiles;
|
||||
|
||||
final List<ServiceFileModel> selectedFiles;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
serviceId,
|
||||
status,
|
||||
error,
|
||||
localFiles,
|
||||
remoteFiles,
|
||||
selectedFiles,
|
||||
];
|
||||
|
||||
List<ServiceFileModel> get allFiles => [...remoteFiles, ...localFiles];
|
||||
|
||||
ServiceFilesState copyWith({
|
||||
String? serviceId,
|
||||
ServiceFilesStatus? status,
|
||||
String? error,
|
||||
List<ServiceFileModel>? localFiles,
|
||||
List<ServiceFileModel>? remoteFiles,
|
||||
List<ServiceFileModel>? selectedFiles,
|
||||
}) {
|
||||
return ServiceFilesState(
|
||||
serviceId: serviceId ?? this.serviceId,
|
||||
status: status ?? this.status,
|
||||
error: error,
|
||||
localFiles: localFiles ?? this.localFiles,
|
||||
remoteFiles: remoteFiles ?? this.remoteFiles,
|
||||
selectedFiles: selectedFiles ?? this.selectedFiles,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,40 +4,40 @@ import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/customers/data/customer_repository.dart';
|
||||
import 'package:flux/features/customers/models/customer_file_model.dart';
|
||||
import 'package:flux/features/operations/models/service_file_model.dart';
|
||||
import 'package:flux/features/operations/models/operation_file_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import '../models/service_model.dart';
|
||||
import '../models/operation_model.dart';
|
||||
|
||||
class ServicesRepository {
|
||||
class OperationsRepository {
|
||||
final _supabase = Supabase.instance.client;
|
||||
final companyId = GetIt.I.get<SessionCubit>().state.company!.id;
|
||||
final CustomerRepository _customerRepository = GetIt.I<CustomerRepository>();
|
||||
|
||||
// --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO ---
|
||||
Future<ServiceModel> fetchServiceById(String id) async {
|
||||
Future<OperationModel> fetchOperationById(String id) async {
|
||||
try {
|
||||
final response = await _supabase
|
||||
.from('operation')
|
||||
.select('''
|
||||
*,
|
||||
customer(nome),
|
||||
energy_service(*),
|
||||
fin_service(*),
|
||||
entertainment_service(*),
|
||||
service_file(*)
|
||||
energy_operation(*),
|
||||
fin_operation(*),
|
||||
entertainment_operation(*),
|
||||
operation_file(*)
|
||||
''')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
|
||||
return ServiceModel.fromMap(response);
|
||||
return OperationModel.fromMap(response);
|
||||
} catch (e) {
|
||||
throw Exception('Errore nel caricamento del servizio: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// --- RECUPERO PAGINATO CON FILTRI E JOIN ---
|
||||
Future<List<ServiceModel>> fetchServices({
|
||||
Future<List<OperationModel>> fetchOperations({
|
||||
required String companyId,
|
||||
required int offset,
|
||||
int limit = 50,
|
||||
@@ -51,10 +51,10 @@ class ServicesRepository {
|
||||
.select('''
|
||||
*,
|
||||
customer(nome),
|
||||
energy_service(*),
|
||||
fin_service(*),
|
||||
entertainment_service(*),
|
||||
service_file(*)
|
||||
energy_operation(*),
|
||||
fin_operation(*),
|
||||
entertainment_operation(*),
|
||||
operation_file(*)
|
||||
''')
|
||||
.eq('company_id', companyId);
|
||||
|
||||
@@ -77,14 +77,14 @@ class ServicesRepository {
|
||||
.range(offset, offset + limit - 1);
|
||||
|
||||
return (response as List)
|
||||
.map((map) => ServiceModel.fromMap(map))
|
||||
.map((map) => OperationModel.fromMap(map))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
throw Exception('Errore nel caricamento servizi: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Stream<List<ServiceModel>> getLastStoreServicesStream({
|
||||
Stream<List<OperationModel>> getLastStoreOperationsStream({
|
||||
required String storeId,
|
||||
required int limit,
|
||||
}) {
|
||||
@@ -96,32 +96,32 @@ class ServicesRepository {
|
||||
.limit(limit)
|
||||
.map(
|
||||
(listOfMaps) =>
|
||||
listOfMaps.map((map) => ServiceModel.fromMap(map)).toList(),
|
||||
listOfMaps.map((map) => OperationModel.fromMap(map)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
|
||||
Future<ServiceModel> saveFullService(ServiceModel operation) async {
|
||||
Future<OperationModel> saveFullOperation(OperationModel operation) async {
|
||||
try {
|
||||
// 1. Upsert del record principale
|
||||
final serviceData = await _supabase
|
||||
final operationData = await _supabase
|
||||
.from('operation')
|
||||
.upsert(operation.toMap())
|
||||
.select()
|
||||
.single();
|
||||
|
||||
final String newId = serviceData['id'];
|
||||
final String newId = operationData['id'];
|
||||
|
||||
// 2. MODIFICA: Pulizia atomica dei figli
|
||||
// Se stiamo modificando (id != null), resettiamo le tabelle collegate
|
||||
if (operation.id != null) {
|
||||
await Future.wait([
|
||||
_supabase.from('energy_service').delete().eq('service_id', newId),
|
||||
_supabase.from('fin_service').delete().eq('service_id', newId),
|
||||
_supabase.from('energy_operation').delete().eq('operation_id', newId),
|
||||
_supabase.from('fin_operation').delete().eq('operation_id', newId),
|
||||
_supabase
|
||||
.from('entertainment_service')
|
||||
.from('entertainment_operation')
|
||||
.delete()
|
||||
.eq('service_id', newId),
|
||||
.eq('operation_id', newId),
|
||||
// Aggiungi qui eventuali altre tabelle pivot o file
|
||||
]);
|
||||
}
|
||||
@@ -129,37 +129,37 @@ class ServicesRepository {
|
||||
// 3. Inserimento dei moduli in parallelo per velocità
|
||||
final List<Future> insertTasks = [];
|
||||
|
||||
if (operation.energyServices.isNotEmpty) {
|
||||
if (operation.energyOperations.isNotEmpty) {
|
||||
insertTasks.add(
|
||||
_supabase
|
||||
.from('energy_service')
|
||||
.from('energy_operation')
|
||||
.insert(
|
||||
operation.energyServices
|
||||
.map((item) => item.copyWith(serviceId: newId).toMap())
|
||||
operation.energyOperations
|
||||
.map((item) => item.copyWith(operationId: newId).toMap())
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (operation.finServices.isNotEmpty) {
|
||||
if (operation.finOperations.isNotEmpty) {
|
||||
insertTasks.add(
|
||||
_supabase
|
||||
.from('fin_service')
|
||||
.from('fin_operation')
|
||||
.insert(
|
||||
operation.finServices
|
||||
.map((item) => item.copyWith(serviceId: newId).toMap())
|
||||
operation.finOperations
|
||||
.map((item) => item.copyWith(operationId: newId).toMap())
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (operation.entertainmentServices.isNotEmpty) {
|
||||
if (operation.entertainmentOperations.isNotEmpty) {
|
||||
insertTasks.add(
|
||||
_supabase
|
||||
.from('entertainment_service')
|
||||
.from('entertainment_operation')
|
||||
.insert(
|
||||
operation.entertainmentServices
|
||||
.map((item) => item.copyWith(serviceId: newId).toMap())
|
||||
operation.entertainmentOperations
|
||||
.map((item) => item.copyWith(operationId: newId).toMap())
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
@@ -186,7 +186,7 @@ class ServicesRepository {
|
||||
: 'image/${file.extension}';
|
||||
|
||||
final fileToSave = file.copyWith(
|
||||
serviceId: newId,
|
||||
operationId: newId,
|
||||
storagePath: storagePath,
|
||||
);
|
||||
|
||||
@@ -202,7 +202,7 @@ class ServicesRepository {
|
||||
);
|
||||
|
||||
// B. Inserimento riga nel DB relazionale
|
||||
await _supabase.from('service_file').insert(fileToSave.toMap());
|
||||
await _supabase.from('operation_file').insert(fileToSave.toMap());
|
||||
}
|
||||
|
||||
uploadTasks.add(uploadAndLink());
|
||||
@@ -214,20 +214,20 @@ class ServicesRepository {
|
||||
|
||||
// 5. GRAN FINALE: RECUPERO DEL MODELLO COMPLETO E AGGIORNATO
|
||||
// Interroghiamo Supabase per farci restituire la pratica con TUTTI gli ID generati
|
||||
// (inclusi quelli della tabella service_file appena inseriti)
|
||||
final updatedServiceData = await _supabase
|
||||
// (inclusi quelli della tabella operation_file appena inseriti)
|
||||
final updatedOperationData = await _supabase
|
||||
.from('operation')
|
||||
.select('''
|
||||
*,
|
||||
energy_service(*),
|
||||
fin_service(*),
|
||||
entertainment_service(*),
|
||||
service_file(*)
|
||||
energy_operation(*),
|
||||
fin_operation(*),
|
||||
entertainment_operation(*),
|
||||
operation_file(*)
|
||||
''')
|
||||
.eq('id', newId)
|
||||
.single();
|
||||
|
||||
return ServiceModel.fromMap(updatedServiceData);
|
||||
return OperationModel.fromMap(updatedOperationData);
|
||||
} catch (e) {
|
||||
// Qui potresti aggiungere una logica di "rollback manuale" se necessario
|
||||
throw Exception('Errore durante il salvataggio corazzato: $e');
|
||||
@@ -235,7 +235,7 @@ class ServicesRepository {
|
||||
}
|
||||
|
||||
// --- ELIMINAZIONE ---
|
||||
Future<void> deleteService(String id) async {
|
||||
Future<void> deleteOperation(String id) async {
|
||||
try {
|
||||
await _supabase.from('operation').delete().eq('id', id);
|
||||
} catch (e) {
|
||||
@@ -249,7 +249,7 @@ class ServicesRepository {
|
||||
// Cerchiamo i tipi più frequenti associati ai servizi di questa company
|
||||
// Nota: dobbiamo passare attraverso la tabella 'operation' per filtrare per company_id
|
||||
final response = await _supabase
|
||||
.from('entertainment_service')
|
||||
.from('entertainment_operation')
|
||||
.select('type, operation!inner(store!inner(company_id))')
|
||||
.eq('operation.store.company_id', companyId)
|
||||
.limit(100); // Prendiamo un campione
|
||||
@@ -276,20 +276,20 @@ class ServicesRepository {
|
||||
}
|
||||
|
||||
/// Ascolta in tempo reale i file caricati per una pratica
|
||||
Stream<List<ServiceFileModel>> getServiceFilesStream(String serviceId) {
|
||||
Stream<List<OperationFileModel>> getOperationFilesStream(String operationId) {
|
||||
return _supabase
|
||||
.from('service_file')
|
||||
.from('operation_file')
|
||||
.stream(primaryKey: ['id'])
|
||||
.eq('service_id', serviceId)
|
||||
.eq('operation_id', operationId)
|
||||
.order('created_at', ascending: false)
|
||||
.map(
|
||||
(listOfMaps) =>
|
||||
listOfMaps.map((map) => ServiceFileModel.fromMap(map)).toList(),
|
||||
listOfMaps.map((map) => OperationFileModel.fromMap(map)).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<ServiceFileModel> uploadAndRegisterServiceFile({
|
||||
required String serviceId,
|
||||
Future<OperationFileModel> uploadAndRegisterOperationFile({
|
||||
required String operationId,
|
||||
required PlatformFile pickedFile,
|
||||
}) async {
|
||||
final cleanFileName = pickedFile.name.replaceAll(
|
||||
@@ -297,10 +297,10 @@ class ServicesRepository {
|
||||
'_',
|
||||
);
|
||||
final storagePath =
|
||||
'$companyId/operations/$serviceId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
|
||||
'$companyId/operations/$operationId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
|
||||
final int fileSize = pickedFile.size;
|
||||
final fileToSave = ServiceFileModel(
|
||||
serviceId: serviceId,
|
||||
final fileToSave = OperationFileModel(
|
||||
operationId: operationId,
|
||||
name: cleanFileName.fileNameWithoutExtension(),
|
||||
extension: cleanFileName.fileExtension(),
|
||||
storagePath: storagePath,
|
||||
@@ -327,19 +327,19 @@ class ServicesRepository {
|
||||
}
|
||||
|
||||
final response = await _supabase
|
||||
.from('service_file')
|
||||
.from('operation_file')
|
||||
.insert(fileToSave.toMap())
|
||||
.select()
|
||||
.single();
|
||||
|
||||
return ServiceFileModel.fromMap(response);
|
||||
return OperationFileModel.fromMap(response);
|
||||
} catch (e) {
|
||||
throw 'Errore durante l\'upload: $e';
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> copyFileToCustomer({
|
||||
required ServiceFileModel file,
|
||||
required OperationFileModel file,
|
||||
required String customerId,
|
||||
}) async {
|
||||
CustomerFileModel fileToCopy = CustomerFileModel(
|
||||
@@ -352,14 +352,17 @@ class ServicesRepository {
|
||||
await _customerRepository.saveFileReference(fileToCopy);
|
||||
}
|
||||
|
||||
Future<void> deleteServiceFiles(List<ServiceFileModel> files) async {
|
||||
Future<void> deleteOperationFiles(List<OperationFileModel> files) async {
|
||||
if (files.isEmpty) return;
|
||||
// 1. Prepariamo le liste di ID e di Percorsi
|
||||
final List<String> idsToDelete = files.map((f) => f.id!).toList();
|
||||
final List<String> storagePaths = files.map((f) => f.storagePath).toList();
|
||||
|
||||
try {
|
||||
await _supabase.from('service_file').delete().inFilter('id', idsToDelete);
|
||||
await _supabase
|
||||
.from('operation_file')
|
||||
.delete()
|
||||
.inFilter('id', idsToDelete);
|
||||
|
||||
await _supabase.storage.from('documents').remove(storagePaths);
|
||||
|
||||
@@ -2,38 +2,38 @@ import 'package:equatable/equatable.dart';
|
||||
|
||||
enum EnergyType { luce, gas } // Mappa il tuo public.energy_type
|
||||
|
||||
class EnergyServiceModel extends Equatable {
|
||||
class EnergyOperationModel extends Equatable {
|
||||
final String? id;
|
||||
final DateTime? createdAt;
|
||||
final EnergyType type;
|
||||
final DateTime expiration;
|
||||
final String providerId;
|
||||
final String? serviceId;
|
||||
final String? operationId;
|
||||
|
||||
const EnergyServiceModel({
|
||||
const EnergyOperationModel({
|
||||
this.id,
|
||||
this.createdAt,
|
||||
required this.type,
|
||||
required this.expiration,
|
||||
required this.providerId,
|
||||
this.serviceId,
|
||||
this.operationId,
|
||||
});
|
||||
|
||||
EnergyServiceModel copyWith({
|
||||
EnergyOperationModel copyWith({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
EnergyType? type,
|
||||
DateTime? expiration,
|
||||
String? providerId,
|
||||
String? serviceId,
|
||||
String? operationId,
|
||||
}) {
|
||||
return EnergyServiceModel(
|
||||
return EnergyOperationModel(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
type: type ?? this.type,
|
||||
expiration: expiration ?? this.expiration,
|
||||
providerId: providerId ?? this.providerId,
|
||||
serviceId: serviceId ?? this.serviceId,
|
||||
operationId: operationId ?? this.operationId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,11 +44,11 @@ class EnergyServiceModel extends Equatable {
|
||||
type,
|
||||
expiration,
|
||||
providerId,
|
||||
serviceId,
|
||||
operationId,
|
||||
];
|
||||
|
||||
factory EnergyServiceModel.fromMap(Map<String, dynamic> map) {
|
||||
return EnergyServiceModel(
|
||||
factory EnergyOperationModel.fromMap(Map<String, dynamic> map) {
|
||||
return EnergyOperationModel(
|
||||
id: map['id'],
|
||||
createdAt: map['created_at'] != null
|
||||
? DateTime.parse(map['created_at'])
|
||||
@@ -56,7 +56,7 @@ class EnergyServiceModel extends Equatable {
|
||||
type: map['type'] == 'gas' ? EnergyType.gas : EnergyType.luce,
|
||||
expiration: DateTime.parse(map['expiration']),
|
||||
providerId: map['provider_id'],
|
||||
serviceId: map['service_id'],
|
||||
operationId: map['operation_id'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ class EnergyServiceModel extends Equatable {
|
||||
'type': type.name, // .name trasforma l'enum in 'luce' o 'gas'
|
||||
'expiration': expiration.toIso8601String(),
|
||||
'provider_id': providerId,
|
||||
'service_id': serviceId,
|
||||
'operation_id': operationId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,40 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class EntertainmentServiceModel extends Equatable {
|
||||
class EntertainmentOperationModel extends Equatable {
|
||||
final String? id;
|
||||
final DateTime? createdAt;
|
||||
final String type; // es. Sky, DAZN, ecc.
|
||||
final bool constrained; // Vincolato?
|
||||
final DateTime constrainExpiration;
|
||||
final String? serviceId;
|
||||
final String? operationId;
|
||||
final String? providerId;
|
||||
|
||||
const EntertainmentServiceModel({
|
||||
const EntertainmentOperationModel({
|
||||
this.id,
|
||||
this.createdAt,
|
||||
required this.type,
|
||||
required this.constrained,
|
||||
required this.constrainExpiration,
|
||||
this.serviceId,
|
||||
this.operationId,
|
||||
this.providerId,
|
||||
});
|
||||
|
||||
EntertainmentServiceModel copyWith({
|
||||
EntertainmentOperationModel copyWith({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
String? type,
|
||||
bool? constrained,
|
||||
DateTime? constrainExpiration,
|
||||
String? serviceId,
|
||||
String? operationId,
|
||||
String? providerId,
|
||||
}) {
|
||||
return EntertainmentServiceModel(
|
||||
return EntertainmentOperationModel(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
type: type ?? this.type,
|
||||
constrained: constrained ?? this.constrained,
|
||||
constrainExpiration: constrainExpiration ?? this.constrainExpiration,
|
||||
serviceId: serviceId ?? this.serviceId,
|
||||
operationId: operationId ?? this.operationId,
|
||||
providerId: providerId ?? this.providerId,
|
||||
);
|
||||
}
|
||||
@@ -46,12 +46,12 @@ class EntertainmentServiceModel extends Equatable {
|
||||
type,
|
||||
constrained,
|
||||
constrainExpiration,
|
||||
serviceId,
|
||||
operationId,
|
||||
providerId,
|
||||
];
|
||||
|
||||
factory EntertainmentServiceModel.fromMap(Map<String, dynamic> map) {
|
||||
return EntertainmentServiceModel(
|
||||
factory EntertainmentOperationModel.fromMap(Map<String, dynamic> map) {
|
||||
return EntertainmentOperationModel(
|
||||
id: map['id'],
|
||||
createdAt: map['created_at'] != null
|
||||
? DateTime.parse(map['created_at'])
|
||||
@@ -59,7 +59,7 @@ class EntertainmentServiceModel extends Equatable {
|
||||
type: map['type'],
|
||||
constrained: map['constrained'] ?? false,
|
||||
constrainExpiration: DateTime.parse(map['constrain_expiration']),
|
||||
serviceId: map['service_id'],
|
||||
operationId: map['operation_id'],
|
||||
providerId: map['provider_id'],
|
||||
);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ class EntertainmentServiceModel extends Equatable {
|
||||
'type': type,
|
||||
'constrained': constrained,
|
||||
'constrain_expiration': constrainExpiration.toIso8601String(),
|
||||
'service_id': serviceId,
|
||||
'operation_id': operationId,
|
||||
'provider_id': providerId,
|
||||
};
|
||||
}
|
||||
@@ -1,51 +1,51 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class FinServiceModel extends Equatable {
|
||||
class FinOperationModel extends Equatable {
|
||||
final String? id;
|
||||
final DateTime? createdAt;
|
||||
final DateTime expiration;
|
||||
final String? serviceId;
|
||||
final String? operationId;
|
||||
final String? modelId; // FK verso model (es. iPhone, Samsung, ecc.)
|
||||
final String? providerId;
|
||||
|
||||
const FinServiceModel({
|
||||
const FinOperationModel({
|
||||
this.id,
|
||||
this.createdAt,
|
||||
required this.expiration,
|
||||
this.serviceId,
|
||||
this.operationId,
|
||||
this.modelId,
|
||||
this.providerId,
|
||||
});
|
||||
|
||||
FinServiceModel copyWith({
|
||||
FinOperationModel copyWith({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
DateTime? expiration,
|
||||
String? serviceId,
|
||||
String? operationId,
|
||||
String? modelId,
|
||||
String? providerId,
|
||||
}) {
|
||||
return FinServiceModel(
|
||||
return FinOperationModel(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
expiration: expiration ?? this.expiration,
|
||||
serviceId: serviceId ?? this.serviceId,
|
||||
operationId: operationId ?? this.operationId,
|
||||
modelId: modelId ?? this.modelId,
|
||||
providerId: providerId ?? this.providerId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, createdAt, expiration, serviceId, modelId];
|
||||
List<Object?> get props => [id, createdAt, expiration, operationId, modelId];
|
||||
|
||||
factory FinServiceModel.fromMap(Map<String, dynamic> map) {
|
||||
return FinServiceModel(
|
||||
factory FinOperationModel.fromMap(Map<String, dynamic> map) {
|
||||
return FinOperationModel(
|
||||
id: map['id'],
|
||||
createdAt: map['created_at'] != null
|
||||
? DateTime.parse(map['created_at'])
|
||||
: null,
|
||||
expiration: DateTime.parse(map['expiration']),
|
||||
serviceId: map['service_id'],
|
||||
operationId: map['operation_id'],
|
||||
modelId: map['model_id'],
|
||||
providerId: map['provider_id'],
|
||||
);
|
||||
@@ -55,7 +55,7 @@ class FinServiceModel extends Equatable {
|
||||
return {
|
||||
if (id != null) 'id': id,
|
||||
'expiration': expiration.toIso8601String(),
|
||||
'service_id': serviceId,
|
||||
'operation_id': operationId,
|
||||
'model_id': modelId,
|
||||
'provider_id': providerId,
|
||||
};
|
||||
@@ -2,23 +2,23 @@ import 'dart:typed_data';
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class ServiceFileModel extends Equatable {
|
||||
class OperationFileModel extends Equatable {
|
||||
final String? id;
|
||||
final DateTime? createdAt;
|
||||
final String name;
|
||||
final String extension;
|
||||
final String storagePath;
|
||||
final String serviceId;
|
||||
final String operationId;
|
||||
final int fileSize;
|
||||
final Uint8List? localBytes;
|
||||
|
||||
const ServiceFileModel({
|
||||
const OperationFileModel({
|
||||
this.id,
|
||||
this.createdAt,
|
||||
required this.name,
|
||||
required this.extension,
|
||||
required this.storagePath,
|
||||
required this.serviceId,
|
||||
required this.operationId,
|
||||
required this.fileSize,
|
||||
this.localBytes,
|
||||
});
|
||||
@@ -37,30 +37,30 @@ class ServiceFileModel extends Equatable {
|
||||
|
||||
bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf';
|
||||
|
||||
ServiceFileModel copyWith({
|
||||
OperationFileModel copyWith({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
String? name,
|
||||
String? extension,
|
||||
String? storagePath,
|
||||
String? serviceId,
|
||||
String? operationId,
|
||||
int? fileSize,
|
||||
Uint8List? localBytes,
|
||||
}) {
|
||||
return ServiceFileModel(
|
||||
return OperationFileModel(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
name: name ?? this.name,
|
||||
extension: extension ?? this.extension,
|
||||
storagePath: storagePath ?? this.storagePath,
|
||||
serviceId: serviceId ?? this.serviceId,
|
||||
operationId: operationId ?? this.operationId,
|
||||
fileSize: fileSize ?? this.fileSize,
|
||||
localBytes: localBytes ?? this.localBytes,
|
||||
);
|
||||
}
|
||||
|
||||
factory ServiceFileModel.fromMap(Map<String, dynamic> map) {
|
||||
return ServiceFileModel(
|
||||
factory OperationFileModel.fromMap(Map<String, dynamic> map) {
|
||||
return OperationFileModel(
|
||||
id: map['id'] as String,
|
||||
createdAt: map['created_at'] != null
|
||||
? DateTime.parse(map['created_at'])
|
||||
@@ -68,7 +68,7 @@ class ServiceFileModel extends Equatable {
|
||||
name: map['name'] ?? '',
|
||||
extension: map['extension'] ?? '',
|
||||
storagePath: map['storage_path'] ?? '',
|
||||
serviceId: map['service_id']?.toString() ?? '',
|
||||
operationId: map['operation_id']?.toString() ?? '',
|
||||
fileSize: map['file_size'] is int
|
||||
? map['file_size']
|
||||
: int.tryParse(map['file_size']?.toString() ?? '0') ?? 0,
|
||||
@@ -81,7 +81,7 @@ class ServiceFileModel extends Equatable {
|
||||
'name': name,
|
||||
'extension': extension,
|
||||
'storage_path': storagePath,
|
||||
'service_id': serviceId,
|
||||
'operation_id': operationId,
|
||||
'file_size': fileSize,
|
||||
};
|
||||
}
|
||||
@@ -93,7 +93,7 @@ class ServiceFileModel extends Equatable {
|
||||
name,
|
||||
extension,
|
||||
storagePath,
|
||||
serviceId,
|
||||
operationId,
|
||||
fileSize,
|
||||
localBytes,
|
||||
];
|
||||
@@ -1,11 +1,11 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flux/core/utils/extensions.dart';
|
||||
import 'package:flux/features/operations/models/energy_service_model.dart';
|
||||
import 'package:flux/features/operations/models/entertainment_service_model.dart';
|
||||
import 'package:flux/features/operations/models/fin_service_model.dart';
|
||||
import 'package:flux/features/operations/models/service_file_model.dart'; // <-- Aggiunto Import
|
||||
import 'package:flux/features/operations/models/energy_operation_model.dart';
|
||||
import 'package:flux/features/operations/models/entertainment_operation_model.dart';
|
||||
import 'package:flux/features/operations/models/fin_operation_model.dart';
|
||||
import 'package:flux/features/operations/models/operation_file_model.dart'; // <-- Aggiunto Import
|
||||
|
||||
class ServiceModel extends Equatable {
|
||||
class OperationModel extends Equatable {
|
||||
final String? id;
|
||||
final DateTime? createdAt;
|
||||
final String storeId;
|
||||
@@ -26,14 +26,14 @@ class ServiceModel extends Equatable {
|
||||
final int telepass;
|
||||
|
||||
// Moduli (Liste)
|
||||
final List<EnergyServiceModel> energyServices;
|
||||
final List<FinServiceModel> finServices;
|
||||
final List<EntertainmentServiceModel> entertainmentServices;
|
||||
final List<EnergyOperationModel> energyOperations;
|
||||
final List<FinOperationModel> finOperations;
|
||||
final List<EntertainmentOperationModel> entertainmentOperations;
|
||||
|
||||
// ALLEGATI (Aggiunto)
|
||||
final List<ServiceFileModel> files;
|
||||
final List<OperationFileModel> files;
|
||||
|
||||
const ServiceModel({
|
||||
const OperationModel({
|
||||
this.id,
|
||||
this.createdAt,
|
||||
required this.storeId,
|
||||
@@ -48,15 +48,15 @@ class ServiceModel extends Equatable {
|
||||
this.nip = 0,
|
||||
this.unica = 0,
|
||||
this.telepass = 0,
|
||||
this.energyServices = const [],
|
||||
this.finServices = const [],
|
||||
this.entertainmentServices = const [],
|
||||
this.energyOperations = const [],
|
||||
this.finOperations = const [],
|
||||
this.entertainmentOperations = const [],
|
||||
this.files = const [], // <-- Aggiunto default vuoto
|
||||
this.customerDisplayName,
|
||||
required this.companyId,
|
||||
});
|
||||
|
||||
ServiceModel copyWith({
|
||||
OperationModel copyWith({
|
||||
String? id,
|
||||
DateTime? createdAt,
|
||||
String? storeId,
|
||||
@@ -71,14 +71,14 @@ class ServiceModel extends Equatable {
|
||||
int? nip,
|
||||
int? unica,
|
||||
int? telepass,
|
||||
List<EnergyServiceModel>? energyServices,
|
||||
List<FinServiceModel>? finServices,
|
||||
List<EntertainmentServiceModel>? entertainmentServices,
|
||||
List<ServiceFileModel>? files, // <-- Aggiunto
|
||||
List<EnergyOperationModel>? energyOperations,
|
||||
List<FinOperationModel>? finOperations,
|
||||
List<EntertainmentOperationModel>? entertainmentOperations,
|
||||
List<OperationFileModel>? files, // <-- Aggiunto
|
||||
String? customerDisplayName,
|
||||
String? companyId,
|
||||
}) {
|
||||
return ServiceModel(
|
||||
return OperationModel(
|
||||
id: id ?? this.id,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
storeId: storeId ?? this.storeId,
|
||||
@@ -93,10 +93,10 @@ class ServiceModel extends Equatable {
|
||||
nip: nip ?? this.nip,
|
||||
unica: unica ?? this.unica,
|
||||
telepass: telepass ?? this.telepass,
|
||||
energyServices: energyServices ?? this.energyServices,
|
||||
finServices: finServices ?? this.finServices,
|
||||
entertainmentServices:
|
||||
entertainmentServices ?? this.entertainmentServices,
|
||||
energyOperations: energyOperations ?? this.energyOperations,
|
||||
finOperations: finOperations ?? this.finOperations,
|
||||
entertainmentOperations:
|
||||
entertainmentOperations ?? this.entertainmentOperations,
|
||||
files: files ?? this.files, // <-- Aggiunto
|
||||
customerDisplayName: customerDisplayName ?? this.customerDisplayName,
|
||||
companyId: companyId ?? this.companyId,
|
||||
@@ -119,16 +119,16 @@ class ServiceModel extends Equatable {
|
||||
nip,
|
||||
unica,
|
||||
telepass,
|
||||
energyServices,
|
||||
finServices,
|
||||
entertainmentServices,
|
||||
energyOperations,
|
||||
finOperations,
|
||||
entertainmentOperations,
|
||||
files, // <-- Aggiunto
|
||||
customerDisplayName,
|
||||
companyId,
|
||||
];
|
||||
|
||||
factory ServiceModel.fromMap(Map<String, dynamic> map) {
|
||||
return ServiceModel(
|
||||
factory OperationModel.fromMap(Map<String, dynamic> map) {
|
||||
return OperationModel(
|
||||
id: map['id'].toString(),
|
||||
createdAt: map['created_at'] != null
|
||||
? DateTime.parse(map['created_at'])
|
||||
@@ -147,26 +147,26 @@ class ServiceModel extends Equatable {
|
||||
telepass: map['telepass'] ?? 0,
|
||||
|
||||
// Estrazione sicura liste collegate
|
||||
energyServices:
|
||||
(map['energy_service'] as List?)
|
||||
?.map((x) => EnergyServiceModel.fromMap(x))
|
||||
energyOperations:
|
||||
(map['energy_operation'] as List?)
|
||||
?.map((x) => EnergyOperationModel.fromMap(x))
|
||||
.toList() ??
|
||||
const [],
|
||||
finServices:
|
||||
(map['fin_service'] as List?)
|
||||
?.map((x) => FinServiceModel.fromMap(x))
|
||||
finOperations:
|
||||
(map['fin_operation'] as List?)
|
||||
?.map((x) => FinOperationModel.fromMap(x))
|
||||
.toList() ??
|
||||
const [],
|
||||
entertainmentServices:
|
||||
(map['entertainment_service'] as List?)
|
||||
?.map((x) => EntertainmentServiceModel.fromMap(x))
|
||||
entertainmentOperations:
|
||||
(map['entertainment_operation'] as List?)
|
||||
?.map((x) => EntertainmentOperationModel.fromMap(x))
|
||||
.toList() ??
|
||||
const [],
|
||||
|
||||
// I FILE! (Assicurati che la foreign key su Supabase usi esattamente questo nome)
|
||||
files:
|
||||
(map['service_file'] as List?)
|
||||
?.map((x) => ServiceFileModel.fromMap(x))
|
||||
(map['operation_file'] as List?)
|
||||
?.map((x) => OperationFileModel.fromMap(x))
|
||||
.toList() ??
|
||||
const [],
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ServiceActionCard extends StatelessWidget {
|
||||
class OperationActionCard extends StatelessWidget {
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final VoidCallback onTap;
|
||||
final Color color;
|
||||
final int count;
|
||||
const ServiceActionCard({
|
||||
const OperationActionCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
@@ -5,9 +5,9 @@ import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/core/widgets/image_viewer_widget.dart';
|
||||
import 'package:flux/core/widgets/pdf_viewer_widget.dart';
|
||||
import 'package:flux/core/widgets/qr_upload_dialog.dart';
|
||||
import 'package:flux/features/operations/blocs/service_files_bloc.dart';
|
||||
import 'package:flux/features/operations/blocs/operation_files_bloc.dart';
|
||||
import 'package:flux/features/operations/blocs/operations_cubit.dart';
|
||||
import 'package:flux/features/operations/models/service_file_model.dart';
|
||||
import 'package:flux/features/operations/models/operation_file_model.dart';
|
||||
|
||||
class AttachmentsSection extends StatelessWidget {
|
||||
const AttachmentsSection({super.key});
|
||||
@@ -22,27 +22,29 @@ class AttachmentsSection extends StatelessWidget {
|
||||
);
|
||||
|
||||
if (result != null && context.mounted) {
|
||||
context.read<ServiceFilesBloc>().add(AddServiceFilesEvent(result.files));
|
||||
context.read<OperationFilesBloc>().add(
|
||||
AddOperationFilesEvent(result.files),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ServiceFilesBloc serviceFilesBloc = BlocProvider.of<ServiceFilesBloc>(
|
||||
OperationFilesBloc operationFilesBloc = BlocProvider.of<OperationFilesBloc>(
|
||||
context,
|
||||
);
|
||||
|
||||
return BlocListener<ServicesCubit, ServicesState>(
|
||||
return BlocListener<OperationsCubit, OperationsState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.currentService?.id == null &&
|
||||
current.currentService?.id != null,
|
||||
previous.currentOperation?.id == null &&
|
||||
current.currentOperation?.id != null,
|
||||
listener: (context, state) {
|
||||
// FIGASSA! La pratica è stata salvata e ora ha un ID.
|
||||
// Diciamo al Bloc dei file di agganciarsi al database.
|
||||
final newId = state.currentService!.id!;
|
||||
context.read<ServiceFilesBloc>().add(ServiceSavedEvent(newId));
|
||||
final newId = state.currentOperation!.id!;
|
||||
context.read<OperationFilesBloc>().add(OperationsavedEvent(newId));
|
||||
},
|
||||
child: BlocBuilder<ServiceFilesBloc, ServiceFilesState>(
|
||||
child: BlocBuilder<OperationFilesBloc, OperationFilesState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -125,8 +127,8 @@ class AttachmentsSection extends StatelessWidget {
|
||||
final isSelected = state.selectedFiles.contains(file);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => serviceFilesBloc.add(
|
||||
ToggleServiceFileSelectionEvent(file),
|
||||
onTap: () => operationFilesBloc.add(
|
||||
ToggleOperationFileSelectionEvent(file),
|
||||
),
|
||||
onDoubleTap: () => _handleDoubleClick(context, file),
|
||||
child: Card(
|
||||
@@ -216,7 +218,7 @@ class AttachmentsSection extends StatelessWidget {
|
||||
label: const Text("Elimina"),
|
||||
onPressed: () {
|
||||
// Qui lancerai l'evento per eliminare i file selezionati!
|
||||
// Es: serviceFilesBloc.add(DeleteSelectedFilesEvent());
|
||||
// Es: operationFilesBloc.add(DeleteSelectedFilesEvent());
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
@@ -243,14 +245,14 @@ class AttachmentsSection extends StatelessWidget {
|
||||
}
|
||||
|
||||
Future<void> _handleGenerateQr(BuildContext context) async {
|
||||
final cubit = context.read<ServicesCubit>();
|
||||
var currentService = cubit.state.currentService;
|
||||
final cubit = context.read<OperationsCubit>();
|
||||
var currentOperation = cubit.state.currentOperation;
|
||||
|
||||
// 1. CATTURIAMO IL BLOC MENTRE SIAMO ANCORA NELLA PAGINA
|
||||
final serviceFilesBloc = context.read<ServiceFilesBloc>();
|
||||
final operationFilesBloc = context.read<OperationFilesBloc>();
|
||||
|
||||
// 2. SE LA PRATICA E' NUOVA (Manca l'ID)
|
||||
if (currentService == null || currentService.id == null) {
|
||||
if (currentOperation == null || currentOperation.id == null) {
|
||||
// NIENTE BlocListener qui! Solo un semplice Dialog di conferma
|
||||
final bool? confirm = await showDialog<bool>(
|
||||
context: context,
|
||||
@@ -275,42 +277,42 @@ class AttachmentsSection extends StatelessWidget {
|
||||
if (confirm != true) return; // Utente ha annullato
|
||||
|
||||
// Salviamo forzatamente in bozza
|
||||
await cubit.saveCurrentService(
|
||||
await cubit.saveCurrentOperation(
|
||||
isBozza: true,
|
||||
shouldPop: false,
|
||||
files: serviceFilesBloc.state.localFiles,
|
||||
files: operationFilesBloc.state.localFiles,
|
||||
);
|
||||
|
||||
// Recuperiamo il servizio aggiornato con l'ID!
|
||||
currentService = cubit.state.currentService;
|
||||
currentOperation = cubit.state.currentOperation;
|
||||
|
||||
if (currentService?.id == null) return;
|
||||
if (currentOperation?.id == null) return;
|
||||
}
|
||||
|
||||
// 3. MOSTRIAMO IL QR CODE (Con il Ponte e l'Auto-Chiusura!)
|
||||
if (context.mounted) {
|
||||
final nomePratica = "Pratica ${currentService?.customerDisplayName ?? ''}"
|
||||
.trim();
|
||||
final nomePratica =
|
||||
"Pratica ${currentOperation?.customerDisplayName ?? ''}".trim();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => BlocProvider.value(
|
||||
// INIETTIAMO IL BLOC NEL CONTESTO DEL DIALOG ALIENO
|
||||
value: serviceFilesBloc,
|
||||
value: operationFilesBloc,
|
||||
|
||||
// ORA METTIAMO L'AUTO-CHIUSURA SUL QR CODE!
|
||||
child: BlocListener<ServiceFilesBloc, ServiceFilesState>(
|
||||
child: BlocListener<OperationFilesBloc, OperationFilesState>(
|
||||
listener: (context, state) {
|
||||
// Se arrivano file remoti e lo stato è success, chiudiamo il QR!
|
||||
// (Nota: usiamo dialogContext per assicurarci di chiudere il popup giusto)
|
||||
if (state.status == ServiceFilesStatus.success &&
|
||||
if (state.status == OperationFilesStatus.success &&
|
||||
state.remoteFiles.isNotEmpty) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
}
|
||||
},
|
||||
child: QrUploadDialog(
|
||||
deepLinkUrl:
|
||||
'fluxapp:///operation/${currentService!.id}/upload?name=${Uri.encodeComponent(nomePratica)}',
|
||||
'fluxapp:///operation/${currentOperation!.id}/upload?name=${Uri.encodeComponent(nomePratica)}',
|
||||
title: 'Scatta per\n$nomePratica',
|
||||
),
|
||||
),
|
||||
@@ -322,7 +324,7 @@ class AttachmentsSection extends StatelessWidget {
|
||||
// --- LOGICA DI COPIA AL CLIENTE ---
|
||||
void saveAndCopyFilesToCustomer(
|
||||
BuildContext context,
|
||||
List<ServiceFileModel> files,
|
||||
List<OperationFileModel> files,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
@@ -341,7 +343,7 @@ class AttachmentsSection extends StatelessWidget {
|
||||
onPressed: () {
|
||||
Navigator.pop(ctx);
|
||||
// 1. Diciamo al Cubit di salvare in Bozza e fare la copia
|
||||
context.read<ServicesCubit>().saveAndCopyFileToCustomer(files);
|
||||
context.read<OperationsCubit>().saveAndCopyFileToCustomer(files);
|
||||
},
|
||||
child: const Text("Salva e Copia"),
|
||||
),
|
||||
@@ -351,7 +353,7 @@ class AttachmentsSection extends StatelessWidget {
|
||||
}
|
||||
|
||||
// --- LOGICA DI VISUALIZZAZIONE OVERLAY ---
|
||||
void _handleDoubleClick(BuildContext context, ServiceFileModel file) {
|
||||
void _handleDoubleClick(BuildContext context, OperationFileModel file) {
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flux/features/customers/ui/customer_search_sheet.dart';
|
||||
import 'package:flux/features/operations/models/service_model.dart';
|
||||
import 'package:flux/features/operations/models/operation_model.dart';
|
||||
|
||||
class CustomerSection extends StatelessWidget {
|
||||
final ServiceModel operation;
|
||||
final OperationModel operation;
|
||||
|
||||
const CustomerSection({super.key, required this.operation});
|
||||
|
||||
@@ -2,32 +2,32 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
|
||||
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
||||
import 'package:flux/features/operations/models/energy_service_model.dart'; // Assicurati degli import
|
||||
import 'package:flux/features/operations/models/energy_operation_model.dart'; // Assicurati degli import
|
||||
|
||||
class EnergyServiceDialog extends StatefulWidget {
|
||||
final List<EnergyServiceModel> initialServices;
|
||||
class EnergyOperationDialog extends StatefulWidget {
|
||||
final List<EnergyOperationModel> initialOperations;
|
||||
final String
|
||||
currentStoreId; // Ci serve per sapere per quale negozio caricare i gestori
|
||||
|
||||
const EnergyServiceDialog({
|
||||
const EnergyOperationDialog({
|
||||
super.key,
|
||||
required this.initialServices,
|
||||
required this.initialOperations,
|
||||
required this.currentStoreId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EnergyServiceDialog> createState() => _EnergyServiceDialogState();
|
||||
State<EnergyOperationDialog> createState() => _EnergyOperationDialogState();
|
||||
}
|
||||
|
||||
class _EnergyServiceDialogState extends State<EnergyServiceDialog> {
|
||||
class _EnergyOperationDialogState extends State<EnergyOperationDialog> {
|
||||
// Lista temporanea per non "sporcare" il cubit finché non si preme Conferma
|
||||
late List<EnergyServiceModel> _tempList;
|
||||
late List<EnergyOperationModel> _tempList;
|
||||
bool _isAddingNew = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tempList = List.from(widget.initialServices);
|
||||
_tempList = List.from(widget.initialOperations);
|
||||
// Al caricamento della modale, chiediamo al Cubit di recuperare i gestori veri!
|
||||
context.read<ProvidersCubit>().loadActiveProvidersForStore(
|
||||
widget.currentStoreId,
|
||||
@@ -52,9 +52,9 @@ class _EnergyServiceDialogState extends State<EnergyServiceDialog> {
|
||||
// Cambia vista in base al flag
|
||||
child: _isAddingNew
|
||||
? _EnergyForm(
|
||||
onSave: (newService) {
|
||||
onSave: (newOperation) {
|
||||
setState(() {
|
||||
_tempList.add(newService);
|
||||
_tempList.add(newOperation);
|
||||
_isAddingNew = false; // Torna alla lista
|
||||
});
|
||||
},
|
||||
@@ -101,7 +101,7 @@ class _EnergyServiceDialogState extends State<EnergyServiceDialog> {
|
||||
// VISTA 1: LA LISTA DEI CONTRATTI
|
||||
// ==========================================
|
||||
class _EnergyList extends StatelessWidget {
|
||||
final List<EnergyServiceModel> operations;
|
||||
final List<EnergyOperationModel> operations;
|
||||
final List<ProviderModel>
|
||||
activeProviders; // <--- NUOVO: La lista vera dal Cubit
|
||||
final Function(int) onDelete;
|
||||
@@ -193,7 +193,7 @@ class _EnergyList extends StatelessWidget {
|
||||
// VISTA 2: IL FORM DI INSERIMENTO
|
||||
// ==========================================
|
||||
class _EnergyForm extends StatefulWidget {
|
||||
final Function(EnergyServiceModel) onSave;
|
||||
final Function(EnergyOperationModel) onSave;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
const _EnergyForm({required this.onSave, required this.onCancel});
|
||||
@@ -400,12 +400,12 @@ class _EnergyFormState extends State<_EnergyForm> {
|
||||
(_selectedProviderId == null || _selectedExpiration == null)
|
||||
? null // Disabilitato se mancano dati obbligatori
|
||||
: () {
|
||||
final newService = EnergyServiceModel(
|
||||
final newOperation = EnergyOperationModel(
|
||||
type: _selectedType,
|
||||
expiration: _selectedExpiration!,
|
||||
providerId: _selectedProviderId!,
|
||||
);
|
||||
widget.onSave(newService);
|
||||
widget.onSave(newOperation);
|
||||
},
|
||||
child: const Text("Salva Contratto"),
|
||||
),
|
||||
@@ -3,34 +3,34 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
|
||||
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
||||
import 'package:flux/features/operations/data/services_repository.dart';
|
||||
import 'package:flux/features/operations/models/entertainment_service_model.dart';
|
||||
import 'package:flux/features/operations/data/operations_repository.dart';
|
||||
import 'package:flux/features/operations/models/entertainment_operation_model.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
|
||||
class EntertainmentServiceDialog extends StatefulWidget {
|
||||
final List<EntertainmentServiceModel> initialServices;
|
||||
class EntertainmentOperationDialog extends StatefulWidget {
|
||||
final List<EntertainmentOperationModel> initialOperations;
|
||||
final String currentStoreId;
|
||||
|
||||
const EntertainmentServiceDialog({
|
||||
const EntertainmentOperationDialog({
|
||||
super.key,
|
||||
required this.initialServices,
|
||||
required this.initialOperations,
|
||||
required this.currentStoreId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<EntertainmentServiceDialog> createState() =>
|
||||
_EntertainmentServiceDialogState();
|
||||
State<EntertainmentOperationDialog> createState() =>
|
||||
_EntertainmentOperationDialogState();
|
||||
}
|
||||
|
||||
class _EntertainmentServiceDialogState
|
||||
extends State<EntertainmentServiceDialog> {
|
||||
late List<EntertainmentServiceModel> _tempList;
|
||||
class _EntertainmentOperationDialogState
|
||||
extends State<EntertainmentOperationDialog> {
|
||||
late List<EntertainmentOperationModel> _tempList;
|
||||
bool _isAddingNew = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tempList = List.from(widget.initialServices);
|
||||
_tempList = List.from(widget.initialOperations);
|
||||
// Carichiamo i provider attivi per lo store corrente
|
||||
context.read<ProvidersCubit>().loadActiveProvidersForStore(
|
||||
widget.currentStoreId,
|
||||
@@ -57,8 +57,8 @@ class _EntertainmentServiceDialogState
|
||||
child: _isAddingNew
|
||||
? _EntertainmentForm(
|
||||
// Il form che abbiamo creato prima
|
||||
onSave: (newService) => setState(() {
|
||||
_tempList.add(newService);
|
||||
onSave: (newOperation) => setState(() {
|
||||
_tempList.add(newOperation);
|
||||
_isAddingNew = false;
|
||||
}),
|
||||
onCancel: () => setState(() => _isAddingNew = false),
|
||||
@@ -94,7 +94,7 @@ class _EntertainmentServiceDialogState
|
||||
}
|
||||
|
||||
class _EntertainmentList extends StatelessWidget {
|
||||
final List<EntertainmentServiceModel> operations;
|
||||
final List<EntertainmentOperationModel> operations;
|
||||
final List<ProviderModel> allProviders;
|
||||
final Function(int) onDelete;
|
||||
final VoidCallback onAddTap;
|
||||
@@ -194,7 +194,7 @@ class _EntertainmentList extends StatelessWidget {
|
||||
// ---ENTERTAINMENT FORM (MODALE)---
|
||||
|
||||
class _EntertainmentForm extends StatefulWidget {
|
||||
final Function(EntertainmentServiceModel) onSave;
|
||||
final Function(EntertainmentOperationModel) onSave;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
const _EntertainmentForm({required this.onSave, required this.onCancel});
|
||||
@@ -280,7 +280,7 @@ class _EntertainmentFormState extends State<_EntertainmentForm> {
|
||||
const SizedBox(height: 8),
|
||||
// Suggerimenti rapidi (Chip)
|
||||
FutureBuilder<List<String>>(
|
||||
future: GetIt.I<ServicesRepository>().fetchTopEntertainmentTypes(
|
||||
future: GetIt.I<OperationsRepository>().fetchTopEntertainmentTypes(
|
||||
GetIt.I<SessionCubit>().state.company!.id!,
|
||||
),
|
||||
builder: (context, snapshot) {
|
||||
@@ -376,7 +376,7 @@ class _EntertainmentFormState extends State<_EntertainmentForm> {
|
||||
(_selectedProviderId == null || _typeController.text.isEmpty)
|
||||
? null
|
||||
: () => widget.onSave(
|
||||
EntertainmentServiceModel(
|
||||
EntertainmentOperationModel(
|
||||
providerId: _selectedProviderId!,
|
||||
type: _typeController.text,
|
||||
constrained: _isConstrained,
|
||||
@@ -5,36 +5,36 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
|
||||
import 'package:flux/features/master_data/products/models/model_model.dart';
|
||||
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
|
||||
import 'package:flux/features/operations/models/fin_service_model.dart';
|
||||
import 'package:flux/features/operations/models/fin_operation_model.dart';
|
||||
import 'package:flux/features/master_data/providers/models/provider_model.dart';
|
||||
|
||||
// ===========================================================================
|
||||
// DIALOG PRINCIPALE
|
||||
// ===========================================================================
|
||||
class FinanceServiceDialog extends StatefulWidget {
|
||||
final List<FinServiceModel> initialServices;
|
||||
class FinanceOperationDialog extends StatefulWidget {
|
||||
final List<FinOperationModel> initialOperations;
|
||||
final String currentStoreId;
|
||||
final ProductCubit productCubit;
|
||||
|
||||
const FinanceServiceDialog({
|
||||
const FinanceOperationDialog({
|
||||
super.key,
|
||||
required this.initialServices,
|
||||
required this.initialOperations,
|
||||
required this.currentStoreId,
|
||||
required this.productCubit,
|
||||
});
|
||||
|
||||
@override
|
||||
State<FinanceServiceDialog> createState() => _FinanceServiceDialogState();
|
||||
State<FinanceOperationDialog> createState() => _FinanceOperationDialogState();
|
||||
}
|
||||
|
||||
class _FinanceServiceDialogState extends State<FinanceServiceDialog> {
|
||||
late List<FinServiceModel> _tempList;
|
||||
class _FinanceOperationDialogState extends State<FinanceOperationDialog> {
|
||||
late List<FinOperationModel> _tempList;
|
||||
bool _isAddingNew = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tempList = List.from(widget.initialServices);
|
||||
_tempList = List.from(widget.initialOperations);
|
||||
// Carichiamo i dati necessari dai Cubit
|
||||
context.read<ProvidersCubit>().loadActiveProvidersForStore(
|
||||
widget.currentStoreId,
|
||||
@@ -109,7 +109,7 @@ class _FinanceServiceDialogState extends State<FinanceServiceDialog> {
|
||||
// VISTA LISTA (STORICA)
|
||||
// ===========================================================================
|
||||
class _FinanceList extends StatelessWidget {
|
||||
final List<FinServiceModel> operations;
|
||||
final List<FinOperationModel> operations;
|
||||
final List<ProviderModel> allProviders;
|
||||
final List<ModelModel> allModels;
|
||||
final Function(int) onDelete;
|
||||
@@ -221,7 +221,7 @@ class _FinanceList extends StatelessWidget {
|
||||
// FORM CON OMNI-SEARCH
|
||||
// ===========================================================================
|
||||
class _FinanceForm extends StatefulWidget {
|
||||
final Function(FinServiceModel) onSave;
|
||||
final Function(FinOperationModel) onSave;
|
||||
final VoidCallback onCancel;
|
||||
|
||||
const _FinanceForm({required this.onSave, required this.onCancel});
|
||||
@@ -428,7 +428,7 @@ class _FinanceFormState extends State<_FinanceForm> {
|
||||
: () {
|
||||
final now = DateTime.now();
|
||||
widget.onSave(
|
||||
FinServiceModel(
|
||||
FinOperationModel(
|
||||
providerId: _selectedProviderId!,
|
||||
modelId: _selectedModel!.id!,
|
||||
expiration: DateTime(
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/operations/blocs/operations_cubit.dart';
|
||||
import 'package:flux/features/operations/models/service_model.dart';
|
||||
import 'package:flux/features/operations/models/operation_model.dart';
|
||||
|
||||
class GeneralInfoSection extends StatelessWidget {
|
||||
final ServiceModel operation;
|
||||
final OperationModel operation;
|
||||
const GeneralInfoSection({super.key, required this.operation});
|
||||
|
||||
@override
|
||||
@@ -44,7 +44,7 @@ class GeneralInfoSection extends StatelessWidget {
|
||||
prefixIcon: Icon(Icons.phone),
|
||||
),
|
||||
onChanged: (val) {
|
||||
context.read<ServicesCubit>().updateField(number: val);
|
||||
context.read<OperationsCubit>().updateField(number: val);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -63,7 +63,7 @@ class GeneralInfoSection extends StatelessWidget {
|
||||
activeThumbColor: Colors.orange,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onChanged: (val) {
|
||||
context.read<ServicesCubit>().updateField(isBozza: val);
|
||||
context.read<OperationsCubit>().updateField(isBozza: val);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -79,7 +79,9 @@ class GeneralInfoSection extends StatelessWidget {
|
||||
activeThumbColor: Colors.green,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onChanged: (val) {
|
||||
context.read<ServicesCubit>().updateField(resultOk: val);
|
||||
context.read<OperationsCubit>().updateField(
|
||||
resultOk: val,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -100,7 +102,7 @@ class GeneralInfoSection extends StatelessWidget {
|
||||
alignLabelWithHint: true,
|
||||
),
|
||||
onChanged: (val) {
|
||||
context.read<ServicesCubit>().updateField(note: val);
|
||||
context.read<OperationsCubit>().updateField(note: val);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -1,49 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/operations/blocs/operations_cubit.dart';
|
||||
import 'package:flux/features/operations/models/service_model.dart';
|
||||
import 'package:flux/features/operations/ui/service_form_screen/attachment_section.dart';
|
||||
import 'package:flux/features/operations/ui/service_form_screen/customer_section.dart';
|
||||
import 'package:flux/features/operations/ui/service_form_screen/general_info_section.dart';
|
||||
import 'package:flux/features/operations/ui/service_form_screen/services_grid.dart';
|
||||
import 'package:flux/features/operations/models/operation_model.dart';
|
||||
import 'package:flux/features/operations/ui/operation_form_screen/attachment_section.dart';
|
||||
import 'package:flux/features/operations/ui/operation_form_screen/customer_section.dart';
|
||||
import 'package:flux/features/operations/ui/operation_form_screen/general_info_section.dart';
|
||||
import 'package:flux/features/operations/ui/operation_form_screen/operations_grid.dart';
|
||||
|
||||
class ServiceFormScreen extends StatefulWidget {
|
||||
final String? serviceId;
|
||||
final ServiceModel? existingService; // <-- AGGIUNTO
|
||||
class OperationFormScreen extends StatefulWidget {
|
||||
final String? operationId;
|
||||
final OperationModel? existingOperation; // <-- AGGIUNTO
|
||||
|
||||
const ServiceFormScreen({
|
||||
const OperationFormScreen({
|
||||
super.key,
|
||||
this.serviceId,
|
||||
this.existingService, // <-- AGGIUNTO
|
||||
this.operationId,
|
||||
this.existingOperation, // <-- AGGIUNTO
|
||||
});
|
||||
|
||||
@override
|
||||
State<ServiceFormScreen> createState() => _ServiceFormScreenState();
|
||||
State<OperationFormScreen> createState() => _OperationFormScreenState();
|
||||
}
|
||||
|
||||
class _ServiceFormScreenState extends State<ServiceFormScreen> {
|
||||
class _OperationFormScreenState extends State<OperationFormScreen> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// Diamo in pasto al Cubit tutto quello che abbiamo!
|
||||
context.read<ServicesCubit>().initServiceForm(
|
||||
existingService: widget.existingService,
|
||||
serviceId: widget.serviceId,
|
||||
context.read<OperationsCubit>().initOperationForm(
|
||||
existingOperation: widget.existingOperation,
|
||||
operationId: widget.operationId,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _performSave(BuildContext context, {required bool isBozza}) {
|
||||
FocusScope.of(context).unfocus();
|
||||
context.read<ServicesCubit>().saveCurrentService(isBozza: isBozza);
|
||||
context.read<OperationsCubit>().saveCurrentOperation(isBozza: isBozza);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<ServicesCubit, ServicesState>(
|
||||
return BlocConsumer<OperationsCubit, OperationsState>(
|
||||
listener: (context, state) {
|
||||
if (state.status == ServicesStatus.saved) {
|
||||
if (state.status == OperationsStatus.saved) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Pratica salvata con successo!"),
|
||||
@@ -52,7 +52,7 @@ class _ServiceFormScreenState extends State<ServiceFormScreen> {
|
||||
);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
if (state.status == ServicesStatus.failure) {
|
||||
if (state.status == OperationsStatus.failure) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("Errore: ${state.errorMessage ?? ''}"),
|
||||
@@ -60,7 +60,7 @@ class _ServiceFormScreenState extends State<ServiceFormScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state.status == ServicesStatus.savedNoPop) {
|
||||
if (state.status == OperationsStatus.savedNoPop) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Pratica salvata con successo!"),
|
||||
@@ -70,9 +70,9 @@ class _ServiceFormScreenState extends State<ServiceFormScreen> {
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
final operation = state.currentService;
|
||||
final isSaving = state.status == ServicesStatus.saving;
|
||||
final isEditMode = widget.serviceId != null;
|
||||
final operation = state.currentOperation;
|
||||
final isSaving = state.status == OperationsStatus.saving;
|
||||
final isEditMode = widget.operationId != null;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -120,7 +120,7 @@ class _ServiceFormScreenState extends State<ServiceFormScreen> {
|
||||
GeneralInfoSection(operation: operation),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
ServicesGrid(operation: operation),
|
||||
OperationsGrid(operation: operation),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
AttachmentsSection(),
|
||||
@@ -3,24 +3,25 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flux/features/operations/blocs/service_files_bloc.dart';
|
||||
import 'package:flux/features/operations/blocs/operation_files_bloc.dart';
|
||||
|
||||
class ServiceMobileUploadScreen extends StatefulWidget {
|
||||
final String serviceId;
|
||||
final String serviceName;
|
||||
class OperationMobileUploadScreen extends StatefulWidget {
|
||||
final String operationId;
|
||||
final String operationName;
|
||||
|
||||
const ServiceMobileUploadScreen({
|
||||
const OperationMobileUploadScreen({
|
||||
super.key,
|
||||
required this.serviceId,
|
||||
required this.serviceName,
|
||||
required this.operationId,
|
||||
required this.operationName,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ServiceMobileUploadScreen> createState() =>
|
||||
_ServiceMobileUploadScreenState();
|
||||
State<OperationMobileUploadScreen> createState() =>
|
||||
_OperationMobileUploadScreenState();
|
||||
}
|
||||
|
||||
class _ServiceMobileUploadScreenState extends State<ServiceMobileUploadScreen> {
|
||||
class _OperationMobileUploadScreenState
|
||||
extends State<OperationMobileUploadScreen> {
|
||||
// 1. LA NOSTRA STAGING AREA (Il "Carrello")
|
||||
final List<PlatformFile> _stagedFiles = [];
|
||||
|
||||
@@ -35,10 +36,10 @@ class _ServiceMobileUploadScreenState extends State<ServiceMobileUploadScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocListener<ServiceFilesBloc, ServiceFilesState>(
|
||||
return BlocListener<OperationFilesBloc, OperationFilesState>(
|
||||
listener: (context, state) {
|
||||
// Quando il BLoC ci dice che ha finito l'upload (Success), chiudiamo la pagina!
|
||||
if (state.status == ServiceFilesStatus.success && _isUploading) {
|
||||
if (state.status == OperationFilesStatus.success && _isUploading) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text("Tutti i file caricati con successo! ✅"),
|
||||
@@ -46,7 +47,7 @@ class _ServiceMobileUploadScreenState extends State<ServiceMobileUploadScreen> {
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
if (state.status == ServiceFilesStatus.failure) {
|
||||
if (state.status == OperationFilesStatus.failure) {
|
||||
setState(() => _isUploading = false);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
@@ -55,7 +56,7 @@ class _ServiceMobileUploadScreenState extends State<ServiceMobileUploadScreen> {
|
||||
},
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text("Upload Pratica:\n${widget.serviceName}"),
|
||||
title: Text("Upload Pratica:\n${widget.operationName}"),
|
||||
automaticallyImplyLeading: !_isUploading,
|
||||
),
|
||||
body: Stack(
|
||||
@@ -294,8 +295,8 @@ class _ServiceMobileUploadScreenState extends State<ServiceMobileUploadScreen> {
|
||||
|
||||
// Diciamo al BLoC di caricare tutti i file.
|
||||
// Usiamo il tuo evento esistente per ogni file (il BLoC li metterà in coda)
|
||||
final bloc = context.read<ServiceFilesBloc>();
|
||||
bloc.add(UploadMultipleServiceFilesEvent(_stagedFiles));
|
||||
final bloc = context.read<OperationFilesBloc>();
|
||||
bloc.add(UploadMultipleOperationFilesEvent(_stagedFiles));
|
||||
|
||||
// N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"!
|
||||
}
|
||||
@@ -2,20 +2,20 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
|
||||
import 'package:flux/features/operations/blocs/operations_cubit.dart';
|
||||
import 'package:flux/features/operations/models/energy_service_model.dart';
|
||||
import 'package:flux/features/operations/models/entertainment_service_model.dart';
|
||||
import 'package:flux/features/operations/models/fin_service_model.dart';
|
||||
import 'package:flux/features/operations/models/service_model.dart';
|
||||
import 'package:flux/features/operations/ui/service_form_screen/action_card.dart';
|
||||
import 'package:flux/features/operations/ui/service_form_screen/energy_service_dialog.dart';
|
||||
import 'package:flux/features/operations/ui/service_form_screen/entertainment_service_card.dart';
|
||||
import 'package:flux/features/operations/ui/service_form_screen/finance_service_dialog.dart';
|
||||
import 'package:flux/features/operations/ui/service_form_screen/int_dialogs.dart'; // Assicurati di importare il modello
|
||||
import 'package:flux/features/operations/models/energy_operation_model.dart';
|
||||
import 'package:flux/features/operations/models/entertainment_operation_model.dart';
|
||||
import 'package:flux/features/operations/models/fin_operation_model.dart';
|
||||
import 'package:flux/features/operations/models/operation_model.dart';
|
||||
import 'package:flux/features/operations/ui/operation_form_screen/action_card.dart';
|
||||
import 'package:flux/features/operations/ui/operation_form_screen/energy_operation_dialog.dart';
|
||||
import 'package:flux/features/operations/ui/operation_form_screen/entertainment_operation_card.dart';
|
||||
import 'package:flux/features/operations/ui/operation_form_screen/finance_operation_dialog.dart';
|
||||
import 'package:flux/features/operations/ui/operation_form_screen/int_dialogs.dart'; // Assicurati di importare il modello
|
||||
|
||||
class ServicesGrid extends StatelessWidget {
|
||||
final ServiceModel operation;
|
||||
class OperationsGrid extends StatelessWidget {
|
||||
final OperationModel operation;
|
||||
|
||||
const ServicesGrid({super.key, required this.operation});
|
||||
const OperationsGrid({super.key, required this.operation});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -60,7 +60,7 @@ class ServicesGrid extends StatelessWidget {
|
||||
"AL",
|
||||
operation.al,
|
||||
(val) =>
|
||||
context.read<ServicesCubit>().updateField(al: val),
|
||||
context.read<OperationsCubit>().updateField(al: val),
|
||||
),
|
||||
),
|
||||
ActionCard(
|
||||
@@ -73,7 +73,7 @@ class ServicesGrid extends StatelessWidget {
|
||||
"MNP",
|
||||
operation.mnp,
|
||||
(val) =>
|
||||
context.read<ServicesCubit>().updateField(mnp: val),
|
||||
context.read<OperationsCubit>().updateField(mnp: val),
|
||||
),
|
||||
),
|
||||
ActionCard(
|
||||
@@ -86,7 +86,7 @@ class ServicesGrid extends StatelessWidget {
|
||||
"NIP",
|
||||
operation.nip,
|
||||
(val) =>
|
||||
context.read<ServicesCubit>().updateField(nip: val),
|
||||
context.read<OperationsCubit>().updateField(nip: val),
|
||||
),
|
||||
),
|
||||
ActionCard(
|
||||
@@ -98,8 +98,9 @@ class ServicesGrid extends StatelessWidget {
|
||||
context,
|
||||
"Unica",
|
||||
operation.unica,
|
||||
(val) =>
|
||||
context.read<ServicesCubit>().updateField(unica: val),
|
||||
(val) => context.read<OperationsCubit>().updateField(
|
||||
unica: val,
|
||||
),
|
||||
),
|
||||
),
|
||||
ActionCard(
|
||||
@@ -111,7 +112,7 @@ class ServicesGrid extends StatelessWidget {
|
||||
context,
|
||||
"Telepass",
|
||||
operation.telepass,
|
||||
(val) => context.read<ServicesCubit>().updateField(
|
||||
(val) => context.read<OperationsCubit>().updateField(
|
||||
telepass: val,
|
||||
),
|
||||
),
|
||||
@@ -120,23 +121,24 @@ class ServicesGrid extends StatelessWidget {
|
||||
// --- MODULI COMPLESSI (Le liste) ---
|
||||
ActionCard(
|
||||
label: "Energia",
|
||||
count: operation.energyServices.length,
|
||||
count: operation.energyOperations.length,
|
||||
icon: Icons.bolt,
|
||||
color: Colors.green,
|
||||
onTap: () async {
|
||||
// Apriamo la modale e aspettiamo il risultato
|
||||
final result = await showDialog<List<EnergyServiceModel>>(
|
||||
context: context,
|
||||
builder: (context) => EnergyServiceDialog(
|
||||
currentStoreId: operation.storeId,
|
||||
initialServices: operation
|
||||
.energyServices, // Passiamo la lista attuale
|
||||
),
|
||||
);
|
||||
final result =
|
||||
await showDialog<List<EnergyOperationModel>>(
|
||||
context: context,
|
||||
builder: (context) => EnergyOperationDialog(
|
||||
currentStoreId: operation.storeId,
|
||||
initialOperations: operation
|
||||
.energyOperations, // Passiamo la lista attuale
|
||||
),
|
||||
);
|
||||
|
||||
// Se l'utente ha premuto "Conferma" e non "Annulla" o tap fuori
|
||||
if (result != null && context.mounted) {
|
||||
context.read<ServicesCubit>().updateEnergyServices(
|
||||
context.read<OperationsCubit>().updateEnergyOperations(
|
||||
result,
|
||||
);
|
||||
}
|
||||
@@ -144,44 +146,47 @@ class ServicesGrid extends StatelessWidget {
|
||||
),
|
||||
ActionCard(
|
||||
label: "Finanziam.",
|
||||
count: operation.finServices.length,
|
||||
count: operation.finOperations.length,
|
||||
icon: Icons.euro_symbol,
|
||||
color: Colors.teal,
|
||||
onTap: () async {
|
||||
final result = await showDialog<List<FinServiceModel>>(
|
||||
final result = await showDialog<List<FinOperationModel>>(
|
||||
context: context,
|
||||
builder: (context) => FinanceServiceDialog(
|
||||
builder: (context) => FinanceOperationDialog(
|
||||
productCubit: context.read<ProductCubit>(),
|
||||
currentStoreId: operation.storeId,
|
||||
initialServices: operation
|
||||
.finServices, // Passiamo la lista attuale
|
||||
initialOperations: operation
|
||||
.finOperations, // Passiamo la lista attuale
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && context.mounted) {
|
||||
context.read<ServicesCubit>().updateFinServices(result);
|
||||
context.read<OperationsCubit>().updateFinOperations(
|
||||
result,
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
ActionCard(
|
||||
label: "Intratten.",
|
||||
count: operation.entertainmentServices.length,
|
||||
count: operation.entertainmentOperations.length,
|
||||
icon: Icons.movie_filter_outlined,
|
||||
color: Colors.purple,
|
||||
onTap: () async {
|
||||
final result =
|
||||
await showDialog<List<EntertainmentServiceModel>>(
|
||||
await showDialog<List<EntertainmentOperationModel>>(
|
||||
context: context,
|
||||
builder: (context) => EntertainmentServiceDialog(
|
||||
initialServices: operation.entertainmentServices,
|
||||
builder: (context) => EntertainmentOperationDialog(
|
||||
initialOperations:
|
||||
operation.entertainmentOperations,
|
||||
currentStoreId: operation.storeId,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && context.mounted) {
|
||||
context
|
||||
.read<ServicesCubit>()
|
||||
.updateEntertainmentServices(result);
|
||||
.read<OperationsCubit>()
|
||||
.updateEntertainmentOperations(result);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -1,19 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/features/operations/blocs/operations_cubit.dart';
|
||||
import 'package:flux/features/operations/models/service_model.dart';
|
||||
import 'package:flux/features/operations/utils/service_actions.dart';
|
||||
import 'package:flux/features/operations/models/operation_model.dart';
|
||||
import 'package:flux/features/operations/utils/operation_actions.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
// Importa i tuoi modelli e cubit
|
||||
|
||||
class ServicesScreen extends StatefulWidget {
|
||||
const ServicesScreen({super.key});
|
||||
class OperationsScreen extends StatefulWidget {
|
||||
const OperationsScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ServicesScreen> createState() => _ServicesScreenState();
|
||||
State<OperationsScreen> createState() => _OperationsScreenState();
|
||||
}
|
||||
|
||||
class _ServicesScreenState extends State<ServicesScreen> {
|
||||
class _OperationsScreenState extends State<OperationsScreen> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
@@ -22,12 +22,12 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
||||
// Agganciamo il listener per la paginazione (Scroll Infinito)
|
||||
_scrollController.addListener(_onScroll);
|
||||
// Carichiamo i servizi iniziali
|
||||
context.read<ServicesCubit>().loadServices();
|
||||
context.read<OperationsCubit>().loadOperations();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_isBottom) {
|
||||
context.read<ServicesCubit>().loadServices();
|
||||
context.read<OperationsCubit>().loadOperations();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,16 +60,16 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
body: BlocBuilder<ServicesCubit, ServicesState>(
|
||||
body: BlocBuilder<OperationsCubit, OperationsState>(
|
||||
builder: (context, state) {
|
||||
// 1. Stato di caricamento iniziale
|
||||
if (state.status == ServicesStatus.loading &&
|
||||
state.allServices.isEmpty) {
|
||||
if (state.status == OperationsStatus.loading &&
|
||||
state.allOperations.isEmpty) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
// 2. Lista vuota
|
||||
if (state.allServices.isEmpty) {
|
||||
if (state.allOperations.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -77,9 +77,9 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
||||
const Text("Nessuna pratica trovata."),
|
||||
const SizedBox(height: 10),
|
||||
ElevatedButton(
|
||||
onPressed: () => context.read<ServicesCubit>().loadServices(
|
||||
refresh: true,
|
||||
),
|
||||
onPressed: () => context
|
||||
.read<OperationsCubit>()
|
||||
.loadOperations(refresh: true),
|
||||
child: const Text("Riprova"),
|
||||
),
|
||||
],
|
||||
@@ -90,15 +90,15 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
||||
// 3. La Lista (con Pull-to-refresh)
|
||||
return RefreshIndicator(
|
||||
onRefresh: () =>
|
||||
context.read<ServicesCubit>().loadServices(refresh: true),
|
||||
context.read<OperationsCubit>().loadOperations(refresh: true),
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.only(bottom: 80), // Spazio per il FAB
|
||||
itemCount: state.hasReachedMax
|
||||
? state.allServices.length
|
||||
: state.allServices.length + 1,
|
||||
? state.allOperations.length
|
||||
: state.allOperations.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index >= state.allServices.length) {
|
||||
if (index >= state.allOperations.length) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(16.0),
|
||||
@@ -107,21 +107,21 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
final operation = state.allServices[index];
|
||||
return _buildServiceCard(context, operation);
|
||||
final operation = state.allOperations[index];
|
||||
return _buildOperationCard(context, operation);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => startNewService(context),
|
||||
onPressed: () => startNewOperation(context),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildServiceCard(BuildContext context, ServiceModel operation) {
|
||||
Widget _buildOperationCard(BuildContext context, OperationModel operation) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
elevation: 2,
|
||||
@@ -164,11 +164,11 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
||||
children: [
|
||||
if (operation.al > 0 || operation.mnp > 0)
|
||||
_miniBadge("📞 Tel", Colors.blue),
|
||||
if (operation.energyServices.isNotEmpty)
|
||||
if (operation.energyOperations.isNotEmpty)
|
||||
_miniBadge("⚡ Energy", Colors.green),
|
||||
if (operation.finServices.isNotEmpty)
|
||||
if (operation.finOperations.isNotEmpty)
|
||||
_miniBadge("💰 Fin", Colors.purple),
|
||||
if (operation.entertainmentServices.isNotEmpty)
|
||||
if (operation.entertainmentOperations.isNotEmpty)
|
||||
_miniBadge("📺 Ent", Colors.red),
|
||||
],
|
||||
),
|
||||
@@ -180,7 +180,7 @@ class _ServicesScreenState extends State<ServicesScreen> {
|
||||
extra: operation, // <-- LA MAGIA È QUI: Passa l'oggetto intero!
|
||||
// Teniamo anche il parametro URL per coerenza di routing
|
||||
queryParameters: operation.id != null
|
||||
? {'serviceId': operation.id!}
|
||||
? {'operationId': operation.id!}
|
||||
: {},
|
||||
),
|
||||
),
|
||||
@@ -3,11 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
|
||||
import 'package:flux/features/operations/blocs/operations_cubit.dart';
|
||||
import 'package:flux/features/operations/models/service_model.dart';
|
||||
import 'package:flux/features/operations/models/operation_model.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
/// Avvia la creazione di un nuovo servizio partendo dalla selezione dell'operatore.
|
||||
void startNewService(BuildContext context) {
|
||||
void startNewOperation(BuildContext context) {
|
||||
final session = context.read<SessionCubit>().state;
|
||||
final currentStoreId = session.currentStore?.id;
|
||||
|
||||
@@ -53,8 +53,8 @@ void startNewService(BuildContext context) {
|
||||
title: Text(member.name),
|
||||
onTap: () {
|
||||
// 1. Inizializza il form nel Cubit
|
||||
context.read<ServicesCubit>().initServiceForm(
|
||||
existingService: ServiceModel(
|
||||
context.read<OperationsCubit>().initOperationForm(
|
||||
existingOperation: OperationModel(
|
||||
storeId: currentStoreId,
|
||||
employeeId: member.id,
|
||||
number: '',
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"welcomeBack": "Welcome back, {name}! 👋",
|
||||
"latestServices": "Latest Operations",
|
||||
"latestOperations": "Latest Operations",
|
||||
"masterData": "Master Data",
|
||||
"settings": "Settings",
|
||||
"newService": "Operation",
|
||||
"newOperation": "Operation",
|
||||
"expiring_contracts": "Expiring Contracts",
|
||||
"sticky_notes": "Sticky Notes",
|
||||
"my_tasks": "My Tasks",
|
||||
"latest_service_tickets": "Latest operation tickets"
|
||||
"latest_operation_tickets": "Latest operation tickets"
|
||||
|
||||
}
|
||||
@@ -50,15 +50,15 @@
|
||||
"commonNewPassword": "Nuova Password",
|
||||
"commonNote": "Nota",
|
||||
"commonSave": "Salva",
|
||||
"commonService": "Servizio",
|
||||
"commonOperation": "Servizio",
|
||||
"commonSettings": "Impostazioni",
|
||||
"commonStickyNotes": "Sticky Notes",
|
||||
"commonTask": "Attività",
|
||||
"homeExpiringContracts": "Contratti in scadenza",
|
||||
"homeLatestServiceTickets": "Ultime assistenze",
|
||||
"homeLatestServices": "Ultimi Servizi",
|
||||
"homeLatestOperationTickets": "Ultime assistenze",
|
||||
"homeLatestOperations": "Ultimi Servizi",
|
||||
"homeMyTasks": "Mie Attività",
|
||||
"homeNewServiceTicket": "Nuova assistenza",
|
||||
"homeNewOperationTicket": "Nuova assistenza",
|
||||
"homeNoStoreFound": "Nessun negozio trovato",
|
||||
"homeWelcomeBack": "Bentornato, {name}! 👋",
|
||||
"imageViewerWidgetErrorOpening": "Errore durante l'apertura dell'immagine",
|
||||
|
||||
@@ -220,11 +220,11 @@ abstract class AppLocalizations {
|
||||
/// **'Salva'**
|
||||
String get commonSave;
|
||||
|
||||
/// No description provided for @commonService.
|
||||
/// No description provided for @commonOperation.
|
||||
///
|
||||
/// In it, this message translates to:
|
||||
/// **'Servizio'**
|
||||
String get commonService;
|
||||
String get commonOperation;
|
||||
|
||||
/// No description provided for @commonSettings.
|
||||
///
|
||||
@@ -250,17 +250,17 @@ abstract class AppLocalizations {
|
||||
/// **'Contratti in scadenza'**
|
||||
String get homeExpiringContracts;
|
||||
|
||||
/// No description provided for @homeLatestServiceTickets.
|
||||
/// No description provided for @homeLatestOperationTickets.
|
||||
///
|
||||
/// In it, this message translates to:
|
||||
/// **'Ultime assistenze'**
|
||||
String get homeLatestServiceTickets;
|
||||
String get homeLatestOperationTickets;
|
||||
|
||||
/// No description provided for @homeLatestServices.
|
||||
/// No description provided for @homeLatestOperations.
|
||||
///
|
||||
/// In it, this message translates to:
|
||||
/// **'Ultimi Servizi'**
|
||||
String get homeLatestServices;
|
||||
String get homeLatestOperations;
|
||||
|
||||
/// No description provided for @homeMyTasks.
|
||||
///
|
||||
@@ -268,11 +268,11 @@ abstract class AppLocalizations {
|
||||
/// **'Mie Attività'**
|
||||
String get homeMyTasks;
|
||||
|
||||
/// No description provided for @homeNewServiceTicket.
|
||||
/// No description provided for @homeNewOperationTicket.
|
||||
///
|
||||
/// In it, this message translates to:
|
||||
/// **'Nuova assistenza'**
|
||||
String get homeNewServiceTicket;
|
||||
String get homeNewOperationTicket;
|
||||
|
||||
/// No description provided for @homeNoStoreFound.
|
||||
///
|
||||
|
||||
@@ -12,10 +12,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get homeExpiringContracts => 'Contratti in scadenza';
|
||||
|
||||
@override
|
||||
String get homeLatestServiceTickets => 'Ultime assistenze';
|
||||
String get homeLatestOperationTickets => 'Ultime assistenze';
|
||||
|
||||
@override
|
||||
String get homeLatestServices => 'Ultimi Servizi';
|
||||
String get homeLatestOperations => 'Ultimi Servizi';
|
||||
|
||||
@override
|
||||
String get homeMasterData => 'Anagrafiche';
|
||||
@@ -24,7 +24,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get homeMyTasks => 'Mie Attività';
|
||||
|
||||
@override
|
||||
String get homeNewService => 'Servizio';
|
||||
String get homeNewOperation => 'Servizio';
|
||||
|
||||
@override
|
||||
String get homeSettings => 'Impostazioni';
|
||||
@@ -41,7 +41,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get homeNoStoreFound => 'Nessun negozio trovato';
|
||||
|
||||
@override
|
||||
String get homeNewServiceTicket => 'Nuova assistenza';
|
||||
String get homeNewOperationTicket => 'Nuova assistenza';
|
||||
|
||||
@override
|
||||
String get homeNewNote => 'Nota';
|
||||
|
||||
@@ -81,7 +81,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get commonSave => 'Salva';
|
||||
|
||||
@override
|
||||
String get commonService => 'Servizio';
|
||||
String get commonOperation => 'Servizio';
|
||||
|
||||
@override
|
||||
String get commonSettings => 'Impostazioni';
|
||||
@@ -96,16 +96,16 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get homeExpiringContracts => 'Contratti in scadenza';
|
||||
|
||||
@override
|
||||
String get homeLatestServiceTickets => 'Ultime assistenze';
|
||||
String get homeLatestOperationTickets => 'Ultime assistenze';
|
||||
|
||||
@override
|
||||
String get homeLatestServices => 'Ultimi Servizi';
|
||||
String get homeLatestOperations => 'Ultimi Servizi';
|
||||
|
||||
@override
|
||||
String get homeMyTasks => 'Mie Attività';
|
||||
|
||||
@override
|
||||
String get homeNewServiceTicket => 'Nuova assistenza';
|
||||
String get homeNewOperationTicket => 'Nuova assistenza';
|
||||
|
||||
@override
|
||||
String get homeNoStoreFound => 'Nessun negozio trovato';
|
||||
|
||||
@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flux/features/auth/bloc/auth_cubit.dart';
|
||||
import 'package:flux/features/home/latest_store_services/bloc/latest_store_services_bloc.dart';
|
||||
import 'package:flux/features/operations/data/operations_repository.dart';
|
||||
import 'package:flux/l10n/app_localizations.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -27,7 +27,6 @@ import 'package:flux/features/master_data/staff/data/staff_repository.dart';
|
||||
import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
|
||||
import 'package:flux/features/master_data/store/data/store_repository.dart';
|
||||
import 'package:flux/features/operations/blocs/operations_cubit.dart';
|
||||
import 'package:flux/features/operations/data/services_repository.dart';
|
||||
import 'package:flux/features/settings/settings.dart';
|
||||
|
||||
void main() async {
|
||||
@@ -52,7 +51,7 @@ void main() async {
|
||||
BlocProvider<CustomerCubit>(create: (_) => CustomerCubit()),
|
||||
BlocProvider<ProductCubit>(create: (_) => ProductCubit()),
|
||||
BlocProvider<StaffCubit>(create: (_) => StaffCubit()),
|
||||
BlocProvider<ServicesCubit>(create: (_) => ServicesCubit()),
|
||||
BlocProvider<OperationsCubit>(create: (_) => OperationsCubit()),
|
||||
BlocProvider<ProvidersCubit>(create: (_) => ProvidersCubit()),
|
||||
],
|
||||
child: const FluxApp(),
|
||||
@@ -85,7 +84,9 @@ Future<void> setupLocator() async {
|
||||
getIt.registerLazySingleton<CustomerRepository>(() => CustomerRepository());
|
||||
getIt.registerLazySingleton<ProductRepository>(() => ProductRepository());
|
||||
getIt.registerLazySingleton<StaffRepository>(() => StaffRepository());
|
||||
getIt.registerLazySingleton<ServicesRepository>(() => ServicesRepository());
|
||||
getIt.registerLazySingleton<OperationsRepository>(
|
||||
() => OperationsRepository(),
|
||||
);
|
||||
getIt.registerLazySingleton<ProviderRepository>(() => ProviderRepository());
|
||||
|
||||
// NOTA: CompanyRepository l'ho tolto perché la logica della Company
|
||||
|
||||
Reference in New Issue
Block a user