This commit is contained in:
2026-05-01 10:11:44 +02:00
parent 9c8576ada5
commit f8bcac51e1
48 changed files with 1187 additions and 1141 deletions

View File

@@ -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_detail_screen.dart';
import 'package:flux/features/customers/ui/customer_mobile_upload_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/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/home/ui/home_screen.dart';
import 'package:flux/features/master_data/master_data_hub_content.dart'; import 'package:flux/features/master_data/master_data_hub_content.dart';
import 'package:flux/features/master_data/products/ui/products_screen.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/master_data/store/ui/stores_screen.dart';
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart'; import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
import 'package:flux/features/onboarding/ui/onboarding_screen.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/blocs/operation_files_bloc.dart';
import 'package:flux/features/operations/models/service_model.dart'; import 'package:flux/features/operations/models/operation_model.dart';
import 'package:flux/features/operations/ui/service_form_screen/service_form_screen.dart'; import 'package:flux/features/operations/ui/operation_form_screen/operation_form_screen.dart';
import 'package:flux/features/operations/ui/service_form_screen/service_mobile_upload_screen.dart'; import 'package:flux/features/operations/ui/operation_form_screen/operation_mobile_upload_screen.dart';
import 'package:flux/features/operations/ui/services_screen.dart'; import 'package:flux/features/operations/ui/operations_screen.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -132,7 +132,7 @@ class AppRouter {
), ),
GoRoute( GoRoute(
path: '/operations', path: '/operations',
builder: (context, state) => const ServicesScreen(), builder: (context, state) => const OperationsScreen(),
), ),
GoRoute( GoRoute(
path: '/customers', path: '/customers',
@@ -171,14 +171,15 @@ class AppRouter {
path: '/operation-form', path: '/operation-form',
name: 'operation-form', name: 'operation-form',
builder: (context, state) { builder: (context, state) {
final existingService = state.extra as ServiceModel?; final existingOperation = state.extra as OperationModel?;
final serviceId = state.uri.queryParameters['serviceId']; final operationId = state.uri.queryParameters['operationId'];
return BlocProvider( return BlocProvider(
create: (context) => create: (context) => OperationFilesBloc(
ServiceFilesBloc(serviceId: serviceId ?? existingService?.id), operationId: operationId ?? existingOperation?.id,
child: ServiceFormScreen( ),
serviceId: serviceId ?? existingService?.id, child: OperationFormScreen(
existingService: existingService, operationId: operationId ?? existingOperation?.id,
existingOperation: existingOperation,
), ),
); );
}, },
@@ -186,13 +187,14 @@ class AppRouter {
GoRoute( GoRoute(
path: '/operation/:id/upload', path: '/operation/:id/upload',
builder: (context, state) { builder: (context, state) {
final serviceId = state.pathParameters['id']!; final operationId = state.pathParameters['id']!;
final serviceName = state.uri.queryParameters['name'] ?? 'Pratica'; final operationName =
state.uri.queryParameters['name'] ?? 'Pratica';
return BlocProvider( return BlocProvider(
create: (context) => ServiceFilesBloc(serviceId: serviceId), create: (context) => OperationFilesBloc(operationId: operationId),
child: ServiceMobileUploadScreen( child: OperationMobileUploadScreen(
serviceId: serviceId, operationId: operationId,
serviceName: serviceName, operationName: operationName,
), ),
); );
}, },

View File

@@ -1,6 +1,6 @@
// lib/ui/common/flux_text_field.dart // lib/ui/common/flux_text_field.dart
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/operations.dart'; import 'package:flutter/services.dart';
import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
class FluxTextField extends StatefulWidget { class FluxTextField extends StatefulWidget {

View File

@@ -263,7 +263,7 @@ extension CompanyLimits on CompanyModel {
} }
} }
int get maxServicesPerMonth { int get maxOperationsPerMonth {
switch (subscriptionTier) { switch (subscriptionTier) {
case SubscriptionTier.free: case SubscriptionTier.free:
return 50; return 50;

View File

@@ -91,7 +91,7 @@ class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
child: IconButton( child: IconButton(
icon: const Icon(Icons.person_add), icon: const Icon(Icons.person_add),
onPressed: () async { onPressed: () async {
final servicesCubit = context.read<ServicesCubit>(); final operationsCubit = context.read<OperationsCubit>();
// Apriamo la dialog passando la query attuale // Apriamo la dialog passando la query attuale
final CustomerModel? nuovoCliente = await showDialog( final CustomerModel? nuovoCliente = await showDialog(
context: context, context: context,
@@ -101,7 +101,7 @@ class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
); );
if (nuovoCliente != null) { if (nuovoCliente != null) {
servicesCubit.updateField( operationsCubit.updateField(
customerId: nuovoCliente.id, customerId: nuovoCliente.id,
customerDisplayName: nuovoCliente.nome, customerDisplayName: nuovoCliente.nome,
); );
@@ -180,7 +180,7 @@ class _CustomerSearchSheetState extends State<CustomerSearchSheet> {
), ),
onTap: () { onTap: () {
// Salviamo l'ID e il nome formattato nel form dei servizi // Salviamo l'ID e il nome formattato nel form dei servizi
context.read<ServicesCubit>().updateField( context.read<OperationsCubit>().updateField(
customerId: customer.id, customerId: customer.id,
customerDisplayName: displayName, customerDisplayName: displayName,
); );

View File

@@ -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(),
),
);
}
});
}
}

View File

@@ -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];
}

View File

@@ -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,
);
}
}

View File

@@ -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,
),
),
],
),
),
);
},
);
},
),
),
],
),
),
);
}
}

View File

@@ -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(),
),
);
}
});
}
}

View File

@@ -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];
}

View File

@@ -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,
);
}
}

View File

@@ -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,
),
),
],
),
),
);
},
);
},
),
),
],
),
),
);
}
}

View File

@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/core/utils/extensions.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/home/ui/quick_actions_widget.dart';
import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart'; import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -76,10 +76,10 @@ class HomeScreen extends StatelessWidget {
color: Colors.green, color: Colors.green,
context: context, context: context,
), ),
LatestStoreServicesCard(), LatestStoreOperationsCard(),
_buildDashboardWidget( _buildDashboardWidget(
title: context.l10n.homeLatestServiceTickets, title: context.l10n.homeLatestOperationTickets,
icon: Icons.support_agent_outlined, icon: Icons.support_agent_outlined,
color: Colors.purple, color: Colors.purple,
context: context, context: context,
@@ -181,7 +181,7 @@ class HomeScreen extends StatelessWidget {
children: [ children: [
QuickActionButton( QuickActionButton(
icon: Icons.add, icon: Icons.add,
label: context.l10n.commonService, label: context.l10n.commonOperation,
color: Colors.blue, color: Colors.blue,
onTap: () { onTap: () {
// Entriamo nel form! Nessun parametro extra = Nuovo Servizio // Entriamo nel form! Nessun parametro extra = Nuovo Servizio
@@ -191,7 +191,7 @@ class HomeScreen extends StatelessWidget {
const SizedBox(width: 12), const SizedBox(width: 12),
QuickActionButton( QuickActionButton(
icon: Icons.handyman, icon: Icons.handyman,
label: context.l10n.homeNewServiceTicket, label: context.l10n.homeNewOperationTicket,
color: Colors.redAccent, color: Colors.redAccent,
onTap: () { onTap: () {
// TODO: Quando avrai la rotta per la nuova assistenza // TODO: Quando avrai la rotta per la nuova assistenza

View File

@@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; 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:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/core/widgets/flux_text_field.dart';
import 'package:flux/features/master_data/store/models/store_model.dart'; import 'package:flux/features/master_data/store/models/store_model.dart';

View 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));
}
}

View 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);
}

View 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,
);
}
}

View File

@@ -4,50 +4,51 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart'; import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/utils/extensions.dart';
import 'package:flux/features/operations/data/services_repository.dart'; import 'package:flux/features/operations/data/operations_repository.dart';
import 'package:flux/features/operations/models/energy_service_model.dart'; import 'package:flux/features/operations/models/energy_operation_model.dart';
import 'package:flux/features/operations/models/entertainment_service_model.dart'; import 'package:flux/features/operations/models/entertainment_operation_model.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/operations/models/service_file_model.dart'; import 'package:flux/features/operations/models/operation_file_model.dart';
import 'package:flux/features/operations/models/service_model.dart'; import 'package:flux/features/operations/models/operation_model.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
part 'services_state.dart'; part 'operations_state.dart';
class ServicesCubit extends Cubit<ServicesState> { class OperationsCubit extends Cubit<OperationsState> {
final ServicesRepository _repository = GetIt.I<ServicesRepository>(); final OperationsRepository _repository = GetIt.I<OperationsRepository>();
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>(); final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
ServicesCubit() : super(const ServicesState(status: ServicesStatus.initial)); OperationsCubit()
: super(const OperationsState(status: OperationsStatus.initial));
// --- CARICAMENTO E PAGINAZIONE --- // --- CARICAMENTO E PAGINAZIONE ---
Future<void> loadServices({bool refresh = false}) async { Future<void> loadOperations({bool refresh = false}) async {
// Se stiamo già caricando, evitiamo chiamate doppie // 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 // Se non è un refresh e abbiamo già raggiunto la fine dei dati, ci fermiamo
if (!refresh && state.hasReachedMax) return; if (!refresh && state.hasReachedMax) return;
emit( emit(
state.copyWith( state.copyWith(
status: ServicesStatus.loading, status: OperationsStatus.loading,
errorMessage: null, errorMessage: null,
// Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading // Se è un refresh, svuotiamo la lista attuale per mostrare lo shimmer/loading
allServices: refresh ? [] : state.allServices, allOperations: refresh ? [] : state.allOperations,
hasReachedMax: refresh ? false : state.hasReachedMax, hasReachedMax: refresh ? false : state.hasReachedMax,
), ),
); );
try { try {
final currentOffset = refresh ? 0 : state.allServices.length; final currentOffset = refresh ? 0 : state.allOperations.length;
final companyId = _sessionCubit.state.company?.id; final companyId = _sessionCubit.state.company?.id;
if (companyId == null) { if (companyId == null) {
throw Exception("Company ID non trovato nella sessione"); throw Exception("Company ID non trovato nella sessione");
} }
final newServices = await _repository.fetchServices( final newOperations = await _repository.fetchOperations(
companyId: companyId, companyId: companyId,
offset: currentOffset, offset: currentOffset,
limit: 50, 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 // 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( emit(
state.copyWith( state.copyWith(
status: ServicesStatus.ready, status: OperationsStatus.ready,
allServices: refresh allOperations: refresh
? newServices ? newOperations
: [...state.allServices, ...newServices], : [...state.allOperations, ...newOperations],
hasReachedMax: reachedMax, hasReachedMax: reachedMax,
), ),
); );
} catch (e) { } catch (e) {
emit( emit(
state.copyWith( state.copyWith(
status: ServicesStatus.failure, status: OperationsStatus.failure,
errorMessage: "Errore nel caricamento servizi: $e", errorMessage: "Errore nel caricamento servizi: $e",
), ),
); );
@@ -87,51 +88,51 @@ class ServicesCubit extends Cubit<ServicesState> {
dateRange: range ?? state.dateRange, dateRange: range ?? state.dateRange,
), ),
); );
loadServices(refresh: true); loadOperations(refresh: true);
} }
/// Pulisce tutti i filtri /// Pulisce tutti i filtri
void clearFilters() { void clearFilters() {
emit(state.copyWith(query: '', dateRange: null)); emit(state.copyWith(query: '', dateRange: null));
loadServices(refresh: true); loadOperations(refresh: true);
} }
// --- GESTIONE BOZZA (DRAFT) --- // --- GESTIONE BOZZA (DRAFT) ---
/// Inizializza un nuovo servizio o ne carica uno esistente per la modifica /// Inizializza un nuovo servizio o ne carica uno esistente per la modifica
void initServiceForm({ void initOperationForm({
ServiceModel? existingService, OperationModel? existingOperation,
String? serviceId, String? operationId,
}) async { }) async {
if (existingService != null) { if (existingOperation != null) {
emit( emit(
state.copyWith( state.copyWith(
currentService: existingService, currentOperation: existingOperation,
status: ServicesStatus.ready, status: OperationsStatus.ready,
), ),
); );
} else if (serviceId != null) { } else if (operationId != null) {
ServiceModel? serviceModel = state.allServices.firstWhereOrNull( OperationModel? operationModel = state.allOperations.firstWhereOrNull(
(s) => s.id == serviceId, (s) => s.id == operationId,
); );
serviceModel ??= await _repository.fetchServiceById(serviceId); operationModel ??= await _repository.fetchOperationById(operationId);
emit( emit(
state.copyWith( state.copyWith(
currentService: serviceModel, currentOperation: operationModel,
status: ServicesStatus.ready, status: OperationsStatus.ready,
), ),
); );
} else { } else {
// Crea un template vuoto con lo store di default (se disponibile) // Crea un template vuoto con lo store di default (se disponibile)
emit( emit(
state.copyWith( state.copyWith(
currentService: ServiceModel( currentOperation: OperationModel(
storeId: _sessionCubit.state.currentStore?.id ?? '', storeId: _sessionCubit.state.currentStore?.id ?? '',
number: '', // Sarà compilato dall'utente number: '', // Sarà compilato dall'utente
createdAt: DateTime.now(), createdAt: DateTime.now(),
companyId: _sessionCubit.state.company!.id!, companyId: _sessionCubit.state.company!.id!,
), ),
status: ServicesStatus.ready, status: OperationsStatus.ready,
), ),
); );
} }
@@ -151,9 +152,9 @@ class ServicesCubit extends Cubit<ServicesState> {
String? customerId, String? customerId,
String? customerDisplayName, String? customerDisplayName,
}) { }) {
if (state.currentService == null) return; if (state.currentOperation == null) return;
final updated = state.currentService!.copyWith( final updated = state.currentOperation!.copyWith(
al: al, al: al,
mnp: mnp, mnp: mnp,
nip: nip, nip: nip,
@@ -167,34 +168,38 @@ class ServicesCubit extends Cubit<ServicesState> {
customerDisplayName: customerDisplayName, customerDisplayName: customerDisplayName,
); );
emit(state.copyWith(currentService: updated)); emit(state.copyWith(currentOperation: updated));
} }
// --- GESTIONE MODULI COMPLESSI --- // --- GESTIONE MODULI COMPLESSI ---
void updateEnergyServices(List<EnergyServiceModel> energyList) { void updateEnergyOperations(List<EnergyOperationModel> energyList) {
emit( emit(
state.copyWith( state.copyWith(
currentService: state.currentService?.copyWith( currentOperation: state.currentOperation?.copyWith(
energyServices: energyList, energyOperations: energyList,
), ),
), ),
); );
} }
void updateFinServices(List<FinServiceModel> finList) { void updateFinOperations(List<FinOperationModel> finList) {
emit( emit(
state.copyWith( 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( emit(
state.copyWith( state.copyWith(
currentService: state.currentService?.copyWith( currentOperation: state.currentOperation?.copyWith(
entertainmentServices: entList, entertainmentOperations: entList,
), ),
), ),
); );
@@ -202,36 +207,40 @@ class ServicesCubit extends Cubit<ServicesState> {
// --- PERSISTENZA --- // --- PERSISTENZA ---
Future<void> saveCurrentService({ Future<void> saveCurrentOperation({
required bool isBozza, required bool isBozza,
bool shouldPop = true, bool shouldPop = true,
List<ServiceFileModel>? files, List<OperationFileModel>? files,
}) async { }) 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 { try {
// 1. Aggiorniamo il flag bozza in base a quale pulsante ha premuto l'utente // 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, isBozza: isBozza,
files: files, files: files,
); );
// 2. Salvataggio corazzato // 2. Salvataggio corazzato
final updatedService = await _repository.saveFullService(serviceToSave); final updatedOperation = await _repository.saveFullOperation(
operationToSave,
);
// 3. Reset e ricaricamento // 3. Reset e ricaricamento
emit( emit(
state.copyWith( state.copyWith(
status: shouldPop ? ServicesStatus.saved : ServicesStatus.savedNoPop, status: shouldPop
currentService: shouldPop ? null : updatedService, ? OperationsStatus.saved
: OperationsStatus.savedNoPop,
currentOperation: shouldPop ? null : updatedOperation,
), ),
); );
await loadServices(refresh: true); await loadOperations(refresh: true);
} catch (e) { } catch (e) {
emit( emit(
state.copyWith( state.copyWith(
status: ServicesStatus.failure, status: OperationsStatus.failure,
errorMessage: e.toString(), errorMessage: e.toString(),
), ),
); );
@@ -242,9 +251,9 @@ class ServicesCubit extends Cubit<ServicesState> {
void addAttachments(List<PlatformFile> files) { void addAttachments(List<PlatformFile> files) {
final newAttachments = files.map((file) { final newAttachments = files.map((file) {
return ServiceFileModel( return OperationFileModel(
id: null, // Meglio null se non è su DB id: null, // Meglio null se non è su DB
serviceId: state.currentService?.id ?? '', operationId: state.currentOperation?.id ?? '',
name: file.name.fileNameWithoutExtension(), name: file.name.fileNameWithoutExtension(),
extension: file.name.fileExtension(), extension: file.name.fileExtension(),
storagePath: '', storagePath: '',
@@ -255,44 +264,46 @@ class ServicesCubit extends Cubit<ServicesState> {
}).toList(); }).toList();
// Creiamo una nuova lista pulita // Creiamo una nuova lista pulita
final List<ServiceFileModel> updatedList = [ final List<OperationFileModel> updatedList = [
...(state.currentService?.files ?? []), ...(state.currentOperation?.files ?? []),
...newAttachments, ...newAttachments,
]; ];
// Emettiamo lo stato assicurandoci che il ServiceModel venga clonato // Emettiamo lo stato assicurandoci che il OperationModel venga clonato
if (state.currentService != null) { if (state.currentOperation != null) {
emit( emit(
state.copyWith( state.copyWith(
currentService: state.currentService!.copyWith(files: updatedList), currentOperation: state.currentOperation!.copyWith(
files: updatedList,
),
), ),
); );
} }
} }
void removeAttachment(int index) { void removeAttachment(int index) {
if (state.currentService == null) return; if (state.currentOperation == null) return;
final updatedList = List<ServiceFileModel>.from( final updatedList = List<OperationFileModel>.from(
state.currentService!.files, state.currentOperation!.files,
); );
updatedList.removeAt(index); updatedList.removeAt(index);
emit( emit(
state.copyWith( state.copyWith(
currentService: state.currentService?.copyWith(files: updatedList), currentOperation: state.currentOperation?.copyWith(files: updatedList),
), ),
); );
} }
void saveAndCopyFileToCustomer(List<ServiceFileModel> selectedFiles) async { void saveAndCopyFileToCustomer(List<OperationFileModel> selectedFiles) async {
final currentService = state.currentService; final currentOperation = state.currentOperation;
// 1. Check di sicurezza: se non c'è il cliente, non sappiamo dove copiare // 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( emit(
state.copyWith( state.copyWith(
status: ServicesStatus.failure, status: OperationsStatus.failure,
errorMessage: errorMessage:
"Impossibile copiare: nessun cliente associato alla pratica.", "Impossibile copiare: nessun cliente associato alla pratica.",
), ),
@@ -300,19 +311,21 @@ class ServicesCubit extends Cubit<ServicesState> {
return; return;
} }
emit(state.copyWith(status: ServicesStatus.loading)); emit(state.copyWith(status: OperationsStatus.loading));
try { try {
// 2. SALVATAGGIO CORAZZATO // 2. SALVATAGGIO CORAZZATO
// Chiamiamo il repo e otteniamo la pratica con TUTTI i file ora dotati di ID e storagePath // 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 // 3. COPIA RELAZIONALE
// Per ogni file che l'utente ha selezionato nella UI, cerchiamo la sua versione // 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. // "ufficiale" (quella con lo storagePath) nel modello appena tornato dal DB.
for (var selectedFile in selectedFiles) { for (var selectedFile in selectedFiles) {
// Cerchiamo il match nel modello aggiornato // Cerchiamo il match nel modello aggiornato
final persistedFile = updatedService.files.firstWhere( final persistedFile = updatedOperation.files.firstWhere(
(f) => (f) =>
f.name == selectedFile.name && f.name == selectedFile.name &&
f.extension == selectedFile.extension, f.extension == selectedFile.extension,
@@ -324,7 +337,7 @@ class ServicesCubit extends Cubit<ServicesState> {
// Creiamo il link nel database del cliente // Creiamo il link nel database del cliente
await _repository.copyFileToCustomer( await _repository.copyFileToCustomer(
file: persistedFile, 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" // Aggiorniamo il Cubit con il servizio salvato così la UI mostra i file come "Remoti"
emit( emit(
state.copyWith( state.copyWith(
status: ServicesStatus.success, status: OperationsStatus.success,
currentService: updatedService, currentOperation: updatedOperation,
), ),
); );
} catch (e) { } catch (e) {
emit( emit(
state.copyWith( state.copyWith(
status: ServicesStatus.failure, status: OperationsStatus.failure,
errorMessage: "Errore durante il salvataggio e copia: $e", errorMessage: "Errore durante il salvataggio e copia: $e",
), ),
); );

View File

@@ -1,6 +1,6 @@
part of 'operations_cubit.dart'; part of 'operations_cubit.dart';
enum ServicesStatus { enum OperationsStatus {
initial, initial,
loading, loading,
ready, ready,
@@ -11,20 +11,20 @@ enum ServicesStatus {
failure, failure,
} }
class ServicesState extends Equatable { class OperationsState extends Equatable {
final ServicesStatus status; final OperationsStatus status;
final List<ServiceModel> allServices; final List<OperationModel> allOperations;
final ServiceModel? currentService; // La bozza che stiamo editando final OperationModel? currentOperation; // La bozza che stiamo editando
final String? errorMessage; final String? errorMessage;
final String query; final String query;
final DateTimeRange? dateRange; final DateTimeRange? dateRange;
final bool hasReachedMax; final bool hasReachedMax;
final bool isSavingDraft; final bool isSavingDraft;
const ServicesState({ const OperationsState({
required this.status, required this.status,
this.allServices = const [], this.allOperations = const [],
this.currentService, this.currentOperation,
this.errorMessage, this.errorMessage,
this.query = '', this.query = '',
this.dateRange, this.dateRange,
@@ -32,20 +32,20 @@ class ServicesState extends Equatable {
this.isSavingDraft = false, this.isSavingDraft = false,
}); });
ServicesState copyWith({ OperationsState copyWith({
ServicesStatus? status, OperationsStatus? status,
List<ServiceModel>? allServices, List<OperationModel>? allOperations,
ServiceModel? currentService, OperationModel? currentOperation,
String? errorMessage, String? errorMessage,
String? query, String? query,
DateTimeRange? dateRange, DateTimeRange? dateRange,
bool? hasReachedMax, bool? hasReachedMax,
bool? isSavingDraft, bool? isSavingDraft,
}) { }) {
return ServicesState( return OperationsState(
status: status ?? this.status, status: status ?? this.status,
allServices: allServices ?? this.allServices, allOperations: allOperations ?? this.allOperations,
currentService: currentService ?? this.currentService, currentOperation: currentOperation ?? this.currentOperation,
errorMessage: errorMessage, errorMessage: errorMessage,
query: query ?? this.query, query: query ?? this.query,
dateRange: dateRange ?? this.dateRange, dateRange: dateRange ?? this.dateRange,
@@ -57,8 +57,8 @@ class ServicesState extends Equatable {
@override @override
List<Object?> get props => [ List<Object?> get props => [
status, status,
allServices, allOperations,
currentService, currentOperation,
errorMessage, errorMessage,
query, query,
dateRange, dateRange,

View File

@@ -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));
}
}

View File

@@ -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);
}

View 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,
);
}
}

View File

@@ -4,40 +4,40 @@ import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/utils/extensions.dart';
import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/features/customers/models/customer_file_model.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:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.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 _supabase = Supabase.instance.client;
final companyId = GetIt.I.get<SessionCubit>().state.company!.id; final companyId = GetIt.I.get<SessionCubit>().state.company!.id;
final CustomerRepository _customerRepository = GetIt.I<CustomerRepository>(); final CustomerRepository _customerRepository = GetIt.I<CustomerRepository>();
// --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO --- // --- RECUPERO SINGOLO SERVIZIO CON JOIN COMPLETO ---
Future<ServiceModel> fetchServiceById(String id) async { Future<OperationModel> fetchOperationById(String id) async {
try { try {
final response = await _supabase final response = await _supabase
.from('operation') .from('operation')
.select(''' .select('''
*, *,
customer(nome), customer(nome),
energy_service(*), energy_operation(*),
fin_service(*), fin_operation(*),
entertainment_service(*), entertainment_operation(*),
service_file(*) operation_file(*)
''') ''')
.eq('id', id) .eq('id', id)
.single(); .single();
return ServiceModel.fromMap(response); return OperationModel.fromMap(response);
} catch (e) { } catch (e) {
throw Exception('Errore nel caricamento del servizio: $e'); throw Exception('Errore nel caricamento del servizio: $e');
} }
} }
// --- RECUPERO PAGINATO CON FILTRI E JOIN --- // --- RECUPERO PAGINATO CON FILTRI E JOIN ---
Future<List<ServiceModel>> fetchServices({ Future<List<OperationModel>> fetchOperations({
required String companyId, required String companyId,
required int offset, required int offset,
int limit = 50, int limit = 50,
@@ -51,10 +51,10 @@ class ServicesRepository {
.select(''' .select('''
*, *,
customer(nome), customer(nome),
energy_service(*), energy_operation(*),
fin_service(*), fin_operation(*),
entertainment_service(*), entertainment_operation(*),
service_file(*) operation_file(*)
''') ''')
.eq('company_id', companyId); .eq('company_id', companyId);
@@ -77,14 +77,14 @@ class ServicesRepository {
.range(offset, offset + limit - 1); .range(offset, offset + limit - 1);
return (response as List) return (response as List)
.map((map) => ServiceModel.fromMap(map)) .map((map) => OperationModel.fromMap(map))
.toList(); .toList();
} catch (e) { } catch (e) {
throw Exception('Errore nel caricamento servizi: $e'); throw Exception('Errore nel caricamento servizi: $e');
} }
} }
Stream<List<ServiceModel>> getLastStoreServicesStream({ Stream<List<OperationModel>> getLastStoreOperationsStream({
required String storeId, required String storeId,
required int limit, required int limit,
}) { }) {
@@ -96,32 +96,32 @@ class ServicesRepository {
.limit(limit) .limit(limit)
.map( .map(
(listOfMaps) => (listOfMaps) =>
listOfMaps.map((map) => ServiceModel.fromMap(map)).toList(), listOfMaps.map((map) => OperationModel.fromMap(map)).toList(),
); );
} }
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) --- // --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
Future<ServiceModel> saveFullService(ServiceModel operation) async { Future<OperationModel> saveFullOperation(OperationModel operation) async {
try { try {
// 1. Upsert del record principale // 1. Upsert del record principale
final serviceData = await _supabase final operationData = await _supabase
.from('operation') .from('operation')
.upsert(operation.toMap()) .upsert(operation.toMap())
.select() .select()
.single(); .single();
final String newId = serviceData['id']; final String newId = operationData['id'];
// 2. MODIFICA: Pulizia atomica dei figli // 2. MODIFICA: Pulizia atomica dei figli
// Se stiamo modificando (id != null), resettiamo le tabelle collegate // Se stiamo modificando (id != null), resettiamo le tabelle collegate
if (operation.id != null) { if (operation.id != null) {
await Future.wait([ await Future.wait([
_supabase.from('energy_service').delete().eq('service_id', newId), _supabase.from('energy_operation').delete().eq('operation_id', newId),
_supabase.from('fin_service').delete().eq('service_id', newId), _supabase.from('fin_operation').delete().eq('operation_id', newId),
_supabase _supabase
.from('entertainment_service') .from('entertainment_operation')
.delete() .delete()
.eq('service_id', newId), .eq('operation_id', newId),
// Aggiungi qui eventuali altre tabelle pivot o file // Aggiungi qui eventuali altre tabelle pivot o file
]); ]);
} }
@@ -129,37 +129,37 @@ class ServicesRepository {
// 3. Inserimento dei moduli in parallelo per velocità // 3. Inserimento dei moduli in parallelo per velocità
final List<Future> insertTasks = []; final List<Future> insertTasks = [];
if (operation.energyServices.isNotEmpty) { if (operation.energyOperations.isNotEmpty) {
insertTasks.add( insertTasks.add(
_supabase _supabase
.from('energy_service') .from('energy_operation')
.insert( .insert(
operation.energyServices operation.energyOperations
.map((item) => item.copyWith(serviceId: newId).toMap()) .map((item) => item.copyWith(operationId: newId).toMap())
.toList(), .toList(),
), ),
); );
} }
if (operation.finServices.isNotEmpty) { if (operation.finOperations.isNotEmpty) {
insertTasks.add( insertTasks.add(
_supabase _supabase
.from('fin_service') .from('fin_operation')
.insert( .insert(
operation.finServices operation.finOperations
.map((item) => item.copyWith(serviceId: newId).toMap()) .map((item) => item.copyWith(operationId: newId).toMap())
.toList(), .toList(),
), ),
); );
} }
if (operation.entertainmentServices.isNotEmpty) { if (operation.entertainmentOperations.isNotEmpty) {
insertTasks.add( insertTasks.add(
_supabase _supabase
.from('entertainment_service') .from('entertainment_operation')
.insert( .insert(
operation.entertainmentServices operation.entertainmentOperations
.map((item) => item.copyWith(serviceId: newId).toMap()) .map((item) => item.copyWith(operationId: newId).toMap())
.toList(), .toList(),
), ),
); );
@@ -186,7 +186,7 @@ class ServicesRepository {
: 'image/${file.extension}'; : 'image/${file.extension}';
final fileToSave = file.copyWith( final fileToSave = file.copyWith(
serviceId: newId, operationId: newId,
storagePath: storagePath, storagePath: storagePath,
); );
@@ -202,7 +202,7 @@ class ServicesRepository {
); );
// B. Inserimento riga nel DB relazionale // 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()); uploadTasks.add(uploadAndLink());
@@ -214,20 +214,20 @@ class ServicesRepository {
// 5. GRAN FINALE: RECUPERO DEL MODELLO COMPLETO E AGGIORNATO // 5. GRAN FINALE: RECUPERO DEL MODELLO COMPLETO E AGGIORNATO
// Interroghiamo Supabase per farci restituire la pratica con TUTTI gli ID generati // Interroghiamo Supabase per farci restituire la pratica con TUTTI gli ID generati
// (inclusi quelli della tabella service_file appena inseriti) // (inclusi quelli della tabella operation_file appena inseriti)
final updatedServiceData = await _supabase final updatedOperationData = await _supabase
.from('operation') .from('operation')
.select(''' .select('''
*, *,
energy_service(*), energy_operation(*),
fin_service(*), fin_operation(*),
entertainment_service(*), entertainment_operation(*),
service_file(*) operation_file(*)
''') ''')
.eq('id', newId) .eq('id', newId)
.single(); .single();
return ServiceModel.fromMap(updatedServiceData); return OperationModel.fromMap(updatedOperationData);
} catch (e) { } catch (e) {
// Qui potresti aggiungere una logica di "rollback manuale" se necessario // Qui potresti aggiungere una logica di "rollback manuale" se necessario
throw Exception('Errore durante il salvataggio corazzato: $e'); throw Exception('Errore durante il salvataggio corazzato: $e');
@@ -235,7 +235,7 @@ class ServicesRepository {
} }
// --- ELIMINAZIONE --- // --- ELIMINAZIONE ---
Future<void> deleteService(String id) async { Future<void> deleteOperation(String id) async {
try { try {
await _supabase.from('operation').delete().eq('id', id); await _supabase.from('operation').delete().eq('id', id);
} catch (e) { } catch (e) {
@@ -249,7 +249,7 @@ class ServicesRepository {
// Cerchiamo i tipi più frequenti associati ai servizi di questa company // Cerchiamo i tipi più frequenti associati ai servizi di questa company
// Nota: dobbiamo passare attraverso la tabella 'operation' per filtrare per company_id // Nota: dobbiamo passare attraverso la tabella 'operation' per filtrare per company_id
final response = await _supabase final response = await _supabase
.from('entertainment_service') .from('entertainment_operation')
.select('type, operation!inner(store!inner(company_id))') .select('type, operation!inner(store!inner(company_id))')
.eq('operation.store.company_id', companyId) .eq('operation.store.company_id', companyId)
.limit(100); // Prendiamo un campione .limit(100); // Prendiamo un campione
@@ -276,20 +276,20 @@ class ServicesRepository {
} }
/// Ascolta in tempo reale i file caricati per una pratica /// Ascolta in tempo reale i file caricati per una pratica
Stream<List<ServiceFileModel>> getServiceFilesStream(String serviceId) { Stream<List<OperationFileModel>> getOperationFilesStream(String operationId) {
return _supabase return _supabase
.from('service_file') .from('operation_file')
.stream(primaryKey: ['id']) .stream(primaryKey: ['id'])
.eq('service_id', serviceId) .eq('operation_id', operationId)
.order('created_at', ascending: false) .order('created_at', ascending: false)
.map( .map(
(listOfMaps) => (listOfMaps) =>
listOfMaps.map((map) => ServiceFileModel.fromMap(map)).toList(), listOfMaps.map((map) => OperationFileModel.fromMap(map)).toList(),
); );
} }
Future<ServiceFileModel> uploadAndRegisterServiceFile({ Future<OperationFileModel> uploadAndRegisterOperationFile({
required String serviceId, required String operationId,
required PlatformFile pickedFile, required PlatformFile pickedFile,
}) async { }) async {
final cleanFileName = pickedFile.name.replaceAll( final cleanFileName = pickedFile.name.replaceAll(
@@ -297,10 +297,10 @@ class ServicesRepository {
'_', '_',
); );
final storagePath = final storagePath =
'$companyId/operations/$serviceId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; '$companyId/operations/$operationId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
final int fileSize = pickedFile.size; final int fileSize = pickedFile.size;
final fileToSave = ServiceFileModel( final fileToSave = OperationFileModel(
serviceId: serviceId, operationId: operationId,
name: cleanFileName.fileNameWithoutExtension(), name: cleanFileName.fileNameWithoutExtension(),
extension: cleanFileName.fileExtension(), extension: cleanFileName.fileExtension(),
storagePath: storagePath, storagePath: storagePath,
@@ -327,19 +327,19 @@ class ServicesRepository {
} }
final response = await _supabase final response = await _supabase
.from('service_file') .from('operation_file')
.insert(fileToSave.toMap()) .insert(fileToSave.toMap())
.select() .select()
.single(); .single();
return ServiceFileModel.fromMap(response); return OperationFileModel.fromMap(response);
} catch (e) { } catch (e) {
throw 'Errore durante l\'upload: $e'; throw 'Errore durante l\'upload: $e';
} }
} }
Future<void> copyFileToCustomer({ Future<void> copyFileToCustomer({
required ServiceFileModel file, required OperationFileModel file,
required String customerId, required String customerId,
}) async { }) async {
CustomerFileModel fileToCopy = CustomerFileModel( CustomerFileModel fileToCopy = CustomerFileModel(
@@ -352,14 +352,17 @@ class ServicesRepository {
await _customerRepository.saveFileReference(fileToCopy); await _customerRepository.saveFileReference(fileToCopy);
} }
Future<void> deleteServiceFiles(List<ServiceFileModel> files) async { Future<void> deleteOperationFiles(List<OperationFileModel> files) async {
if (files.isEmpty) return; if (files.isEmpty) return;
// 1. Prepariamo le liste di ID e di Percorsi // 1. Prepariamo le liste di ID e di Percorsi
final List<String> idsToDelete = files.map((f) => f.id!).toList(); final List<String> idsToDelete = files.map((f) => f.id!).toList();
final List<String> storagePaths = files.map((f) => f.storagePath).toList(); final List<String> storagePaths = files.map((f) => f.storagePath).toList();
try { 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); await _supabase.storage.from('documents').remove(storagePaths);

View File

@@ -2,38 +2,38 @@ import 'package:equatable/equatable.dart';
enum EnergyType { luce, gas } // Mappa il tuo public.energy_type enum EnergyType { luce, gas } // Mappa il tuo public.energy_type
class EnergyServiceModel extends Equatable { class EnergyOperationModel extends Equatable {
final String? id; final String? id;
final DateTime? createdAt; final DateTime? createdAt;
final EnergyType type; final EnergyType type;
final DateTime expiration; final DateTime expiration;
final String providerId; final String providerId;
final String? serviceId; final String? operationId;
const EnergyServiceModel({ const EnergyOperationModel({
this.id, this.id,
this.createdAt, this.createdAt,
required this.type, required this.type,
required this.expiration, required this.expiration,
required this.providerId, required this.providerId,
this.serviceId, this.operationId,
}); });
EnergyServiceModel copyWith({ EnergyOperationModel copyWith({
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
EnergyType? type, EnergyType? type,
DateTime? expiration, DateTime? expiration,
String? providerId, String? providerId,
String? serviceId, String? operationId,
}) { }) {
return EnergyServiceModel( return EnergyOperationModel(
id: id ?? this.id, id: id ?? this.id,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
type: type ?? this.type, type: type ?? this.type,
expiration: expiration ?? this.expiration, expiration: expiration ?? this.expiration,
providerId: providerId ?? this.providerId, providerId: providerId ?? this.providerId,
serviceId: serviceId ?? this.serviceId, operationId: operationId ?? this.operationId,
); );
} }
@@ -44,11 +44,11 @@ class EnergyServiceModel extends Equatable {
type, type,
expiration, expiration,
providerId, providerId,
serviceId, operationId,
]; ];
factory EnergyServiceModel.fromMap(Map<String, dynamic> map) { factory EnergyOperationModel.fromMap(Map<String, dynamic> map) {
return EnergyServiceModel( return EnergyOperationModel(
id: map['id'], id: map['id'],
createdAt: map['created_at'] != null createdAt: map['created_at'] != null
? DateTime.parse(map['created_at']) ? DateTime.parse(map['created_at'])
@@ -56,7 +56,7 @@ class EnergyServiceModel extends Equatable {
type: map['type'] == 'gas' ? EnergyType.gas : EnergyType.luce, type: map['type'] == 'gas' ? EnergyType.gas : EnergyType.luce,
expiration: DateTime.parse(map['expiration']), expiration: DateTime.parse(map['expiration']),
providerId: map['provider_id'], 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' 'type': type.name, // .name trasforma l'enum in 'luce' o 'gas'
'expiration': expiration.toIso8601String(), 'expiration': expiration.toIso8601String(),
'provider_id': providerId, 'provider_id': providerId,
'service_id': serviceId, 'operation_id': operationId,
}; };
} }
} }

View File

@@ -1,40 +1,40 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class EntertainmentServiceModel extends Equatable { class EntertainmentOperationModel extends Equatable {
final String? id; final String? id;
final DateTime? createdAt; final DateTime? createdAt;
final String type; // es. Sky, DAZN, ecc. final String type; // es. Sky, DAZN, ecc.
final bool constrained; // Vincolato? final bool constrained; // Vincolato?
final DateTime constrainExpiration; final DateTime constrainExpiration;
final String? serviceId; final String? operationId;
final String? providerId; final String? providerId;
const EntertainmentServiceModel({ const EntertainmentOperationModel({
this.id, this.id,
this.createdAt, this.createdAt,
required this.type, required this.type,
required this.constrained, required this.constrained,
required this.constrainExpiration, required this.constrainExpiration,
this.serviceId, this.operationId,
this.providerId, this.providerId,
}); });
EntertainmentServiceModel copyWith({ EntertainmentOperationModel copyWith({
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
String? type, String? type,
bool? constrained, bool? constrained,
DateTime? constrainExpiration, DateTime? constrainExpiration,
String? serviceId, String? operationId,
String? providerId, String? providerId,
}) { }) {
return EntertainmentServiceModel( return EntertainmentOperationModel(
id: id ?? this.id, id: id ?? this.id,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
type: type ?? this.type, type: type ?? this.type,
constrained: constrained ?? this.constrained, constrained: constrained ?? this.constrained,
constrainExpiration: constrainExpiration ?? this.constrainExpiration, constrainExpiration: constrainExpiration ?? this.constrainExpiration,
serviceId: serviceId ?? this.serviceId, operationId: operationId ?? this.operationId,
providerId: providerId ?? this.providerId, providerId: providerId ?? this.providerId,
); );
} }
@@ -46,12 +46,12 @@ class EntertainmentServiceModel extends Equatable {
type, type,
constrained, constrained,
constrainExpiration, constrainExpiration,
serviceId, operationId,
providerId, providerId,
]; ];
factory EntertainmentServiceModel.fromMap(Map<String, dynamic> map) { factory EntertainmentOperationModel.fromMap(Map<String, dynamic> map) {
return EntertainmentServiceModel( return EntertainmentOperationModel(
id: map['id'], id: map['id'],
createdAt: map['created_at'] != null createdAt: map['created_at'] != null
? DateTime.parse(map['created_at']) ? DateTime.parse(map['created_at'])
@@ -59,7 +59,7 @@ class EntertainmentServiceModel extends Equatable {
type: map['type'], type: map['type'],
constrained: map['constrained'] ?? false, constrained: map['constrained'] ?? false,
constrainExpiration: DateTime.parse(map['constrain_expiration']), constrainExpiration: DateTime.parse(map['constrain_expiration']),
serviceId: map['service_id'], operationId: map['operation_id'],
providerId: map['provider_id'], providerId: map['provider_id'],
); );
} }
@@ -70,7 +70,7 @@ class EntertainmentServiceModel extends Equatable {
'type': type, 'type': type,
'constrained': constrained, 'constrained': constrained,
'constrain_expiration': constrainExpiration.toIso8601String(), 'constrain_expiration': constrainExpiration.toIso8601String(),
'service_id': serviceId, 'operation_id': operationId,
'provider_id': providerId, 'provider_id': providerId,
}; };
} }

View File

@@ -1,51 +1,51 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class FinServiceModel extends Equatable { class FinOperationModel extends Equatable {
final String? id; final String? id;
final DateTime? createdAt; final DateTime? createdAt;
final DateTime expiration; final DateTime expiration;
final String? serviceId; final String? operationId;
final String? modelId; // FK verso model (es. iPhone, Samsung, ecc.) final String? modelId; // FK verso model (es. iPhone, Samsung, ecc.)
final String? providerId; final String? providerId;
const FinServiceModel({ const FinOperationModel({
this.id, this.id,
this.createdAt, this.createdAt,
required this.expiration, required this.expiration,
this.serviceId, this.operationId,
this.modelId, this.modelId,
this.providerId, this.providerId,
}); });
FinServiceModel copyWith({ FinOperationModel copyWith({
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
DateTime? expiration, DateTime? expiration,
String? serviceId, String? operationId,
String? modelId, String? modelId,
String? providerId, String? providerId,
}) { }) {
return FinServiceModel( return FinOperationModel(
id: id ?? this.id, id: id ?? this.id,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
expiration: expiration ?? this.expiration, expiration: expiration ?? this.expiration,
serviceId: serviceId ?? this.serviceId, operationId: operationId ?? this.operationId,
modelId: modelId ?? this.modelId, modelId: modelId ?? this.modelId,
providerId: providerId ?? this.providerId, providerId: providerId ?? this.providerId,
); );
} }
@override @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) { factory FinOperationModel.fromMap(Map<String, dynamic> map) {
return FinServiceModel( return FinOperationModel(
id: map['id'], id: map['id'],
createdAt: map['created_at'] != null createdAt: map['created_at'] != null
? DateTime.parse(map['created_at']) ? DateTime.parse(map['created_at'])
: null, : null,
expiration: DateTime.parse(map['expiration']), expiration: DateTime.parse(map['expiration']),
serviceId: map['service_id'], operationId: map['operation_id'],
modelId: map['model_id'], modelId: map['model_id'],
providerId: map['provider_id'], providerId: map['provider_id'],
); );
@@ -55,7 +55,7 @@ class FinServiceModel extends Equatable {
return { return {
if (id != null) 'id': id, if (id != null) 'id': id,
'expiration': expiration.toIso8601String(), 'expiration': expiration.toIso8601String(),
'service_id': serviceId, 'operation_id': operationId,
'model_id': modelId, 'model_id': modelId,
'provider_id': providerId, 'provider_id': providerId,
}; };

View File

@@ -2,23 +2,23 @@ import 'dart:typed_data';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
class ServiceFileModel extends Equatable { class OperationFileModel extends Equatable {
final String? id; final String? id;
final DateTime? createdAt; final DateTime? createdAt;
final String name; final String name;
final String extension; final String extension;
final String storagePath; final String storagePath;
final String serviceId; final String operationId;
final int fileSize; final int fileSize;
final Uint8List? localBytes; final Uint8List? localBytes;
const ServiceFileModel({ const OperationFileModel({
this.id, this.id,
this.createdAt, this.createdAt,
required this.name, required this.name,
required this.extension, required this.extension,
required this.storagePath, required this.storagePath,
required this.serviceId, required this.operationId,
required this.fileSize, required this.fileSize,
this.localBytes, this.localBytes,
}); });
@@ -37,30 +37,30 @@ class ServiceFileModel extends Equatable {
bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf'; bool get isPdf => extension.toLowerCase().replaceAll('.', '') == 'pdf';
ServiceFileModel copyWith({ OperationFileModel copyWith({
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
String? name, String? name,
String? extension, String? extension,
String? storagePath, String? storagePath,
String? serviceId, String? operationId,
int? fileSize, int? fileSize,
Uint8List? localBytes, Uint8List? localBytes,
}) { }) {
return ServiceFileModel( return OperationFileModel(
id: id ?? this.id, id: id ?? this.id,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
name: name ?? this.name, name: name ?? this.name,
extension: extension ?? this.extension, extension: extension ?? this.extension,
storagePath: storagePath ?? this.storagePath, storagePath: storagePath ?? this.storagePath,
serviceId: serviceId ?? this.serviceId, operationId: operationId ?? this.operationId,
fileSize: fileSize ?? this.fileSize, fileSize: fileSize ?? this.fileSize,
localBytes: localBytes ?? this.localBytes, localBytes: localBytes ?? this.localBytes,
); );
} }
factory ServiceFileModel.fromMap(Map<String, dynamic> map) { factory OperationFileModel.fromMap(Map<String, dynamic> map) {
return ServiceFileModel( return OperationFileModel(
id: map['id'] as String, id: map['id'] as String,
createdAt: map['created_at'] != null createdAt: map['created_at'] != null
? DateTime.parse(map['created_at']) ? DateTime.parse(map['created_at'])
@@ -68,7 +68,7 @@ class ServiceFileModel extends Equatable {
name: map['name'] ?? '', name: map['name'] ?? '',
extension: map['extension'] ?? '', extension: map['extension'] ?? '',
storagePath: map['storage_path'] ?? '', storagePath: map['storage_path'] ?? '',
serviceId: map['service_id']?.toString() ?? '', operationId: map['operation_id']?.toString() ?? '',
fileSize: map['file_size'] is int fileSize: map['file_size'] is int
? map['file_size'] ? map['file_size']
: int.tryParse(map['file_size']?.toString() ?? '0') ?? 0, : int.tryParse(map['file_size']?.toString() ?? '0') ?? 0,
@@ -81,7 +81,7 @@ class ServiceFileModel extends Equatable {
'name': name, 'name': name,
'extension': extension, 'extension': extension,
'storage_path': storagePath, 'storage_path': storagePath,
'service_id': serviceId, 'operation_id': operationId,
'file_size': fileSize, 'file_size': fileSize,
}; };
} }
@@ -93,7 +93,7 @@ class ServiceFileModel extends Equatable {
name, name,
extension, extension,
storagePath, storagePath,
serviceId, operationId,
fileSize, fileSize,
localBytes, localBytes,
]; ];

View File

@@ -1,11 +1,11 @@
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/utils/extensions.dart';
import 'package:flux/features/operations/models/energy_service_model.dart'; import 'package:flux/features/operations/models/energy_operation_model.dart';
import 'package:flux/features/operations/models/entertainment_service_model.dart'; import 'package:flux/features/operations/models/entertainment_operation_model.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/operations/models/service_file_model.dart'; // <-- Aggiunto Import import 'package:flux/features/operations/models/operation_file_model.dart'; // <-- Aggiunto Import
class ServiceModel extends Equatable { class OperationModel extends Equatable {
final String? id; final String? id;
final DateTime? createdAt; final DateTime? createdAt;
final String storeId; final String storeId;
@@ -26,14 +26,14 @@ class ServiceModel extends Equatable {
final int telepass; final int telepass;
// Moduli (Liste) // Moduli (Liste)
final List<EnergyServiceModel> energyServices; final List<EnergyOperationModel> energyOperations;
final List<FinServiceModel> finServices; final List<FinOperationModel> finOperations;
final List<EntertainmentServiceModel> entertainmentServices; final List<EntertainmentOperationModel> entertainmentOperations;
// ALLEGATI (Aggiunto) // ALLEGATI (Aggiunto)
final List<ServiceFileModel> files; final List<OperationFileModel> files;
const ServiceModel({ const OperationModel({
this.id, this.id,
this.createdAt, this.createdAt,
required this.storeId, required this.storeId,
@@ -48,15 +48,15 @@ class ServiceModel extends Equatable {
this.nip = 0, this.nip = 0,
this.unica = 0, this.unica = 0,
this.telepass = 0, this.telepass = 0,
this.energyServices = const [], this.energyOperations = const [],
this.finServices = const [], this.finOperations = const [],
this.entertainmentServices = const [], this.entertainmentOperations = const [],
this.files = const [], // <-- Aggiunto default vuoto this.files = const [], // <-- Aggiunto default vuoto
this.customerDisplayName, this.customerDisplayName,
required this.companyId, required this.companyId,
}); });
ServiceModel copyWith({ OperationModel copyWith({
String? id, String? id,
DateTime? createdAt, DateTime? createdAt,
String? storeId, String? storeId,
@@ -71,14 +71,14 @@ class ServiceModel extends Equatable {
int? nip, int? nip,
int? unica, int? unica,
int? telepass, int? telepass,
List<EnergyServiceModel>? energyServices, List<EnergyOperationModel>? energyOperations,
List<FinServiceModel>? finServices, List<FinOperationModel>? finOperations,
List<EntertainmentServiceModel>? entertainmentServices, List<EntertainmentOperationModel>? entertainmentOperations,
List<ServiceFileModel>? files, // <-- Aggiunto List<OperationFileModel>? files, // <-- Aggiunto
String? customerDisplayName, String? customerDisplayName,
String? companyId, String? companyId,
}) { }) {
return ServiceModel( return OperationModel(
id: id ?? this.id, id: id ?? this.id,
createdAt: createdAt ?? this.createdAt, createdAt: createdAt ?? this.createdAt,
storeId: storeId ?? this.storeId, storeId: storeId ?? this.storeId,
@@ -93,10 +93,10 @@ class ServiceModel extends Equatable {
nip: nip ?? this.nip, nip: nip ?? this.nip,
unica: unica ?? this.unica, unica: unica ?? this.unica,
telepass: telepass ?? this.telepass, telepass: telepass ?? this.telepass,
energyServices: energyServices ?? this.energyServices, energyOperations: energyOperations ?? this.energyOperations,
finServices: finServices ?? this.finServices, finOperations: finOperations ?? this.finOperations,
entertainmentServices: entertainmentOperations:
entertainmentServices ?? this.entertainmentServices, entertainmentOperations ?? this.entertainmentOperations,
files: files ?? this.files, // <-- Aggiunto files: files ?? this.files, // <-- Aggiunto
customerDisplayName: customerDisplayName ?? this.customerDisplayName, customerDisplayName: customerDisplayName ?? this.customerDisplayName,
companyId: companyId ?? this.companyId, companyId: companyId ?? this.companyId,
@@ -119,16 +119,16 @@ class ServiceModel extends Equatable {
nip, nip,
unica, unica,
telepass, telepass,
energyServices, energyOperations,
finServices, finOperations,
entertainmentServices, entertainmentOperations,
files, // <-- Aggiunto files, // <-- Aggiunto
customerDisplayName, customerDisplayName,
companyId, companyId,
]; ];
factory ServiceModel.fromMap(Map<String, dynamic> map) { factory OperationModel.fromMap(Map<String, dynamic> map) {
return ServiceModel( return OperationModel(
id: map['id'].toString(), id: map['id'].toString(),
createdAt: map['created_at'] != null createdAt: map['created_at'] != null
? DateTime.parse(map['created_at']) ? DateTime.parse(map['created_at'])
@@ -147,26 +147,26 @@ class ServiceModel extends Equatable {
telepass: map['telepass'] ?? 0, telepass: map['telepass'] ?? 0,
// Estrazione sicura liste collegate // Estrazione sicura liste collegate
energyServices: energyOperations:
(map['energy_service'] as List?) (map['energy_operation'] as List?)
?.map((x) => EnergyServiceModel.fromMap(x)) ?.map((x) => EnergyOperationModel.fromMap(x))
.toList() ?? .toList() ??
const [], const [],
finServices: finOperations:
(map['fin_service'] as List?) (map['fin_operation'] as List?)
?.map((x) => FinServiceModel.fromMap(x)) ?.map((x) => FinOperationModel.fromMap(x))
.toList() ?? .toList() ??
const [], const [],
entertainmentServices: entertainmentOperations:
(map['entertainment_service'] as List?) (map['entertainment_operation'] as List?)
?.map((x) => EntertainmentServiceModel.fromMap(x)) ?.map((x) => EntertainmentOperationModel.fromMap(x))
.toList() ?? .toList() ??
const [], const [],
// I FILE! (Assicurati che la foreign key su Supabase usi esattamente questo nome) // I FILE! (Assicurati che la foreign key su Supabase usi esattamente questo nome)
files: files:
(map['service_file'] as List?) (map['operation_file'] as List?)
?.map((x) => ServiceFileModel.fromMap(x)) ?.map((x) => OperationFileModel.fromMap(x))
.toList() ?? .toList() ??
const [], const [],

View File

@@ -1,12 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class ServiceActionCard extends StatelessWidget { class OperationActionCard extends StatelessWidget {
final String title; final String title;
final IconData icon; final IconData icon;
final VoidCallback onTap; final VoidCallback onTap;
final Color color; final Color color;
final int count; final int count;
const ServiceActionCard({ const OperationActionCard({
super.key, super.key,
required this.title, required this.title,
required this.icon, required this.icon,

View File

@@ -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/image_viewer_widget.dart';
import 'package:flux/core/widgets/pdf_viewer_widget.dart'; import 'package:flux/core/widgets/pdf_viewer_widget.dart';
import 'package:flux/core/widgets/qr_upload_dialog.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/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 { class AttachmentsSection extends StatelessWidget {
const AttachmentsSection({super.key}); const AttachmentsSection({super.key});
@@ -22,27 +22,29 @@ class AttachmentsSection extends StatelessWidget {
); );
if (result != null && context.mounted) { if (result != null && context.mounted) {
context.read<ServiceFilesBloc>().add(AddServiceFilesEvent(result.files)); context.read<OperationFilesBloc>().add(
AddOperationFilesEvent(result.files),
);
} }
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ServiceFilesBloc serviceFilesBloc = BlocProvider.of<ServiceFilesBloc>( OperationFilesBloc operationFilesBloc = BlocProvider.of<OperationFilesBloc>(
context, context,
); );
return BlocListener<ServicesCubit, ServicesState>( return BlocListener<OperationsCubit, OperationsState>(
listenWhen: (previous, current) => listenWhen: (previous, current) =>
previous.currentService?.id == null && previous.currentOperation?.id == null &&
current.currentService?.id != null, current.currentOperation?.id != null,
listener: (context, state) { listener: (context, state) {
// FIGASSA! La pratica è stata salvata e ora ha un ID. // FIGASSA! La pratica è stata salvata e ora ha un ID.
// Diciamo al Bloc dei file di agganciarsi al database. // Diciamo al Bloc dei file di agganciarsi al database.
final newId = state.currentService!.id!; final newId = state.currentOperation!.id!;
context.read<ServiceFilesBloc>().add(ServiceSavedEvent(newId)); context.read<OperationFilesBloc>().add(OperationsavedEvent(newId));
}, },
child: BlocBuilder<ServiceFilesBloc, ServiceFilesState>( child: BlocBuilder<OperationFilesBloc, OperationFilesState>(
builder: (context, state) { builder: (context, state) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -125,8 +127,8 @@ class AttachmentsSection extends StatelessWidget {
final isSelected = state.selectedFiles.contains(file); final isSelected = state.selectedFiles.contains(file);
return GestureDetector( return GestureDetector(
onTap: () => serviceFilesBloc.add( onTap: () => operationFilesBloc.add(
ToggleServiceFileSelectionEvent(file), ToggleOperationFileSelectionEvent(file),
), ),
onDoubleTap: () => _handleDoubleClick(context, file), onDoubleTap: () => _handleDoubleClick(context, file),
child: Card( child: Card(
@@ -216,7 +218,7 @@ class AttachmentsSection extends StatelessWidget {
label: const Text("Elimina"), label: const Text("Elimina"),
onPressed: () { onPressed: () {
// Qui lancerai l'evento per eliminare i file selezionati! // Qui lancerai l'evento per eliminare i file selezionati!
// Es: serviceFilesBloc.add(DeleteSelectedFilesEvent()); // Es: operationFilesBloc.add(DeleteSelectedFilesEvent());
}, },
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
@@ -243,14 +245,14 @@ class AttachmentsSection extends StatelessWidget {
} }
Future<void> _handleGenerateQr(BuildContext context) async { Future<void> _handleGenerateQr(BuildContext context) async {
final cubit = context.read<ServicesCubit>(); final cubit = context.read<OperationsCubit>();
var currentService = cubit.state.currentService; var currentOperation = cubit.state.currentOperation;
// 1. CATTURIAMO IL BLOC MENTRE SIAMO ANCORA NELLA PAGINA // 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) // 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 // NIENTE BlocListener qui! Solo un semplice Dialog di conferma
final bool? confirm = await showDialog<bool>( final bool? confirm = await showDialog<bool>(
context: context, context: context,
@@ -275,42 +277,42 @@ class AttachmentsSection extends StatelessWidget {
if (confirm != true) return; // Utente ha annullato if (confirm != true) return; // Utente ha annullato
// Salviamo forzatamente in bozza // Salviamo forzatamente in bozza
await cubit.saveCurrentService( await cubit.saveCurrentOperation(
isBozza: true, isBozza: true,
shouldPop: false, shouldPop: false,
files: serviceFilesBloc.state.localFiles, files: operationFilesBloc.state.localFiles,
); );
// Recuperiamo il servizio aggiornato con l'ID! // 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!) // 3. MOSTRIAMO IL QR CODE (Con il Ponte e l'Auto-Chiusura!)
if (context.mounted) { if (context.mounted) {
final nomePratica = "Pratica ${currentService?.customerDisplayName ?? ''}" final nomePratica =
.trim(); "Pratica ${currentOperation?.customerDisplayName ?? ''}".trim();
showDialog( showDialog(
context: context, context: context,
builder: (dialogContext) => BlocProvider.value( builder: (dialogContext) => BlocProvider.value(
// INIETTIAMO IL BLOC NEL CONTESTO DEL DIALOG ALIENO // INIETTIAMO IL BLOC NEL CONTESTO DEL DIALOG ALIENO
value: serviceFilesBloc, value: operationFilesBloc,
// ORA METTIAMO L'AUTO-CHIUSURA SUL QR CODE! // ORA METTIAMO L'AUTO-CHIUSURA SUL QR CODE!
child: BlocListener<ServiceFilesBloc, ServiceFilesState>( child: BlocListener<OperationFilesBloc, OperationFilesState>(
listener: (context, state) { listener: (context, state) {
// Se arrivano file remoti e lo stato è success, chiudiamo il QR! // Se arrivano file remoti e lo stato è success, chiudiamo il QR!
// (Nota: usiamo dialogContext per assicurarci di chiudere il popup giusto) // (Nota: usiamo dialogContext per assicurarci di chiudere il popup giusto)
if (state.status == ServiceFilesStatus.success && if (state.status == OperationFilesStatus.success &&
state.remoteFiles.isNotEmpty) { state.remoteFiles.isNotEmpty) {
Navigator.of(dialogContext).pop(); Navigator.of(dialogContext).pop();
} }
}, },
child: QrUploadDialog( child: QrUploadDialog(
deepLinkUrl: deepLinkUrl:
'fluxapp:///operation/${currentService!.id}/upload?name=${Uri.encodeComponent(nomePratica)}', 'fluxapp:///operation/${currentOperation!.id}/upload?name=${Uri.encodeComponent(nomePratica)}',
title: 'Scatta per\n$nomePratica', title: 'Scatta per\n$nomePratica',
), ),
), ),
@@ -322,7 +324,7 @@ class AttachmentsSection extends StatelessWidget {
// --- LOGICA DI COPIA AL CLIENTE --- // --- LOGICA DI COPIA AL CLIENTE ---
void saveAndCopyFilesToCustomer( void saveAndCopyFilesToCustomer(
BuildContext context, BuildContext context,
List<ServiceFileModel> files, List<OperationFileModel> files,
) { ) {
showDialog( showDialog(
context: context, context: context,
@@ -341,7 +343,7 @@ class AttachmentsSection extends StatelessWidget {
onPressed: () { onPressed: () {
Navigator.pop(ctx); Navigator.pop(ctx);
// 1. Diciamo al Cubit di salvare in Bozza e fare la copia // 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"), child: const Text("Salva e Copia"),
), ),
@@ -351,7 +353,7 @@ class AttachmentsSection extends StatelessWidget {
} }
// --- LOGICA DI VISUALIZZAZIONE OVERLAY --- // --- LOGICA DI VISUALIZZAZIONE OVERLAY ---
void _handleDoubleClick(BuildContext context, ServiceFileModel file) { void _handleDoubleClick(BuildContext context, OperationFileModel file) {
showDialog( showDialog(
context: context, context: context,
barrierDismissible: true, barrierDismissible: true,

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flux/features/customers/ui/customer_search_sheet.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 { class CustomerSection extends StatelessWidget {
final ServiceModel operation; final OperationModel operation;
const CustomerSection({super.key, required this.operation}); const CustomerSection({super.key, required this.operation});

View File

@@ -2,32 +2,32 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/blocs/provider_cubit.dart';
import 'package:flux/features/master_data/providers/models/provider_model.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 { class EnergyOperationDialog extends StatefulWidget {
final List<EnergyServiceModel> initialServices; final List<EnergyOperationModel> initialOperations;
final String final String
currentStoreId; // Ci serve per sapere per quale negozio caricare i gestori currentStoreId; // Ci serve per sapere per quale negozio caricare i gestori
const EnergyServiceDialog({ const EnergyOperationDialog({
super.key, super.key,
required this.initialServices, required this.initialOperations,
required this.currentStoreId, required this.currentStoreId,
}); });
@override @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 // Lista temporanea per non "sporcare" il cubit finché non si preme Conferma
late List<EnergyServiceModel> _tempList; late List<EnergyOperationModel> _tempList;
bool _isAddingNew = false; bool _isAddingNew = false;
@override @override
void initState() { void initState() {
super.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! // Al caricamento della modale, chiediamo al Cubit di recuperare i gestori veri!
context.read<ProvidersCubit>().loadActiveProvidersForStore( context.read<ProvidersCubit>().loadActiveProvidersForStore(
widget.currentStoreId, widget.currentStoreId,
@@ -52,9 +52,9 @@ class _EnergyServiceDialogState extends State<EnergyServiceDialog> {
// Cambia vista in base al flag // Cambia vista in base al flag
child: _isAddingNew child: _isAddingNew
? _EnergyForm( ? _EnergyForm(
onSave: (newService) { onSave: (newOperation) {
setState(() { setState(() {
_tempList.add(newService); _tempList.add(newOperation);
_isAddingNew = false; // Torna alla lista _isAddingNew = false; // Torna alla lista
}); });
}, },
@@ -101,7 +101,7 @@ class _EnergyServiceDialogState extends State<EnergyServiceDialog> {
// VISTA 1: LA LISTA DEI CONTRATTI // VISTA 1: LA LISTA DEI CONTRATTI
// ========================================== // ==========================================
class _EnergyList extends StatelessWidget { class _EnergyList extends StatelessWidget {
final List<EnergyServiceModel> operations; final List<EnergyOperationModel> operations;
final List<ProviderModel> final List<ProviderModel>
activeProviders; // <--- NUOVO: La lista vera dal Cubit activeProviders; // <--- NUOVO: La lista vera dal Cubit
final Function(int) onDelete; final Function(int) onDelete;
@@ -193,7 +193,7 @@ class _EnergyList extends StatelessWidget {
// VISTA 2: IL FORM DI INSERIMENTO // VISTA 2: IL FORM DI INSERIMENTO
// ========================================== // ==========================================
class _EnergyForm extends StatefulWidget { class _EnergyForm extends StatefulWidget {
final Function(EnergyServiceModel) onSave; final Function(EnergyOperationModel) onSave;
final VoidCallback onCancel; final VoidCallback onCancel;
const _EnergyForm({required this.onSave, required this.onCancel}); const _EnergyForm({required this.onSave, required this.onCancel});
@@ -400,12 +400,12 @@ class _EnergyFormState extends State<_EnergyForm> {
(_selectedProviderId == null || _selectedExpiration == null) (_selectedProviderId == null || _selectedExpiration == null)
? null // Disabilitato se mancano dati obbligatori ? null // Disabilitato se mancano dati obbligatori
: () { : () {
final newService = EnergyServiceModel( final newOperation = EnergyOperationModel(
type: _selectedType, type: _selectedType,
expiration: _selectedExpiration!, expiration: _selectedExpiration!,
providerId: _selectedProviderId!, providerId: _selectedProviderId!,
); );
widget.onSave(newService); widget.onSave(newOperation);
}, },
child: const Text("Salva Contratto"), child: const Text("Salva Contratto"),
), ),

View File

@@ -3,34 +3,34 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.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/blocs/provider_cubit.dart';
import 'package:flux/features/master_data/providers/models/provider_model.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/data/operations_repository.dart';
import 'package:flux/features/operations/models/entertainment_service_model.dart'; import 'package:flux/features/operations/models/entertainment_operation_model.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
class EntertainmentServiceDialog extends StatefulWidget { class EntertainmentOperationDialog extends StatefulWidget {
final List<EntertainmentServiceModel> initialServices; final List<EntertainmentOperationModel> initialOperations;
final String currentStoreId; final String currentStoreId;
const EntertainmentServiceDialog({ const EntertainmentOperationDialog({
super.key, super.key,
required this.initialServices, required this.initialOperations,
required this.currentStoreId, required this.currentStoreId,
}); });
@override @override
State<EntertainmentServiceDialog> createState() => State<EntertainmentOperationDialog> createState() =>
_EntertainmentServiceDialogState(); _EntertainmentOperationDialogState();
} }
class _EntertainmentServiceDialogState class _EntertainmentOperationDialogState
extends State<EntertainmentServiceDialog> { extends State<EntertainmentOperationDialog> {
late List<EntertainmentServiceModel> _tempList; late List<EntertainmentOperationModel> _tempList;
bool _isAddingNew = false; bool _isAddingNew = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_tempList = List.from(widget.initialServices); _tempList = List.from(widget.initialOperations);
// Carichiamo i provider attivi per lo store corrente // Carichiamo i provider attivi per lo store corrente
context.read<ProvidersCubit>().loadActiveProvidersForStore( context.read<ProvidersCubit>().loadActiveProvidersForStore(
widget.currentStoreId, widget.currentStoreId,
@@ -57,8 +57,8 @@ class _EntertainmentServiceDialogState
child: _isAddingNew child: _isAddingNew
? _EntertainmentForm( ? _EntertainmentForm(
// Il form che abbiamo creato prima // Il form che abbiamo creato prima
onSave: (newService) => setState(() { onSave: (newOperation) => setState(() {
_tempList.add(newService); _tempList.add(newOperation);
_isAddingNew = false; _isAddingNew = false;
}), }),
onCancel: () => setState(() => _isAddingNew = false), onCancel: () => setState(() => _isAddingNew = false),
@@ -94,7 +94,7 @@ class _EntertainmentServiceDialogState
} }
class _EntertainmentList extends StatelessWidget { class _EntertainmentList extends StatelessWidget {
final List<EntertainmentServiceModel> operations; final List<EntertainmentOperationModel> operations;
final List<ProviderModel> allProviders; final List<ProviderModel> allProviders;
final Function(int) onDelete; final Function(int) onDelete;
final VoidCallback onAddTap; final VoidCallback onAddTap;
@@ -194,7 +194,7 @@ class _EntertainmentList extends StatelessWidget {
// ---ENTERTAINMENT FORM (MODALE)--- // ---ENTERTAINMENT FORM (MODALE)---
class _EntertainmentForm extends StatefulWidget { class _EntertainmentForm extends StatefulWidget {
final Function(EntertainmentServiceModel) onSave; final Function(EntertainmentOperationModel) onSave;
final VoidCallback onCancel; final VoidCallback onCancel;
const _EntertainmentForm({required this.onSave, required this.onCancel}); const _EntertainmentForm({required this.onSave, required this.onCancel});
@@ -280,7 +280,7 @@ class _EntertainmentFormState extends State<_EntertainmentForm> {
const SizedBox(height: 8), const SizedBox(height: 8),
// Suggerimenti rapidi (Chip) // Suggerimenti rapidi (Chip)
FutureBuilder<List<String>>( FutureBuilder<List<String>>(
future: GetIt.I<ServicesRepository>().fetchTopEntertainmentTypes( future: GetIt.I<OperationsRepository>().fetchTopEntertainmentTypes(
GetIt.I<SessionCubit>().state.company!.id!, GetIt.I<SessionCubit>().state.company!.id!,
), ),
builder: (context, snapshot) { builder: (context, snapshot) {
@@ -376,7 +376,7 @@ class _EntertainmentFormState extends State<_EntertainmentForm> {
(_selectedProviderId == null || _typeController.text.isEmpty) (_selectedProviderId == null || _typeController.text.isEmpty)
? null ? null
: () => widget.onSave( : () => widget.onSave(
EntertainmentServiceModel( EntertainmentOperationModel(
providerId: _selectedProviderId!, providerId: _selectedProviderId!,
type: _typeController.text, type: _typeController.text,
constrained: _isConstrained, constrained: _isConstrained,

View File

@@ -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/blocs/product_cubit.dart';
import 'package:flux/features/master_data/products/models/model_model.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/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'; import 'package:flux/features/master_data/providers/models/provider_model.dart';
// =========================================================================== // ===========================================================================
// DIALOG PRINCIPALE // DIALOG PRINCIPALE
// =========================================================================== // ===========================================================================
class FinanceServiceDialog extends StatefulWidget { class FinanceOperationDialog extends StatefulWidget {
final List<FinServiceModel> initialServices; final List<FinOperationModel> initialOperations;
final String currentStoreId; final String currentStoreId;
final ProductCubit productCubit; final ProductCubit productCubit;
const FinanceServiceDialog({ const FinanceOperationDialog({
super.key, super.key,
required this.initialServices, required this.initialOperations,
required this.currentStoreId, required this.currentStoreId,
required this.productCubit, required this.productCubit,
}); });
@override @override
State<FinanceServiceDialog> createState() => _FinanceServiceDialogState(); State<FinanceOperationDialog> createState() => _FinanceOperationDialogState();
} }
class _FinanceServiceDialogState extends State<FinanceServiceDialog> { class _FinanceOperationDialogState extends State<FinanceOperationDialog> {
late List<FinServiceModel> _tempList; late List<FinOperationModel> _tempList;
bool _isAddingNew = false; bool _isAddingNew = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_tempList = List.from(widget.initialServices); _tempList = List.from(widget.initialOperations);
// Carichiamo i dati necessari dai Cubit // Carichiamo i dati necessari dai Cubit
context.read<ProvidersCubit>().loadActiveProvidersForStore( context.read<ProvidersCubit>().loadActiveProvidersForStore(
widget.currentStoreId, widget.currentStoreId,
@@ -109,7 +109,7 @@ class _FinanceServiceDialogState extends State<FinanceServiceDialog> {
// VISTA LISTA (STORICA) // VISTA LISTA (STORICA)
// =========================================================================== // ===========================================================================
class _FinanceList extends StatelessWidget { class _FinanceList extends StatelessWidget {
final List<FinServiceModel> operations; final List<FinOperationModel> operations;
final List<ProviderModel> allProviders; final List<ProviderModel> allProviders;
final List<ModelModel> allModels; final List<ModelModel> allModels;
final Function(int) onDelete; final Function(int) onDelete;
@@ -221,7 +221,7 @@ class _FinanceList extends StatelessWidget {
// FORM CON OMNI-SEARCH // FORM CON OMNI-SEARCH
// =========================================================================== // ===========================================================================
class _FinanceForm extends StatefulWidget { class _FinanceForm extends StatefulWidget {
final Function(FinServiceModel) onSave; final Function(FinOperationModel) onSave;
final VoidCallback onCancel; final VoidCallback onCancel;
const _FinanceForm({required this.onSave, required this.onCancel}); const _FinanceForm({required this.onSave, required this.onCancel});
@@ -428,7 +428,7 @@ class _FinanceFormState extends State<_FinanceForm> {
: () { : () {
final now = DateTime.now(); final now = DateTime.now();
widget.onSave( widget.onSave(
FinServiceModel( FinOperationModel(
providerId: _selectedProviderId!, providerId: _selectedProviderId!,
modelId: _selectedModel!.id!, modelId: _selectedModel!.id!,
expiration: DateTime( expiration: DateTime(

View File

@@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/operations/blocs/operations_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';
class GeneralInfoSection extends StatelessWidget { class GeneralInfoSection extends StatelessWidget {
final ServiceModel operation; final OperationModel operation;
const GeneralInfoSection({super.key, required this.operation}); const GeneralInfoSection({super.key, required this.operation});
@override @override
@@ -44,7 +44,7 @@ class GeneralInfoSection extends StatelessWidget {
prefixIcon: Icon(Icons.phone), prefixIcon: Icon(Icons.phone),
), ),
onChanged: (val) { onChanged: (val) {
context.read<ServicesCubit>().updateField(number: val); context.read<OperationsCubit>().updateField(number: val);
}, },
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
@@ -63,7 +63,7 @@ class GeneralInfoSection extends StatelessWidget {
activeThumbColor: Colors.orange, activeThumbColor: Colors.orange,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
onChanged: (val) { 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, activeThumbColor: Colors.green,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
onChanged: (val) { onChanged: (val) {
context.read<ServicesCubit>().updateField(resultOk: val); context.read<OperationsCubit>().updateField(
resultOk: val,
);
}, },
), ),
), ),
@@ -100,7 +102,7 @@ class GeneralInfoSection extends StatelessWidget {
alignLabelWithHint: true, alignLabelWithHint: true,
), ),
onChanged: (val) { onChanged: (val) {
context.read<ServicesCubit>().updateField(note: val); context.read<OperationsCubit>().updateField(note: val);
}, },
), ),
], ],

View File

@@ -1,49 +1,49 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/operations/blocs/operations_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:flux/features/operations/ui/service_form_screen/attachment_section.dart'; import 'package:flux/features/operations/ui/operation_form_screen/attachment_section.dart';
import 'package:flux/features/operations/ui/service_form_screen/customer_section.dart'; import 'package:flux/features/operations/ui/operation_form_screen/customer_section.dart';
import 'package:flux/features/operations/ui/service_form_screen/general_info_section.dart'; import 'package:flux/features/operations/ui/operation_form_screen/general_info_section.dart';
import 'package:flux/features/operations/ui/service_form_screen/services_grid.dart'; import 'package:flux/features/operations/ui/operation_form_screen/operations_grid.dart';
class ServiceFormScreen extends StatefulWidget { class OperationFormScreen extends StatefulWidget {
final String? serviceId; final String? operationId;
final ServiceModel? existingService; // <-- AGGIUNTO final OperationModel? existingOperation; // <-- AGGIUNTO
const ServiceFormScreen({ const OperationFormScreen({
super.key, super.key,
this.serviceId, this.operationId,
this.existingService, // <-- AGGIUNTO this.existingOperation, // <-- AGGIUNTO
}); });
@override @override
State<ServiceFormScreen> createState() => _ServiceFormScreenState(); State<OperationFormScreen> createState() => _OperationFormScreenState();
} }
class _ServiceFormScreenState extends State<ServiceFormScreen> { class _OperationFormScreenState extends State<OperationFormScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
// Diamo in pasto al Cubit tutto quello che abbiamo! // Diamo in pasto al Cubit tutto quello che abbiamo!
context.read<ServicesCubit>().initServiceForm( context.read<OperationsCubit>().initOperationForm(
existingService: widget.existingService, existingOperation: widget.existingOperation,
serviceId: widget.serviceId, operationId: widget.operationId,
); );
}); });
} }
void _performSave(BuildContext context, {required bool isBozza}) { void _performSave(BuildContext context, {required bool isBozza}) {
FocusScope.of(context).unfocus(); FocusScope.of(context).unfocus();
context.read<ServicesCubit>().saveCurrentService(isBozza: isBozza); context.read<OperationsCubit>().saveCurrentOperation(isBozza: isBozza);
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocConsumer<ServicesCubit, ServicesState>( return BlocConsumer<OperationsCubit, OperationsState>(
listener: (context, state) { listener: (context, state) {
if (state.status == ServicesStatus.saved) { if (state.status == OperationsStatus.saved) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text("Pratica salvata con successo!"), content: Text("Pratica salvata con successo!"),
@@ -52,7 +52,7 @@ class _ServiceFormScreenState extends State<ServiceFormScreen> {
); );
Navigator.pop(context); Navigator.pop(context);
} }
if (state.status == ServicesStatus.failure) { if (state.status == OperationsStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
content: Text("Errore: ${state.errorMessage ?? ''}"), 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( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text("Pratica salvata con successo!"), content: Text("Pratica salvata con successo!"),
@@ -70,9 +70,9 @@ class _ServiceFormScreenState extends State<ServiceFormScreen> {
} }
}, },
builder: (context, state) { builder: (context, state) {
final operation = state.currentService; final operation = state.currentOperation;
final isSaving = state.status == ServicesStatus.saving; final isSaving = state.status == OperationsStatus.saving;
final isEditMode = widget.serviceId != null; final isEditMode = widget.operationId != null;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
@@ -120,7 +120,7 @@ class _ServiceFormScreenState extends State<ServiceFormScreen> {
GeneralInfoSection(operation: operation), GeneralInfoSection(operation: operation),
const SizedBox(height: 24), const SizedBox(height: 24),
ServicesGrid(operation: operation), OperationsGrid(operation: operation),
const SizedBox(height: 32), const SizedBox(height: 32),
AttachmentsSection(), AttachmentsSection(),

View File

@@ -3,24 +3,25 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:file_picker/file_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 { class OperationMobileUploadScreen extends StatefulWidget {
final String serviceId; final String operationId;
final String serviceName; final String operationName;
const ServiceMobileUploadScreen({ const OperationMobileUploadScreen({
super.key, super.key,
required this.serviceId, required this.operationId,
required this.serviceName, required this.operationName,
}); });
@override @override
State<ServiceMobileUploadScreen> createState() => State<OperationMobileUploadScreen> createState() =>
_ServiceMobileUploadScreenState(); _OperationMobileUploadScreenState();
} }
class _ServiceMobileUploadScreenState extends State<ServiceMobileUploadScreen> { class _OperationMobileUploadScreenState
extends State<OperationMobileUploadScreen> {
// 1. LA NOSTRA STAGING AREA (Il "Carrello") // 1. LA NOSTRA STAGING AREA (Il "Carrello")
final List<PlatformFile> _stagedFiles = []; final List<PlatformFile> _stagedFiles = [];
@@ -35,10 +36,10 @@ class _ServiceMobileUploadScreenState extends State<ServiceMobileUploadScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocListener<ServiceFilesBloc, ServiceFilesState>( return BlocListener<OperationFilesBloc, OperationFilesState>(
listener: (context, state) { listener: (context, state) {
// Quando il BLoC ci dice che ha finito l'upload (Success), chiudiamo la pagina! // 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( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( const SnackBar(
content: Text("Tutti i file caricati con successo! ✅"), content: Text("Tutti i file caricati con successo! ✅"),
@@ -46,7 +47,7 @@ class _ServiceMobileUploadScreenState extends State<ServiceMobileUploadScreen> {
); );
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
if (state.status == ServiceFilesStatus.failure) { if (state.status == OperationFilesStatus.failure) {
setState(() => _isUploading = false); setState(() => _isUploading = false);
ScaffoldMessenger.of( ScaffoldMessenger.of(
context, context,
@@ -55,7 +56,7 @@ class _ServiceMobileUploadScreenState extends State<ServiceMobileUploadScreen> {
}, },
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text("Upload Pratica:\n${widget.serviceName}"), title: Text("Upload Pratica:\n${widget.operationName}"),
automaticallyImplyLeading: !_isUploading, automaticallyImplyLeading: !_isUploading,
), ),
body: Stack( body: Stack(
@@ -294,8 +295,8 @@ class _ServiceMobileUploadScreenState extends State<ServiceMobileUploadScreen> {
// Diciamo al BLoC di caricare tutti i file. // Diciamo al BLoC di caricare tutti i file.
// Usiamo il tuo evento esistente per ogni file (il BLoC li metterà in coda) // Usiamo il tuo evento esistente per ogni file (il BLoC li metterà in coda)
final bloc = context.read<ServiceFilesBloc>(); final bloc = context.read<OperationFilesBloc>();
bloc.add(UploadMultipleServiceFilesEvent(_stagedFiles)); bloc.add(UploadMultipleOperationFilesEvent(_stagedFiles));
// N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"! // N.B: Il Navigator.pop() viene chiamato dal BlocListener in alto quando lo stato diventa "success"!
} }

View File

@@ -2,20 +2,20 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/master_data/products/blocs/product_cubit.dart'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
import 'package:flux/features/operations/blocs/operations_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/energy_operation_model.dart';
import 'package:flux/features/operations/models/entertainment_service_model.dart'; import 'package:flux/features/operations/models/entertainment_operation_model.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/operations/models/service_model.dart'; import 'package:flux/features/operations/models/operation_model.dart';
import 'package:flux/features/operations/ui/service_form_screen/action_card.dart'; import 'package:flux/features/operations/ui/operation_form_screen/action_card.dart';
import 'package:flux/features/operations/ui/service_form_screen/energy_service_dialog.dart'; import 'package:flux/features/operations/ui/operation_form_screen/energy_operation_dialog.dart';
import 'package:flux/features/operations/ui/service_form_screen/entertainment_service_card.dart'; import 'package:flux/features/operations/ui/operation_form_screen/entertainment_operation_card.dart';
import 'package:flux/features/operations/ui/service_form_screen/finance_service_dialog.dart'; import 'package:flux/features/operations/ui/operation_form_screen/finance_operation_dialog.dart';
import 'package:flux/features/operations/ui/service_form_screen/int_dialogs.dart'; // Assicurati di importare il modello import 'package:flux/features/operations/ui/operation_form_screen/int_dialogs.dart'; // Assicurati di importare il modello
class ServicesGrid extends StatelessWidget { class OperationsGrid extends StatelessWidget {
final ServiceModel operation; final OperationModel operation;
const ServicesGrid({super.key, required this.operation}); const OperationsGrid({super.key, required this.operation});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -60,7 +60,7 @@ class ServicesGrid extends StatelessWidget {
"AL", "AL",
operation.al, operation.al,
(val) => (val) =>
context.read<ServicesCubit>().updateField(al: val), context.read<OperationsCubit>().updateField(al: val),
), ),
), ),
ActionCard( ActionCard(
@@ -73,7 +73,7 @@ class ServicesGrid extends StatelessWidget {
"MNP", "MNP",
operation.mnp, operation.mnp,
(val) => (val) =>
context.read<ServicesCubit>().updateField(mnp: val), context.read<OperationsCubit>().updateField(mnp: val),
), ),
), ),
ActionCard( ActionCard(
@@ -86,7 +86,7 @@ class ServicesGrid extends StatelessWidget {
"NIP", "NIP",
operation.nip, operation.nip,
(val) => (val) =>
context.read<ServicesCubit>().updateField(nip: val), context.read<OperationsCubit>().updateField(nip: val),
), ),
), ),
ActionCard( ActionCard(
@@ -98,8 +98,9 @@ class ServicesGrid extends StatelessWidget {
context, context,
"Unica", "Unica",
operation.unica, operation.unica,
(val) => (val) => context.read<OperationsCubit>().updateField(
context.read<ServicesCubit>().updateField(unica: val), unica: val,
),
), ),
), ),
ActionCard( ActionCard(
@@ -111,7 +112,7 @@ class ServicesGrid extends StatelessWidget {
context, context,
"Telepass", "Telepass",
operation.telepass, operation.telepass,
(val) => context.read<ServicesCubit>().updateField( (val) => context.read<OperationsCubit>().updateField(
telepass: val, telepass: val,
), ),
), ),
@@ -120,23 +121,24 @@ class ServicesGrid extends StatelessWidget {
// --- MODULI COMPLESSI (Le liste) --- // --- MODULI COMPLESSI (Le liste) ---
ActionCard( ActionCard(
label: "Energia", label: "Energia",
count: operation.energyServices.length, count: operation.energyOperations.length,
icon: Icons.bolt, icon: Icons.bolt,
color: Colors.green, color: Colors.green,
onTap: () async { onTap: () async {
// Apriamo la modale e aspettiamo il risultato // Apriamo la modale e aspettiamo il risultato
final result = await showDialog<List<EnergyServiceModel>>( final result =
context: context, await showDialog<List<EnergyOperationModel>>(
builder: (context) => EnergyServiceDialog( context: context,
currentStoreId: operation.storeId, builder: (context) => EnergyOperationDialog(
initialServices: operation currentStoreId: operation.storeId,
.energyServices, // Passiamo la lista attuale initialOperations: operation
), .energyOperations, // Passiamo la lista attuale
); ),
);
// Se l'utente ha premuto "Conferma" e non "Annulla" o tap fuori // Se l'utente ha premuto "Conferma" e non "Annulla" o tap fuori
if (result != null && context.mounted) { if (result != null && context.mounted) {
context.read<ServicesCubit>().updateEnergyServices( context.read<OperationsCubit>().updateEnergyOperations(
result, result,
); );
} }
@@ -144,44 +146,47 @@ class ServicesGrid extends StatelessWidget {
), ),
ActionCard( ActionCard(
label: "Finanziam.", label: "Finanziam.",
count: operation.finServices.length, count: operation.finOperations.length,
icon: Icons.euro_symbol, icon: Icons.euro_symbol,
color: Colors.teal, color: Colors.teal,
onTap: () async { onTap: () async {
final result = await showDialog<List<FinServiceModel>>( final result = await showDialog<List<FinOperationModel>>(
context: context, context: context,
builder: (context) => FinanceServiceDialog( builder: (context) => FinanceOperationDialog(
productCubit: context.read<ProductCubit>(), productCubit: context.read<ProductCubit>(),
currentStoreId: operation.storeId, currentStoreId: operation.storeId,
initialServices: operation initialOperations: operation
.finServices, // Passiamo la lista attuale .finOperations, // Passiamo la lista attuale
), ),
); );
if (result != null && context.mounted) { if (result != null && context.mounted) {
context.read<ServicesCubit>().updateFinServices(result); context.read<OperationsCubit>().updateFinOperations(
result,
);
} }
}, },
), ),
ActionCard( ActionCard(
label: "Intratten.", label: "Intratten.",
count: operation.entertainmentServices.length, count: operation.entertainmentOperations.length,
icon: Icons.movie_filter_outlined, icon: Icons.movie_filter_outlined,
color: Colors.purple, color: Colors.purple,
onTap: () async { onTap: () async {
final result = final result =
await showDialog<List<EntertainmentServiceModel>>( await showDialog<List<EntertainmentOperationModel>>(
context: context, context: context,
builder: (context) => EntertainmentServiceDialog( builder: (context) => EntertainmentOperationDialog(
initialServices: operation.entertainmentServices, initialOperations:
operation.entertainmentOperations,
currentStoreId: operation.storeId, currentStoreId: operation.storeId,
), ),
); );
if (result != null && context.mounted) { if (result != null && context.mounted) {
context context
.read<ServicesCubit>() .read<OperationsCubit>()
.updateEntertainmentServices(result); .updateEntertainmentOperations(result);
} }
}, },
), ),

View File

@@ -1,19 +1,19 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/operations/blocs/operations_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:flux/features/operations/utils/service_actions.dart'; import 'package:flux/features/operations/utils/operation_actions.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
// Importa i tuoi modelli e cubit // Importa i tuoi modelli e cubit
class ServicesScreen extends StatefulWidget { class OperationsScreen extends StatefulWidget {
const ServicesScreen({super.key}); const OperationsScreen({super.key});
@override @override
State<ServicesScreen> createState() => _ServicesScreenState(); State<OperationsScreen> createState() => _OperationsScreenState();
} }
class _ServicesScreenState extends State<ServicesScreen> { class _OperationsScreenState extends State<OperationsScreen> {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
@override @override
@@ -22,12 +22,12 @@ class _ServicesScreenState extends State<ServicesScreen> {
// Agganciamo il listener per la paginazione (Scroll Infinito) // Agganciamo il listener per la paginazione (Scroll Infinito)
_scrollController.addListener(_onScroll); _scrollController.addListener(_onScroll);
// Carichiamo i servizi iniziali // Carichiamo i servizi iniziali
context.read<ServicesCubit>().loadServices(); context.read<OperationsCubit>().loadOperations();
} }
void _onScroll() { void _onScroll() {
if (_isBottom) { 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) { builder: (context, state) {
// 1. Stato di caricamento iniziale // 1. Stato di caricamento iniziale
if (state.status == ServicesStatus.loading && if (state.status == OperationsStatus.loading &&
state.allServices.isEmpty) { state.allOperations.isEmpty) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
// 2. Lista vuota // 2. Lista vuota
if (state.allServices.isEmpty) { if (state.allOperations.isEmpty) {
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -77,9 +77,9 @@ class _ServicesScreenState extends State<ServicesScreen> {
const Text("Nessuna pratica trovata."), const Text("Nessuna pratica trovata."),
const SizedBox(height: 10), const SizedBox(height: 10),
ElevatedButton( ElevatedButton(
onPressed: () => context.read<ServicesCubit>().loadServices( onPressed: () => context
refresh: true, .read<OperationsCubit>()
), .loadOperations(refresh: true),
child: const Text("Riprova"), child: const Text("Riprova"),
), ),
], ],
@@ -90,15 +90,15 @@ class _ServicesScreenState extends State<ServicesScreen> {
// 3. La Lista (con Pull-to-refresh) // 3. La Lista (con Pull-to-refresh)
return RefreshIndicator( return RefreshIndicator(
onRefresh: () => onRefresh: () =>
context.read<ServicesCubit>().loadServices(refresh: true), context.read<OperationsCubit>().loadOperations(refresh: true),
child: ListView.builder( child: ListView.builder(
controller: _scrollController, controller: _scrollController,
padding: const EdgeInsets.only(bottom: 80), // Spazio per il FAB padding: const EdgeInsets.only(bottom: 80), // Spazio per il FAB
itemCount: state.hasReachedMax itemCount: state.hasReachedMax
? state.allServices.length ? state.allOperations.length
: state.allServices.length + 1, : state.allOperations.length + 1,
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (index >= state.allServices.length) { if (index >= state.allOperations.length) {
return const Center( return const Center(
child: Padding( child: Padding(
padding: EdgeInsets.all(16.0), padding: EdgeInsets.all(16.0),
@@ -107,21 +107,21 @@ class _ServicesScreenState extends State<ServicesScreen> {
); );
} }
final operation = state.allServices[index]; final operation = state.allOperations[index];
return _buildServiceCard(context, operation); return _buildOperationCard(context, operation);
}, },
), ),
); );
}, },
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () => startNewService(context), onPressed: () => startNewOperation(context),
child: const Icon(Icons.add), child: const Icon(Icons.add),
), ),
); );
} }
Widget _buildServiceCard(BuildContext context, ServiceModel operation) { Widget _buildOperationCard(BuildContext context, OperationModel operation) {
return Card( return Card(
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
elevation: 2, elevation: 2,
@@ -164,11 +164,11 @@ class _ServicesScreenState extends State<ServicesScreen> {
children: [ children: [
if (operation.al > 0 || operation.mnp > 0) if (operation.al > 0 || operation.mnp > 0)
_miniBadge("📞 Tel", Colors.blue), _miniBadge("📞 Tel", Colors.blue),
if (operation.energyServices.isNotEmpty) if (operation.energyOperations.isNotEmpty)
_miniBadge("⚡ Energy", Colors.green), _miniBadge("⚡ Energy", Colors.green),
if (operation.finServices.isNotEmpty) if (operation.finOperations.isNotEmpty)
_miniBadge("💰 Fin", Colors.purple), _miniBadge("💰 Fin", Colors.purple),
if (operation.entertainmentServices.isNotEmpty) if (operation.entertainmentOperations.isNotEmpty)
_miniBadge("📺 Ent", Colors.red), _miniBadge("📺 Ent", Colors.red),
], ],
), ),
@@ -180,7 +180,7 @@ class _ServicesScreenState extends State<ServicesScreen> {
extra: operation, // <-- LA MAGIA È QUI: Passa l'oggetto intero! extra: operation, // <-- LA MAGIA È QUI: Passa l'oggetto intero!
// Teniamo anche il parametro URL per coerenza di routing // Teniamo anche il parametro URL per coerenza di routing
queryParameters: operation.id != null queryParameters: operation.id != null
? {'serviceId': operation.id!} ? {'operationId': operation.id!}
: {}, : {},
), ),
), ),

View File

@@ -3,11 +3,11 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.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/master_data/store/bloc/store_cubit.dart';
import 'package:flux/features/operations/blocs/operations_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'; import 'package:go_router/go_router.dart';
/// Avvia la creazione di un nuovo servizio partendo dalla selezione dell'operatore. /// 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 session = context.read<SessionCubit>().state;
final currentStoreId = session.currentStore?.id; final currentStoreId = session.currentStore?.id;
@@ -53,8 +53,8 @@ void startNewService(BuildContext context) {
title: Text(member.name), title: Text(member.name),
onTap: () { onTap: () {
// 1. Inizializza il form nel Cubit // 1. Inizializza il form nel Cubit
context.read<ServicesCubit>().initServiceForm( context.read<OperationsCubit>().initOperationForm(
existingService: ServiceModel( existingOperation: OperationModel(
storeId: currentStoreId, storeId: currentStoreId,
employeeId: member.id, employeeId: member.id,
number: '', number: '',

View File

@@ -1,13 +1,13 @@
{ {
"@@locale": "en", "@@locale": "en",
"welcomeBack": "Welcome back, {name}! 👋", "welcomeBack": "Welcome back, {name}! 👋",
"latestServices": "Latest Operations", "latestOperations": "Latest Operations",
"masterData": "Master Data", "masterData": "Master Data",
"settings": "Settings", "settings": "Settings",
"newService": "Operation", "newOperation": "Operation",
"expiring_contracts": "Expiring Contracts", "expiring_contracts": "Expiring Contracts",
"sticky_notes": "Sticky Notes", "sticky_notes": "Sticky Notes",
"my_tasks": "My Tasks", "my_tasks": "My Tasks",
"latest_service_tickets": "Latest operation tickets" "latest_operation_tickets": "Latest operation tickets"
} }

View File

@@ -50,15 +50,15 @@
"commonNewPassword": "Nuova Password", "commonNewPassword": "Nuova Password",
"commonNote": "Nota", "commonNote": "Nota",
"commonSave": "Salva", "commonSave": "Salva",
"commonService": "Servizio", "commonOperation": "Servizio",
"commonSettings": "Impostazioni", "commonSettings": "Impostazioni",
"commonStickyNotes": "Sticky Notes", "commonStickyNotes": "Sticky Notes",
"commonTask": "Attività", "commonTask": "Attività",
"homeExpiringContracts": "Contratti in scadenza", "homeExpiringContracts": "Contratti in scadenza",
"homeLatestServiceTickets": "Ultime assistenze", "homeLatestOperationTickets": "Ultime assistenze",
"homeLatestServices": "Ultimi Servizi", "homeLatestOperations": "Ultimi Servizi",
"homeMyTasks": "Mie Attività", "homeMyTasks": "Mie Attività",
"homeNewServiceTicket": "Nuova assistenza", "homeNewOperationTicket": "Nuova assistenza",
"homeNoStoreFound": "Nessun negozio trovato", "homeNoStoreFound": "Nessun negozio trovato",
"homeWelcomeBack": "Bentornato, {name}! 👋", "homeWelcomeBack": "Bentornato, {name}! 👋",
"imageViewerWidgetErrorOpening": "Errore durante l'apertura dell'immagine", "imageViewerWidgetErrorOpening": "Errore durante l'apertura dell'immagine",

View File

@@ -220,11 +220,11 @@ abstract class AppLocalizations {
/// **'Salva'** /// **'Salva'**
String get commonSave; String get commonSave;
/// No description provided for @commonService. /// No description provided for @commonOperation.
/// ///
/// In it, this message translates to: /// In it, this message translates to:
/// **'Servizio'** /// **'Servizio'**
String get commonService; String get commonOperation;
/// No description provided for @commonSettings. /// No description provided for @commonSettings.
/// ///
@@ -250,17 +250,17 @@ abstract class AppLocalizations {
/// **'Contratti in scadenza'** /// **'Contratti in scadenza'**
String get homeExpiringContracts; String get homeExpiringContracts;
/// No description provided for @homeLatestServiceTickets. /// No description provided for @homeLatestOperationTickets.
/// ///
/// In it, this message translates to: /// In it, this message translates to:
/// **'Ultime assistenze'** /// **'Ultime assistenze'**
String get homeLatestServiceTickets; String get homeLatestOperationTickets;
/// No description provided for @homeLatestServices. /// No description provided for @homeLatestOperations.
/// ///
/// In it, this message translates to: /// In it, this message translates to:
/// **'Ultimi Servizi'** /// **'Ultimi Servizi'**
String get homeLatestServices; String get homeLatestOperations;
/// No description provided for @homeMyTasks. /// No description provided for @homeMyTasks.
/// ///
@@ -268,11 +268,11 @@ abstract class AppLocalizations {
/// **'Mie Attività'** /// **'Mie Attività'**
String get homeMyTasks; String get homeMyTasks;
/// No description provided for @homeNewServiceTicket. /// No description provided for @homeNewOperationTicket.
/// ///
/// In it, this message translates to: /// In it, this message translates to:
/// **'Nuova assistenza'** /// **'Nuova assistenza'**
String get homeNewServiceTicket; String get homeNewOperationTicket;
/// No description provided for @homeNoStoreFound. /// No description provided for @homeNoStoreFound.
/// ///

View File

@@ -12,10 +12,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get homeExpiringContracts => 'Contratti in scadenza'; String get homeExpiringContracts => 'Contratti in scadenza';
@override @override
String get homeLatestServiceTickets => 'Ultime assistenze'; String get homeLatestOperationTickets => 'Ultime assistenze';
@override @override
String get homeLatestServices => 'Ultimi Servizi'; String get homeLatestOperations => 'Ultimi Servizi';
@override @override
String get homeMasterData => 'Anagrafiche'; String get homeMasterData => 'Anagrafiche';
@@ -24,7 +24,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get homeMyTasks => 'Mie Attività'; String get homeMyTasks => 'Mie Attività';
@override @override
String get homeNewService => 'Servizio'; String get homeNewOperation => 'Servizio';
@override @override
String get homeSettings => 'Impostazioni'; String get homeSettings => 'Impostazioni';
@@ -41,7 +41,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get homeNoStoreFound => 'Nessun negozio trovato'; String get homeNoStoreFound => 'Nessun negozio trovato';
@override @override
String get homeNewServiceTicket => 'Nuova assistenza'; String get homeNewOperationTicket => 'Nuova assistenza';
@override @override
String get homeNewNote => 'Nota'; String get homeNewNote => 'Nota';

View File

@@ -81,7 +81,7 @@ class AppLocalizationsIt extends AppLocalizations {
String get commonSave => 'Salva'; String get commonSave => 'Salva';
@override @override
String get commonService => 'Servizio'; String get commonOperation => 'Servizio';
@override @override
String get commonSettings => 'Impostazioni'; String get commonSettings => 'Impostazioni';
@@ -96,16 +96,16 @@ class AppLocalizationsIt extends AppLocalizations {
String get homeExpiringContracts => 'Contratti in scadenza'; String get homeExpiringContracts => 'Contratti in scadenza';
@override @override
String get homeLatestServiceTickets => 'Ultime assistenze'; String get homeLatestOperationTickets => 'Ultime assistenze';
@override @override
String get homeLatestServices => 'Ultimi Servizi'; String get homeLatestOperations => 'Ultimi Servizi';
@override @override
String get homeMyTasks => 'Mie Attività'; String get homeMyTasks => 'Mie Attività';
@override @override
String get homeNewServiceTicket => 'Nuova assistenza'; String get homeNewOperationTicket => 'Nuova assistenza';
@override @override
String get homeNoStoreFound => 'Nessun negozio trovato'; String get homeNoStoreFound => 'Nessun negozio trovato';

View File

@@ -5,7 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flux/features/auth/bloc/auth_cubit.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:flux/l10n/app_localizations.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -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/bloc/store_cubit.dart';
import 'package:flux/features/master_data/store/data/store_repository.dart'; import 'package:flux/features/master_data/store/data/store_repository.dart';
import 'package:flux/features/operations/blocs/operations_cubit.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart';
import 'package:flux/features/operations/data/services_repository.dart';
import 'package:flux/features/settings/settings.dart'; import 'package:flux/features/settings/settings.dart';
void main() async { void main() async {
@@ -52,7 +51,7 @@ void main() async {
BlocProvider<CustomerCubit>(create: (_) => CustomerCubit()), BlocProvider<CustomerCubit>(create: (_) => CustomerCubit()),
BlocProvider<ProductCubit>(create: (_) => ProductCubit()), BlocProvider<ProductCubit>(create: (_) => ProductCubit()),
BlocProvider<StaffCubit>(create: (_) => StaffCubit()), BlocProvider<StaffCubit>(create: (_) => StaffCubit()),
BlocProvider<ServicesCubit>(create: (_) => ServicesCubit()), BlocProvider<OperationsCubit>(create: (_) => OperationsCubit()),
BlocProvider<ProvidersCubit>(create: (_) => ProvidersCubit()), BlocProvider<ProvidersCubit>(create: (_) => ProvidersCubit()),
], ],
child: const FluxApp(), child: const FluxApp(),
@@ -85,7 +84,9 @@ Future<void> setupLocator() async {
getIt.registerLazySingleton<CustomerRepository>(() => CustomerRepository()); getIt.registerLazySingleton<CustomerRepository>(() => CustomerRepository());
getIt.registerLazySingleton<ProductRepository>(() => ProductRepository()); getIt.registerLazySingleton<ProductRepository>(() => ProductRepository());
getIt.registerLazySingleton<StaffRepository>(() => StaffRepository()); getIt.registerLazySingleton<StaffRepository>(() => StaffRepository());
getIt.registerLazySingleton<ServicesRepository>(() => ServicesRepository()); getIt.registerLazySingleton<OperationsRepository>(
() => OperationsRepository(),
);
getIt.registerLazySingleton<ProviderRepository>(() => ProviderRepository()); getIt.registerLazySingleton<ProviderRepository>(() => ProviderRepository());
// NOTA: CompanyRepository l'ho tolto perché la logica della Company // NOTA: CompanyRepository l'ho tolto perché la logica della Company