feat-ultimi_servizi-contratti_in_scadenza #12

Merged
brontomark merged 18 commits from feat-ultimi_servizi-contratti_in_scadenza into main 2026-05-04 15:36:42 +02:00
26 changed files with 136 additions and 134 deletions
Showing only changes of commit 9c8576ada5 - Show all commits

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip distributionUrl=https\://operations.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -131,7 +131,7 @@ class AppRouter {
), ),
), ),
GoRoute( GoRoute(
path: '/services', path: '/operations',
builder: (context, state) => const ServicesScreen(), builder: (context, state) => const ServicesScreen(),
), ),
GoRoute( GoRoute(
@@ -168,8 +168,8 @@ class AppRouter {
}, },
), ),
GoRoute( GoRoute(
path: '/service-form', path: '/operation-form',
name: 'service-form', name: 'operation-form',
builder: (context, state) { builder: (context, state) {
final existingService = state.extra as ServiceModel?; final existingService = state.extra as ServiceModel?;
final serviceId = state.uri.queryParameters['serviceId']; final serviceId = state.uri.queryParameters['serviceId'];
@@ -184,7 +184,7 @@ class AppRouter {
}, },
), ),
GoRoute( GoRoute(
path: '/service/:id/upload', path: '/operation/:id/upload',
builder: (context, state) { builder: (context, state) {
final serviceId = state.pathParameters['id']!; final serviceId = state.pathParameters['id']!;
final serviceName = state.uri.queryParameters['name'] ?? 'Pratica'; final serviceName = state.uri.queryParameters['name'] ?? 'Pratica';

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/services.dart'; import 'package:flutter/operations.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

@@ -3,7 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/features/customers/blocs/customer_cubit.dart'; import 'package:flux/features/customers/blocs/customer_cubit.dart';
import 'package:flux/features/customers/models/customer_model.dart'; import 'package:flux/features/customers/models/customer_model.dart';
import 'package:flux/features/customers/ui/quick_customer_dialog.dart'; import 'package:flux/features/customers/ui/quick_customer_dialog.dart';
import 'package:flux/features/operations/blocs/services_cubit.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart';
class CustomerSearchSheet extends StatefulWidget { class CustomerSearchSheet extends StatefulWidget {
const CustomerSearchSheet({super.key}); const CustomerSearchSheet({super.key});

View File

@@ -29,10 +29,10 @@ class LatestStoreServicesBloc
// Questo gira ad ogni "scatto" dello stream di Supabase // Questo gira ad ogni "scatto" dello stream di Supabase
List<ServiceModel> fullyHydratedServices = []; List<ServiceModel> fullyHydratedServices = [];
for (ServiceModel service in rawServices) { for (ServiceModel operation in rawServices) {
// Peschiamo i dati completi (incluso il cliente) // Peschiamo i dati completi (incluso il cliente)
ServiceModel fullService = await _repository.fetchServiceById( ServiceModel fullService = await _repository.fetchServiceById(
service.id!, operation.id!,
); );
fullyHydratedServices.add(fullService); fullyHydratedServices.add(fullService);
} }
@@ -47,7 +47,7 @@ class LatestStoreServicesBloc
onData: (List<ServiceModel> fullyHydratedServices) { onData: (List<ServiceModel> fullyHydratedServices) {
// Qui ora è tutto sincrono e bellissimo // Qui ora è tutto sincrono e bellissimo
return state.copyWith( return state.copyWith(
services: fullyHydratedServices, operations: fullyHydratedServices,
status: LatestStoreServicesStatus.success, status: LatestStoreServicesStatus.success,
); );
}, },

View File

@@ -5,26 +5,26 @@ enum LatestStoreServicesStatus { initial, loading, success, failure }
class LatestStoreServicesState extends Equatable { class LatestStoreServicesState extends Equatable {
final LatestStoreServicesStatus status; final LatestStoreServicesStatus status;
final String? error; final String? error;
final List<ServiceModel> services; final List<ServiceModel> operations;
const LatestStoreServicesState({ const LatestStoreServicesState({
required this.status, required this.status,
this.error, this.error,
this.services = const [], this.operations = const [],
}); });
@override @override
List<Object?> get props => [status, error, services]; List<Object?> get props => [status, error, operations];
LatestStoreServicesState copyWith({ LatestStoreServicesState copyWith({
LatestStoreServicesStatus? status, LatestStoreServicesStatus? status,
String? error, String? error,
List<ServiceModel>? services, List<ServiceModel>? operations,
}) { }) {
return LatestStoreServicesState( return LatestStoreServicesState(
status: status ?? this.status, status: status ?? this.status,
error: error, error: error,
services: services ?? this.services, operations: operations ?? this.operations,
); );
} }
} }

View File

@@ -70,7 +70,7 @@ class _LatestServicesCardContent extends StatelessWidget {
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
child: TextButton( child: TextButton(
onPressed: () => context.push('/services'), onPressed: () => context.push('/operations'),
child: Text( child: Text(
context.l10n.homeLatestServices, context.l10n.homeLatestServices,
style: TextStyle( style: TextStyle(
@@ -105,7 +105,7 @@ class _LatestServicesCardContent extends StatelessWidget {
); );
} }
if (state.services.isEmpty) { if (state.operations.isEmpty) {
return Center( return Center(
child: Text( child: Text(
"Nessun servizio recente.", "Nessun servizio recente.",
@@ -118,16 +118,16 @@ class _LatestServicesCardContent extends StatelessWidget {
} }
return ListView.separated( return ListView.separated(
itemCount: state.services.length, itemCount: state.operations.length,
separatorBuilder: (context, index) => Divider( separatorBuilder: (context, index) => Divider(
height: 1, height: 1,
color: theme.dividerColor.withValues(alpha: 0.3), color: theme.dividerColor.withValues(alpha: 0.3),
), ),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final service = state.services[index]; final operation = state.operations[index];
return InkWell( return InkWell(
onTap: () => onTap: () =>
context.push('/service-form', extra: service), context.push('/operation-form', extra: operation),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0), padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row( child: Row(
@@ -136,7 +136,7 @@ class _LatestServicesCardContent extends StatelessWidget {
Expanded( Expanded(
flex: 5, flex: 5,
child: Text( child: Text(
service.customerDisplayName ?? operation.customerDisplayName ??
'Cliente sconosciuto', 'Cliente sconosciuto',
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -147,7 +147,7 @@ class _LatestServicesCardContent extends StatelessWidget {
Expanded( Expanded(
flex: 5, flex: 5,
child: Text( child: Text(
service.number, operation.number,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: context.primaryText, color: context.primaryText,
@@ -157,7 +157,7 @@ class _LatestServicesCardContent extends StatelessWidget {
), ),
), ),
Text( Text(
"${service.createdAt?.day}/${service.createdAt?.month}", "${operation.createdAt?.day}/${operation.createdAt?.month}",
style: TextStyle( style: TextStyle(
color: context.secondaryText, color: context.secondaryText,
fontSize: 12, fontSize: 12,

View File

@@ -185,7 +185,7 @@ class HomeScreen extends StatelessWidget {
color: Colors.blue, color: Colors.blue,
onTap: () { onTap: () {
// Entriamo nel form! Nessun parametro extra = Nuovo Servizio // Entriamo nel form! Nessun parametro extra = Nuovo Servizio
context.push('/service-form'); context.push('/operation-form');
}, },
), ),
const SizedBox(width: 12), const SizedBox(width: 12),

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; // <-- IMPORTANTE per i formatter import 'package:flutter/operations.dart'; // <-- IMPORTANTE per i formatter
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

@@ -17,11 +17,11 @@ class ServiceSavedEvent extends ServiceFilesEvent {
class LoadServiceFilesEvent extends ServiceFilesEvent { class LoadServiceFilesEvent extends ServiceFilesEvent {
final String? serviceId; final String? serviceId;
final ServiceModel? service; final ServiceModel? operation;
const LoadServiceFilesEvent({this.serviceId, this.service}); const LoadServiceFilesEvent({this.serviceId, this.operation});
@override @override
List<Object?> get props => [serviceId, service]; List<Object?> get props => [serviceId, operation];
} }
class AddServiceFilesEvent extends ServiceFilesEvent { class AddServiceFilesEvent extends ServiceFilesEvent {

View File

@@ -1,4 +1,4 @@
part of 'services_cubit.dart'; part of 'operations_cubit.dart';
enum ServicesStatus { enum ServicesStatus {
initial, initial,

View File

@@ -18,7 +18,7 @@ class ServicesRepository {
Future<ServiceModel> fetchServiceById(String id) async { Future<ServiceModel> fetchServiceById(String id) async {
try { try {
final response = await _supabase final response = await _supabase
.from('service') .from('operation')
.select(''' .select('''
*, *,
customer(nome), customer(nome),
@@ -47,7 +47,7 @@ class ServicesRepository {
try { try {
// Nota: 'customer(name, surname)' serve per il display name nella card // Nota: 'customer(name, surname)' serve per il display name nella card
var query = _supabase var query = _supabase
.from('service') .from('operation')
.select(''' .select('''
*, *,
customer(nome), customer(nome),
@@ -89,7 +89,7 @@ class ServicesRepository {
required int limit, required int limit,
}) { }) {
return _supabase return _supabase
.from('service') .from('operation')
.stream(primaryKey: ['id']) .stream(primaryKey: ['id'])
.eq('store_id', storeId) .eq('store_id', storeId)
.order('created_at', ascending: false) .order('created_at', ascending: false)
@@ -101,12 +101,12 @@ class ServicesRepository {
} }
// --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) --- // --- SALVATAGGIO COMPLETO (PRIMA PADRE, POI FIGLI) ---
Future<ServiceModel> saveFullService(ServiceModel service) async { Future<ServiceModel> saveFullService(ServiceModel operation) async {
try { try {
// 1. Upsert del record principale // 1. Upsert del record principale
final serviceData = await _supabase final serviceData = await _supabase
.from('service') .from('operation')
.upsert(service.toMap()) .upsert(operation.toMap())
.select() .select()
.single(); .single();
@@ -114,7 +114,7 @@ class ServicesRepository {
// 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 (service.id != null) { if (operation.id != null) {
await Future.wait([ await Future.wait([
_supabase.from('energy_service').delete().eq('service_id', newId), _supabase.from('energy_service').delete().eq('service_id', newId),
_supabase.from('fin_service').delete().eq('service_id', newId), _supabase.from('fin_service').delete().eq('service_id', newId),
@@ -129,36 +129,36 @@ 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 (service.energyServices.isNotEmpty) { if (operation.energyServices.isNotEmpty) {
insertTasks.add( insertTasks.add(
_supabase _supabase
.from('energy_service') .from('energy_service')
.insert( .insert(
service.energyServices operation.energyServices
.map((item) => item.copyWith(serviceId: newId).toMap()) .map((item) => item.copyWith(serviceId: newId).toMap())
.toList(), .toList(),
), ),
); );
} }
if (service.finServices.isNotEmpty) { if (operation.finServices.isNotEmpty) {
insertTasks.add( insertTasks.add(
_supabase _supabase
.from('fin_service') .from('fin_service')
.insert( .insert(
service.finServices operation.finServices
.map((item) => item.copyWith(serviceId: newId).toMap()) .map((item) => item.copyWith(serviceId: newId).toMap())
.toList(), .toList(),
), ),
); );
} }
if (service.entertainmentServices.isNotEmpty) { if (operation.entertainmentServices.isNotEmpty) {
insertTasks.add( insertTasks.add(
_supabase _supabase
.from('entertainment_service') .from('entertainment_service')
.insert( .insert(
service.entertainmentServices operation.entertainmentServices
.map((item) => item.copyWith(serviceId: newId).toMap()) .map((item) => item.copyWith(serviceId: newId).toMap())
.toList(), .toList(),
), ),
@@ -171,7 +171,7 @@ class ServicesRepository {
// 4. UPLOAD DEI FILE LOCALI (Nuovi) // 4. UPLOAD DEI FILE LOCALI (Nuovi)
// Filtriamo solo i file che non hanno ancora un ID (quindi sono locali) // Filtriamo solo i file che non hanno ancora un ID (quindi sono locali)
final localFilesToUpload = service.files final localFilesToUpload = operation.files
.where((f) => f.id == null) .where((f) => f.id == null)
.toList(); .toList();
@@ -180,7 +180,7 @@ class ServicesRepository {
for (var file in localFilesToUpload) { for (var file in localFilesToUpload) {
final storagePath = final storagePath =
'$companyId/services/$newId/${DateTime.now().millisecondsSinceEpoch}_${file.name}.${file.extension}'; '$companyId/operations/$newId/${DateTime.now().millisecondsSinceEpoch}_${file.name}.${file.extension}';
final String mimeType = file.extension.toLowerCase() == 'pdf' final String mimeType = file.extension.toLowerCase() == 'pdf'
? 'application/pdf' ? 'application/pdf'
: 'image/${file.extension}'; : 'image/${file.extension}';
@@ -216,7 +216,7 @@ class ServicesRepository {
// 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 service_file appena inseriti)
final updatedServiceData = await _supabase final updatedServiceData = await _supabase
.from('service') .from('operation')
.select(''' .select('''
*, *,
energy_service(*), energy_service(*),
@@ -237,7 +237,7 @@ class ServicesRepository {
// --- ELIMINAZIONE --- // --- ELIMINAZIONE ---
Future<void> deleteService(String id) async { Future<void> deleteService(String id) async {
try { try {
await _supabase.from('service').delete().eq('id', id); await _supabase.from('operation').delete().eq('id', id);
} catch (e) { } catch (e) {
throw Exception('Errore durante l\'eliminazione: $e'); throw Exception('Errore durante l\'eliminazione: $e');
} }
@@ -247,11 +247,11 @@ class ServicesRepository {
Future<List<String>> fetchTopEntertainmentTypes(String companyId) async { Future<List<String>> fetchTopEntertainmentTypes(String companyId) async {
try { try {
// 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 'service' 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_service')
.select('type, service!inner(store!inner(company_id))') .select('type, operation!inner(store!inner(company_id))')
.eq('service.store.company_id', companyId) .eq('operation.store.company_id', companyId)
.limit(100); // Prendiamo un campione .limit(100); // Prendiamo un campione
// Logica rapida per contare le occorrenze e prendere i primi 5 // Logica rapida per contare le occorrenze e prendere i primi 5
@@ -297,7 +297,7 @@ class ServicesRepository {
'_', '_',
); );
final storagePath = final storagePath =
'$companyId/services/$serviceId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName'; '$companyId/operations/$serviceId/${DateTime.now().millisecondsSinceEpoch}_$cleanFileName';
final int fileSize = pickedFile.size; final int fileSize = pickedFile.size;
final fileToSave = ServiceFileModel( final fileToSave = ServiceFileModel(
serviceId: serviceId, serviceId: serviceId,

View File

@@ -6,7 +6,7 @@ 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/service_files_bloc.dart';
import 'package:flux/features/operations/blocs/services_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/service_file_model.dart';
class AttachmentsSection extends StatelessWidget { class AttachmentsSection extends StatelessWidget {
@@ -310,7 +310,7 @@ class AttachmentsSection extends StatelessWidget {
}, },
child: QrUploadDialog( child: QrUploadDialog(
deepLinkUrl: deepLinkUrl:
'fluxapp:///service/${currentService!.id}/upload?name=${Uri.encodeComponent(nomePratica)}', 'fluxapp:///operation/${currentService!.id}/upload?name=${Uri.encodeComponent(nomePratica)}',
title: 'Scatta per\n$nomePratica', title: 'Scatta per\n$nomePratica',
), ),
), ),

View File

@@ -3,9 +3,9 @@ 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/service_model.dart';
class CustomerSection extends StatelessWidget { class CustomerSection extends StatelessWidget {
final ServiceModel service; final ServiceModel operation;
const CustomerSection({super.key, required this.service}); const CustomerSection({super.key, required this.operation});
void _openCustomerSearch(BuildContext context) { void _openCustomerSearch(BuildContext context) {
showModalBottomSheet( showModalBottomSheet(
@@ -28,8 +28,8 @@ class CustomerSection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// Niente BlocBuilder qui! Leggiamo solo la variabile 'service' // Niente BlocBuilder qui! Leggiamo solo la variabile 'operation'
final hasCustomer = service.customerId != null; final hasCustomer = operation.customerId != null;
return Card( return Card(
elevation: 2, elevation: 2,
@@ -74,7 +74,7 @@ class CustomerSection extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
service.customerDisplayName ?? "Cliente Selezionato", operation.customerDisplayName ?? "Cliente Selezionato",
style: const TextStyle( style: const TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,

View File

@@ -63,7 +63,7 @@ class _EnergyServiceDialogState extends State<EnergyServiceDialog> {
}, },
) )
: _EnergyList( : _EnergyList(
services: _tempList, operations: _tempList,
onDelete: (index) { onDelete: (index) {
setState(() => _tempList.removeAt(index)); setState(() => _tempList.removeAt(index));
}, },
@@ -101,14 +101,14 @@ 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> services; final List<EnergyServiceModel> 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;
final VoidCallback onAddTap; final VoidCallback onAddTap;
const _EnergyList({ const _EnergyList({
required this.services, required this.operations,
required this.activeProviders, // <--- Richiesto required this.activeProviders, // <--- Richiesto
required this.onDelete, required this.onDelete,
required this.onAddTap, required this.onAddTap,
@@ -120,7 +120,7 @@ class _EnergyList extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (services.isEmpty) if (operations.isEmpty)
const Padding( const Padding(
padding: EdgeInsets.symmetric(vertical: 32.0), padding: EdgeInsets.symmetric(vertical: 32.0),
child: Text( child: Text(
@@ -133,10 +133,10 @@ class _EnergyList extends StatelessWidget {
Flexible( Flexible(
child: ListView.separated( child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
itemCount: services.length, itemCount: operations.length,
separatorBuilder: (_, _) => const Divider(height: 1), separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final s = services[index]; final s = operations[index];
final isLuce = s.type == EnergyType.luce; final isLuce = s.type == EnergyType.luce;
// LA MAGIA: Troviamo il nome partendo dall'ID salvato nel servizio // LA MAGIA: Troviamo il nome partendo dall'ID salvato nel servizio

View File

@@ -67,7 +67,7 @@ class _EntertainmentServiceDialogState
builder: (context, state) { builder: (context, state) {
// Passiamo allProviders per garantire la visione dello storico // Passiamo allProviders per garantire la visione dello storico
return _EntertainmentList( return _EntertainmentList(
services: _tempList, operations: _tempList,
allProviders: state.allProviders, allProviders: state.allProviders,
onDelete: (index) => onDelete: (index) =>
setState(() => _tempList.removeAt(index)), setState(() => _tempList.removeAt(index)),
@@ -94,13 +94,13 @@ class _EntertainmentServiceDialogState
} }
class _EntertainmentList extends StatelessWidget { class _EntertainmentList extends StatelessWidget {
final List<EntertainmentServiceModel> services; final List<EntertainmentServiceModel> operations;
final List<ProviderModel> allProviders; final List<ProviderModel> allProviders;
final Function(int) onDelete; final Function(int) onDelete;
final VoidCallback onAddTap; final VoidCallback onAddTap;
const _EntertainmentList({ const _EntertainmentList({
required this.services, required this.operations,
required this.allProviders, required this.allProviders,
required this.onDelete, required this.onDelete,
required this.onAddTap, required this.onAddTap,
@@ -112,7 +112,7 @@ class _EntertainmentList extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
if (services.isEmpty) if (operations.isEmpty)
const Padding( const Padding(
padding: EdgeInsets.symmetric(vertical: 32.0), padding: EdgeInsets.symmetric(vertical: 32.0),
child: Text( child: Text(
@@ -125,10 +125,10 @@ class _EntertainmentList extends StatelessWidget {
Flexible( Flexible(
child: ListView.separated( child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
itemCount: services.length, itemCount: operations.length,
separatorBuilder: (_, _) => const Divider(height: 1), separatorBuilder: (_, _) => const Divider(height: 1),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final s = services[index]; final s = operations[index];
final providerName = allProviders final providerName = allProviders
.firstWhere( .firstWhere(

View File

@@ -74,7 +74,7 @@ class _FinanceServiceDialogState extends State<FinanceServiceDialog> {
return BlocBuilder<ProductCubit, ProductState>( return BlocBuilder<ProductCubit, ProductState>(
builder: (context, prodState) { builder: (context, prodState) {
return _FinanceList( return _FinanceList(
services: _tempList, operations: _tempList,
allProviders: allProviders:
provState.allProviders, // Per vedere lo storico provState.allProviders, // Per vedere lo storico
allModels: prodState.models, allModels: prodState.models,
@@ -109,14 +109,14 @@ class _FinanceServiceDialogState extends State<FinanceServiceDialog> {
// VISTA LISTA (STORICA) // VISTA LISTA (STORICA)
// =========================================================================== // ===========================================================================
class _FinanceList extends StatelessWidget { class _FinanceList extends StatelessWidget {
final List<FinServiceModel> services; final List<FinServiceModel> 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;
final VoidCallback onAddTap; final VoidCallback onAddTap;
const _FinanceList({ const _FinanceList({
required this.services, required this.operations,
required this.allProviders, required this.allProviders,
required this.allModels, required this.allModels,
required this.onDelete, required this.onDelete,
@@ -125,7 +125,7 @@ class _FinanceList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (services.isEmpty) { if (operations.isEmpty) {
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -151,10 +151,10 @@ class _FinanceList extends StatelessWidget {
Flexible( Flexible(
child: ListView.separated( child: ListView.separated(
shrinkWrap: true, shrinkWrap: true,
itemCount: services.length, itemCount: operations.length,
separatorBuilder: (_, _) => const Divider(), separatorBuilder: (_, _) => const Divider(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final s = services[index]; final s = operations[index];
// Cerchiamo il nome del provider in TUTTI quelli caricati (storico) // Cerchiamo il nome del provider in TUTTI quelli caricati (storico)
final providerName = allProviders final providerName = allProviders

View File

@@ -1,11 +1,11 @@
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/services_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/service_model.dart';
class GeneralInfoSection extends StatelessWidget { class GeneralInfoSection extends StatelessWidget {
final ServiceModel service; final ServiceModel operation;
const GeneralInfoSection({super.key, required this.service}); const GeneralInfoSection({super.key, required this.operation});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -34,7 +34,7 @@ class GeneralInfoSection extends StatelessWidget {
// Numero di Riferimento / Telefono // Numero di Riferimento / Telefono
TextFormField( TextFormField(
initialValue: service.number, initialValue: operation.number,
keyboardType: TextInputType keyboardType: TextInputType
.phone, // Fa aprire il tastierino numerico su mobile .phone, // Fa aprire il tastierino numerico su mobile
decoration: const InputDecoration( decoration: const InputDecoration(
@@ -59,7 +59,7 @@ class GeneralInfoSection extends StatelessWidget {
"Pratica in lavorazione", "Pratica in lavorazione",
style: TextStyle(fontSize: 12), style: TextStyle(fontSize: 12),
), ),
value: service.isBozza, value: operation.isBozza,
activeThumbColor: Colors.orange, activeThumbColor: Colors.orange,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
onChanged: (val) { onChanged: (val) {
@@ -75,7 +75,7 @@ class GeneralInfoSection extends StatelessWidget {
"Esito positivo", "Esito positivo",
style: TextStyle(fontSize: 12), style: TextStyle(fontSize: 12),
), ),
value: service.resultOk, value: operation.resultOk,
activeThumbColor: Colors.green, activeThumbColor: Colors.green,
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
onChanged: (val) { onChanged: (val) {
@@ -89,7 +89,7 @@ class GeneralInfoSection extends StatelessWidget {
// Campo Note // Campo Note
TextFormField( TextFormField(
initialValue: service.note, initialValue: operation.note,
maxLines: 4, maxLines: 4,
minLines: 2, minLines: 2,
decoration: const InputDecoration( decoration: const InputDecoration(

View File

@@ -1,6 +1,6 @@
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/services_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/service_model.dart';
import 'package:flux/features/operations/ui/service_form_screen/attachment_section.dart'; import 'package:flux/features/operations/ui/service_form_screen/attachment_section.dart';
import 'package:flux/features/operations/ui/service_form_screen/customer_section.dart'; import 'package:flux/features/operations/ui/service_form_screen/customer_section.dart';
@@ -70,7 +70,7 @@ class _ServiceFormScreenState extends State<ServiceFormScreen> {
} }
}, },
builder: (context, state) { builder: (context, state) {
final service = state.currentService; final operation = state.currentService;
final isSaving = state.status == ServicesStatus.saving; final isSaving = state.status == ServicesStatus.saving;
final isEditMode = widget.serviceId != null; final isEditMode = widget.serviceId != null;
@@ -89,7 +89,7 @@ class _ServiceFormScreenState extends State<ServiceFormScreen> {
), ),
), ),
) )
else if (service != null) ...[ else if (operation != null) ...[
IconButton( IconButton(
icon: const Icon(Icons.edit_note), icon: const Icon(Icons.edit_note),
tooltip: "Salva come Bozza", tooltip: "Salva come Bozza",
@@ -107,20 +107,20 @@ class _ServiceFormScreenState extends State<ServiceFormScreen> {
], ],
], ],
), ),
body: (service == null) body: (operation == null)
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: SingleChildScrollView( : SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
CustomerSection(service: service), CustomerSection(operation: operation),
const SizedBox(height: 24), const SizedBox(height: 24),
GeneralInfoSection(service: service), GeneralInfoSection(operation: operation),
const SizedBox(height: 24), const SizedBox(height: 24),
ServicesGrid(service: service), ServicesGrid(operation: operation),
const SizedBox(height: 32), const SizedBox(height: 32),
AttachmentsSection(), AttachmentsSection(),

View File

@@ -1,7 +1,7 @@
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/master_data/products/blocs/product_cubit.dart'; import 'package:flux/features/master_data/products/blocs/product_cubit.dart';
import 'package:flux/features/operations/blocs/services_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_service_model.dart';
import 'package:flux/features/operations/models/entertainment_service_model.dart'; import 'package:flux/features/operations/models/entertainment_service_model.dart';
import 'package:flux/features/operations/models/fin_service_model.dart'; import 'package:flux/features/operations/models/fin_service_model.dart';
@@ -13,9 +13,9 @@ import 'package:flux/features/operations/ui/service_form_screen/finance_service_
import 'package:flux/features/operations/ui/service_form_screen/int_dialogs.dart'; // Assicurati di importare il modello import 'package:flux/features/operations/ui/service_form_screen/int_dialogs.dart'; // Assicurati di importare il modello
class ServicesGrid extends StatelessWidget { class ServicesGrid extends StatelessWidget {
final ServiceModel service; final ServiceModel operation;
const ServicesGrid({super.key, required this.service}); const ServicesGrid({super.key, required this.operation});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -52,65 +52,65 @@ class ServicesGrid extends StatelessWidget {
// --- CONTATORI SEMPLICI --- // --- CONTATORI SEMPLICI ---
ActionCard( ActionCard(
label: "AL", label: "AL",
count: service.al, count: operation.al,
icon: Icons.sim_card, icon: Icons.sim_card,
color: Colors.blue, color: Colors.blue,
onTap: () => updateCountDialog( onTap: () => updateCountDialog(
context, context,
"AL", "AL",
service.al, operation.al,
(val) => (val) =>
context.read<ServicesCubit>().updateField(al: val), context.read<ServicesCubit>().updateField(al: val),
), ),
), ),
ActionCard( ActionCard(
label: "MNP", label: "MNP",
count: service.mnp, count: operation.mnp,
icon: Icons.phone_android, icon: Icons.phone_android,
color: Colors.indigo, color: Colors.indigo,
onTap: () => updateCountDialog( onTap: () => updateCountDialog(
context, context,
"MNP", "MNP",
service.mnp, operation.mnp,
(val) => (val) =>
context.read<ServicesCubit>().updateField(mnp: val), context.read<ServicesCubit>().updateField(mnp: val),
), ),
), ),
ActionCard( ActionCard(
label: "NIP", label: "NIP",
count: service.nip, count: operation.nip,
icon: Icons.compare_arrows, icon: Icons.compare_arrows,
color: Colors.cyan, color: Colors.cyan,
onTap: () => updateCountDialog( onTap: () => updateCountDialog(
context, context,
"NIP", "NIP",
service.nip, operation.nip,
(val) => (val) =>
context.read<ServicesCubit>().updateField(nip: val), context.read<ServicesCubit>().updateField(nip: val),
), ),
), ),
ActionCard( ActionCard(
label: "Unica", label: "Unica",
count: service.unica, count: operation.unica,
icon: Icons.all_inclusive, icon: Icons.all_inclusive,
color: Colors.purple, color: Colors.purple,
onTap: () => updateCountDialog( onTap: () => updateCountDialog(
context, context,
"Unica", "Unica",
service.unica, operation.unica,
(val) => (val) =>
context.read<ServicesCubit>().updateField(unica: val), context.read<ServicesCubit>().updateField(unica: val),
), ),
), ),
ActionCard( ActionCard(
label: "Telepass", label: "Telepass",
count: service.telepass, count: operation.telepass,
icon: Icons.directions_car, icon: Icons.directions_car,
color: Colors.amber.shade700, color: Colors.amber.shade700,
onTap: () => updateCountDialog( onTap: () => updateCountDialog(
context, context,
"Telepass", "Telepass",
service.telepass, operation.telepass,
(val) => context.read<ServicesCubit>().updateField( (val) => context.read<ServicesCubit>().updateField(
telepass: val, telepass: val,
), ),
@@ -120,7 +120,7 @@ class ServicesGrid extends StatelessWidget {
// --- MODULI COMPLESSI (Le liste) --- // --- MODULI COMPLESSI (Le liste) ---
ActionCard( ActionCard(
label: "Energia", label: "Energia",
count: service.energyServices.length, count: operation.energyServices.length,
icon: Icons.bolt, icon: Icons.bolt,
color: Colors.green, color: Colors.green,
onTap: () async { onTap: () async {
@@ -128,8 +128,8 @@ class ServicesGrid extends StatelessWidget {
final result = await showDialog<List<EnergyServiceModel>>( final result = await showDialog<List<EnergyServiceModel>>(
context: context, context: context,
builder: (context) => EnergyServiceDialog( builder: (context) => EnergyServiceDialog(
currentStoreId: service.storeId, currentStoreId: operation.storeId,
initialServices: service initialServices: operation
.energyServices, // Passiamo la lista attuale .energyServices, // Passiamo la lista attuale
), ),
); );
@@ -144,7 +144,7 @@ class ServicesGrid extends StatelessWidget {
), ),
ActionCard( ActionCard(
label: "Finanziam.", label: "Finanziam.",
count: service.finServices.length, count: operation.finServices.length,
icon: Icons.euro_symbol, icon: Icons.euro_symbol,
color: Colors.teal, color: Colors.teal,
onTap: () async { onTap: () async {
@@ -152,9 +152,9 @@ class ServicesGrid extends StatelessWidget {
context: context, context: context,
builder: (context) => FinanceServiceDialog( builder: (context) => FinanceServiceDialog(
productCubit: context.read<ProductCubit>(), productCubit: context.read<ProductCubit>(),
currentStoreId: service.storeId, currentStoreId: operation.storeId,
initialServices: initialServices: operation
service.finServices, // Passiamo la lista attuale .finServices, // Passiamo la lista attuale
), ),
); );
@@ -165,7 +165,7 @@ class ServicesGrid extends StatelessWidget {
), ),
ActionCard( ActionCard(
label: "Intratten.", label: "Intratten.",
count: service.entertainmentServices.length, count: operation.entertainmentServices.length,
icon: Icons.movie_filter_outlined, icon: Icons.movie_filter_outlined,
color: Colors.purple, color: Colors.purple,
onTap: () async { onTap: () async {
@@ -173,8 +173,8 @@ class ServicesGrid extends StatelessWidget {
await showDialog<List<EntertainmentServiceModel>>( await showDialog<List<EntertainmentServiceModel>>(
context: context, context: context,
builder: (context) => EntertainmentServiceDialog( builder: (context) => EntertainmentServiceDialog(
initialServices: service.entertainmentServices, initialServices: operation.entertainmentServices,
currentStoreId: service.storeId, currentStoreId: operation.storeId,
), ),
); );

View File

@@ -1,6 +1,6 @@
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/services_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/service_model.dart';
import 'package:flux/features/operations/utils/service_actions.dart'; import 'package:flux/features/operations/utils/service_actions.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -107,8 +107,8 @@ class _ServicesScreenState extends State<ServicesScreen> {
); );
} }
final service = state.allServices[index]; final operation = state.allServices[index];
return _buildServiceCard(context, service); return _buildServiceCard(context, operation);
}, },
), ),
); );
@@ -121,7 +121,7 @@ class _ServicesScreenState extends State<ServicesScreen> {
); );
} }
Widget _buildServiceCard(BuildContext context, ServiceModel service) { Widget _buildServiceCard(BuildContext context, ServiceModel 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,
@@ -132,14 +132,14 @@ class _ServicesScreenState extends State<ServicesScreen> {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
service.customerDisplayName ?? "Cliente sconosciuto", operation.customerDisplayName ?? "Cliente sconosciuto",
style: const TextStyle( style: const TextStyle(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16, fontSize: 16,
), ),
), ),
), ),
if (service.isBozza) if (operation.isBozza)
const Chip( const Chip(
label: Text( label: Text(
"BOZZA", "BOZZA",
@@ -155,20 +155,20 @@ class _ServicesScreenState extends State<ServicesScreen> {
children: [ children: [
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
"Pratica: ${service.number}${service.createdAt?.day}/${service.createdAt?.month}/${service.createdAt?.year}", "Pratica: ${operation.number}${operation.createdAt?.day}/${operation.createdAt?.month}/${operation.createdAt?.year}",
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
// I nostri mini-chip per i servizi attivati // I nostri mini-chip per i servizi attivati
Wrap( Wrap(
spacing: 6, spacing: 6,
children: [ children: [
if (service.al > 0 || service.mnp > 0) if (operation.al > 0 || operation.mnp > 0)
_miniBadge("📞 Tel", Colors.blue), _miniBadge("📞 Tel", Colors.blue),
if (service.energyServices.isNotEmpty) if (operation.energyServices.isNotEmpty)
_miniBadge("⚡ Energy", Colors.green), _miniBadge("⚡ Energy", Colors.green),
if (service.finServices.isNotEmpty) if (operation.finServices.isNotEmpty)
_miniBadge("💰 Fin", Colors.purple), _miniBadge("💰 Fin", Colors.purple),
if (service.entertainmentServices.isNotEmpty) if (operation.entertainmentServices.isNotEmpty)
_miniBadge("📺 Ent", Colors.red), _miniBadge("📺 Ent", Colors.red),
], ],
), ),
@@ -176,10 +176,12 @@ class _ServicesScreenState extends State<ServicesScreen> {
), ),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () => context.pushNamed( onTap: () => context.pushNamed(
'service-form', 'operation-form',
extra: service, // <-- 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: service.id != null ? {'serviceId': service.id!} : {}, queryParameters: operation.id != null
? {'serviceId': operation.id!}
: {},
), ),
), ),
); );

View File

@@ -2,7 +2,7 @@ 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/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/services_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/service_model.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -67,7 +67,7 @@ void startNewService(BuildContext context) {
Navigator.pop(modalContext); Navigator.pop(modalContext);
// 3. Naviga verso il form // 3. Naviga verso il form
context.pushNamed('service-form'); context.pushNamed('operation-form');
}, },
), ),
), ),

View File

@@ -1,13 +1,13 @@
{ {
"@@locale": "en", "@@locale": "en",
"welcomeBack": "Welcome back, {name}! 👋", "welcomeBack": "Welcome back, {name}! 👋",
"latestServices": "Latest Services", "latestServices": "Latest Operations",
"masterData": "Master Data", "masterData": "Master Data",
"settings": "Settings", "settings": "Settings",
"newService": "Service", "newService": "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 service tickets" "latest_service_tickets": "Latest operation tickets"
} }

View File

@@ -26,7 +26,7 @@ import 'package:flux/features/master_data/staff/blocs/staff_cubit.dart';
import 'package:flux/features/master_data/staff/data/staff_repository.dart'; import 'package:flux/features/master_data/staff/data/staff_repository.dart';
import 'package:flux/features/master_data/store/bloc/store_cubit.dart'; import 'package:flux/features/master_data/store/bloc/store_cubit.dart';
import 'package:flux/features/master_data/store/data/store_repository.dart'; import 'package:flux/features/master_data/store/data/store_repository.dart';
import 'package:flux/features/operations/blocs/services_cubit.dart'; import 'package:flux/features/operations/blocs/operations_cubit.dart';
import 'package:flux/features/operations/data/services_repository.dart'; import 'package:flux/features/operations/data/services_repository.dart';
import 'package:flux/features/settings/settings.dart'; import 'package:flux/features/settings/settings.dart';

View File

@@ -35,9 +35,9 @@
<menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/> <menuItem isSeparatorItem="YES" id="VOq-y0-SEH"/>
<menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/> <menuItem title="Preferences…" keyEquivalent="," id="BOF-NM-1cW"/>
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/> <menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
<menuItem title="Services" id="NMo-om-nkz"> <menuItem title="Operations" id="NMo-om-nkz">
<modifierMask key="keyEquivalentModifierMask"/> <modifierMask key="keyEquivalentModifierMask"/>
<menu key="submenu" title="Services" systemMenu="services" id="hz9-B4-Xy5"/> <menu key="submenu" title="Operations" systemMenu="operations" id="hz9-B4-Xy5"/>
</menuItem> </menuItem>
<menuItem isSeparatorItem="YES" id="4je-JR-u6R"/> <menuItem isSeparatorItem="YES" id="4je-JR-u6R"/>
<menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN"> <menuItem title="Hide APP_NAME" keyEquivalent="h" id="Olw-nP-bQN">