rifatta operation form e diverse migliorie generali

This commit is contained in:
2026-05-19 10:32:01 +02:00
parent ecb161bc07
commit 00d5890a37
17 changed files with 484 additions and 494 deletions

View File

@@ -12,10 +12,11 @@ import 'package:flux/core/widgets/image_upload/ui/upload_success_screen.dart';
import 'package:flux/features/auth/ui/auth_screen.dart'; import 'package:flux/features/auth/ui/auth_screen.dart';
import 'package:flux/features/company/bloc/company_settings_cubit.dart'; import 'package:flux/features/company/bloc/company_settings_cubit.dart';
import 'package:flux/features/company/ui/company_settings_screen.dart'; import 'package:flux/features/company/ui/company_settings_screen.dart';
import 'package:flux/features/customers/blocs/customers_cubit.dart'; import 'package:flux/features/customers/blocs/customer_form_cubit.dart';
import 'package:flux/features/customers/blocs/customers_list_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/customer_detail_screen.dart'; import 'package:flux/features/customers/ui/customer_detail_screen.dart';
import 'package:flux/features/customers/ui/customer_form.dart'; import 'package:flux/features/customers/ui/customer_form_screen.dart';
import 'package:flux/features/customers/ui/customers_list_screen.dart'; import 'package:flux/features/customers/ui/customers_list_screen.dart';
import 'package:flux/features/home/ui/home_screen.dart'; import 'package:flux/features/home/ui/home_screen.dart';
import 'package:flux/features/master_data/master_data_hub_content.dart'; import 'package:flux/features/master_data/master_data_hub_content.dart';
@@ -247,7 +248,7 @@ class AppRouter {
TrackingParentType.ticket, TrackingParentType.ticket,
); );
} }
context.read<CustomersCubit>().loadCustomers(); context.read<CustomersListCubit>().loadCustomers();
context.read<ProductsCubit>().loadModels(); context.read<ProductsCubit>().loadModels();
context.read<ProductsCubit>().loadBrands(); context.read<ProductsCubit>().loadBrands();
@@ -328,13 +329,24 @@ class AppRouter {
path: '/customer/form/:id', path: '/customer/form/:id',
name: Routes.customerForm, name: Routes.customerForm,
builder: (context, state) { builder: (context, state) {
final String pathId = state.pathParameters['id'] ?? 'new';
final String? realCustomerId;
if (pathId == 'new') {
realCustomerId = null;
} else {
realCustomerId = pathId;
}
final customer = state.extra as CustomerModel?; final customer = state.extra as CustomerModel?;
return BlocProvider( return BlocProvider(
create: (context) => AttachmentsBloc( create: (context) => CustomerFormCubit(
parentType: AttachmentParentType.customer, existingCustomer: customer,
parentId: customer.id, customerId: realCustomerId,
),
child: CustomerFormScreen(
customer: customer,
customerId: realCustomerId,
), ),
child: CustomerForm(customer: customer),
); );
}, },
), ),
@@ -365,7 +377,7 @@ class AppRouter {
.state .state
.currentStore! .currentStore!
.id!; .id!;
context.read<CustomersCubit>().loadCustomers(); context.read<CustomersListCubit>().loadCustomers();
context.read<ProviderListCubit>().loadProviders(currentStoreId); context.read<ProviderListCubit>().loadProviders(currentStoreId);
context.read<ProductsCubit>().loadModels(); context.read<ProductsCubit>().loadModels();
context.read<ProductsCubit>().loadBrands(); context.read<ProductsCubit>().loadBrands();

View File

@@ -1,4 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/utils/extensions.dart'; import 'package:flux/core/utils/extensions.dart';
import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/core/widgets/flux_text_field.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
@@ -72,6 +74,12 @@ class _SetPasswordScreenState extends State<SetPasswordScreen> {
title: Text(context.l10n.setPasswordScreenWelcomeInFlux), title: Text(context.l10n.setPasswordScreenWelcomeInFlux),
automaticallyImplyLeading: automaticallyImplyLeading:
false, // Non può tornare indietro, deve mettere la password! false, // Non può tornare indietro, deve mettere la password!
actions: [
IconButton.filled(
onPressed: () => context.read<SessionCubit>().signOut(),
icon: Icon(Icons.logout),
),
],
), ),
body: Padding( body: Padding(
padding: const EdgeInsets.all(24.0), padding: const EdgeInsets.all(24.0),

View File

@@ -426,7 +426,7 @@ class _SharedAttachmentsSectionState extends State<SharedAttachmentsSection> {
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
title: const Text( title: const Text(
'Cartella Export (Es. TIM AttachmentRepository)', 'Cartella Export PDF',
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
subtitle: Text( subtitle: Text(

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/routes/routes.dart'; import 'package:flux/core/routes/routes.dart';
import 'package:flux/features/customers/blocs/customers_cubit.dart'; import 'package:flux/features/customers/blocs/customers_list_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:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@@ -253,7 +253,7 @@ class SharedCustomerSection extends StatelessWidget {
), ),
onChanged: (query) { onChanged: (query) {
currentSearchQuery = query; currentSearchQuery = query;
context.read<CustomersCubit>().searchCustomers(query); context.read<CustomersListCubit>().searchCustomers(query);
}, },
), ),
), ),
@@ -272,7 +272,7 @@ class SharedCustomerSection extends StatelessWidget {
context: context, context: context,
builder: (dialogContext) { builder: (dialogContext) {
return BlocProvider.value( return BlocProvider.value(
value: context.read<CustomersCubit>(), value: context.read<CustomersListCubit>(),
child: QuickCustomerDialog( child: QuickCustomerDialog(
initialQuery: initialQuery:
currentSearchQuery, // <-- Passiamo quello che ha digitato! currentSearchQuery, // <-- Passiamo quello che ha digitato!
@@ -297,9 +297,9 @@ class SharedCustomerSection extends StatelessWidget {
const Divider(), const Divider(),
// Lista Clienti dal Bloc // Lista Clienti dal Bloc
Expanded( Expanded(
child: BlocBuilder<CustomersCubit, CustomersState>( child: BlocBuilder<CustomersListCubit, CustomersListState>(
builder: (context, state) { builder: (context, state) {
if (state.status == CustomersStatus.loading) { if (state.status == CustomersListStatus.loading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
if (state.customers.isEmpty) { if (state.customers.isEmpty) {

View File

@@ -11,7 +11,7 @@ class CustomerFormCubit extends Cubit<CustomerFormState> {
final CustomerRepository _repository = GetIt.I<CustomerRepository>(); final CustomerRepository _repository = GetIt.I<CustomerRepository>();
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>(); final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
CustomerFormCubit({CustomerModel? existingCustomer}) CustomerFormCubit({CustomerModel? existingCustomer, String? customerId})
: super( : super(
CustomerFormState(customer: existingCustomer ?? CustomerModel.empty()), CustomerFormState(customer: existingCustomer ?? CustomerModel.empty()),
); );
@@ -103,4 +103,26 @@ class CustomerFormCubit extends Cubit<CustomerFormState> {
); );
} }
} }
Future<CustomerModel?> quickCreateCustomer({
required String name,
String? phone,
String? email,
}) async {
final newCustomer = CustomerModel(
name: name,
phoneNumber: phone ?? '',
email: email ?? '',
companyId: _sessionCubit.state.company!.id!,
note: '',
);
try {
final saved = await _repository.insertCustomer(newCustomer);
// Lo aggiungeremo in cima ai suggerimenti
return saved;
} catch (e) {
return null;
}
}
} }

View File

@@ -1,160 +0,0 @@
import 'dart:async'; // Serve per il Timer del debounce
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/features/customers/models/customer_model.dart';
import 'package:get_it/get_it.dart';
part 'customers_state.dart';
class CustomersCubit extends Cubit<CustomersState> {
final CustomerRepository _repository = GetIt.I<CustomerRepository>();
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
// Variabile per gestire il debounce della ricerca
Timer? _searchDebounce;
CustomersCubit() : super(const CustomersState());
// --- LETTURA ---
Future<void> loadCustomers() async {
emit(state.copyWith(status: CustomersStatus.loading));
try {
final customers = await _repository.getCustomers(
_sessionCubit.state.company!.id!,
);
emit(
state.copyWith(status: CustomersStatus.success, customers: customers),
);
} catch (e) {
emit(
state.copyWith(
status: CustomersStatus.failure,
errorMessage: e.toString(),
),
);
}
}
// --- CREAZIONE ---
Future<void> createCustomer(CustomerModel customer) async {
emit(state.copyWith(status: CustomersStatus.loading));
try {
final newCustomer = await _repository.saveCustomer(customer);
// Aggiorniamo la lista locale aggiungendo il nuovo cliente in cima
final updatedList = List<CustomerModel>.from(state.customers)
..insert(0, newCustomer);
emit(
state.copyWith(
status: CustomersStatus.success,
customers: updatedList,
lastCreatedCustomer: newCustomer,
),
);
} catch (e) {
emit(
state.copyWith(
status: CustomersStatus.failure,
errorMessage: e.toString(),
),
);
}
}
// --- AGGIORNAMENTO ---
Future<void> updateCustomer(CustomerModel customer) async {
emit(state.copyWith(status: CustomersStatus.loading));
try {
final updatedCustomer = await _repository.updateCustomer(customer);
final updatedList = List<CustomerModel>.from(state.customers);
final index = updatedList.indexWhere((c) => c.id == updatedCustomer.id);
if (index != -1) {
updatedList[index] = updatedCustomer;
}
emit(
state.copyWith(
status: CustomersStatus.success,
customers: updatedList,
lastCreatedCustomer:
updatedCustomer, // Utile se modifichi un cliente appena creato
),
);
} catch (e) {
emit(
state.copyWith(
status: CustomersStatus.failure,
errorMessage: e.toString(),
),
);
}
}
// --- RICERCA CON DEBOUNCE ---
void searchCustomers(String query) {
// 1. Se c'è già una ricerca in attesa (l'utente sta digitando veloce), la annulliamo
if (_searchDebounce?.isActive ?? false) _searchDebounce!.cancel();
// 2. Facciamo partire un timer di 400 millisecondi
_searchDebounce = Timer(const Duration(milliseconds: 300), () async {
// Se cancella tutto e la query è vuota, ricarichiamo la lista base
if (query.trim().isEmpty) {
await loadCustomers();
return;
}
// Nessun "loading" state qui, per evitare sfarfallii visivi mentre si scrive
try {
final results = await _repository.searchCustomers(
_sessionCubit.state.company!.id!,
query,
);
emit(
state.copyWith(status: CustomersStatus.success, customers: results),
);
} catch (e) {
emit(
state.copyWith(
status: CustomersStatus.failure,
errorMessage: e.toString(),
),
);
}
});
}
Future<CustomerModel?> quickCreateCustomer({
required String name,
String? phone,
String? email,
}) async {
final newCustomer = CustomerModel(
name: name,
phoneNumber: phone ?? '',
email: email ?? '',
companyId: _sessionCubit.state.company!.id!,
note: '',
);
try {
final saved = await _repository.saveCustomer(newCustomer);
// Lo aggiungiamo in cima ai suggerimenti
emit(state.copyWith(customers: [saved, ...state.customers]));
return saved;
} catch (e) {
return null;
}
}
// Pulizia della memoria quando il Cubit viene distrutto
@override
Future<void> close() {
_searchDebounce?.cancel();
return super.close();
}
}

View File

@@ -0,0 +1,85 @@
import 'dart:async'; // Serve per il Timer del debounce
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/features/customers/data/customer_repository.dart';
import 'package:flux/features/customers/models/customer_model.dart';
import 'package:get_it/get_it.dart';
part 'customers_list_state.dart';
class CustomersListCubit extends Cubit<CustomersListState> {
final CustomerRepository _repository = GetIt.I<CustomerRepository>();
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
// Variabile per gestire il debounce della ricerca
Timer? _searchDebounce;
CustomersListCubit() : super(const CustomersListState());
// --- LETTURA ---
Future<void> loadCustomers() async {
emit(state.copyWith(status: CustomersListStatus.loading));
try {
final customers = await _repository.getCustomers(
_sessionCubit.state.company!.id!,
);
emit(
state.copyWith(
status: CustomersListStatus.success,
customers: customers,
),
);
} catch (e) {
emit(
state.copyWith(
status: CustomersListStatus.failure,
errorMessage: e.toString(),
),
);
}
}
// --- RICERCA CON DEBOUNCE ---
void searchCustomers(String query) {
// 1. Se c'è già una ricerca in attesa (l'utente sta digitando veloce), la annulliamo
if (_searchDebounce?.isActive ?? false) _searchDebounce!.cancel();
// 2. Facciamo partire un timer di 400 millisecondi
_searchDebounce = Timer(const Duration(milliseconds: 300), () async {
// Se cancella tutto e la query è vuota, ricarichiamo la lista base
if (query.trim().isEmpty) {
await loadCustomers();
return;
}
// Nessun "loading" state qui, per evitare sfarfallii visivi mentre si scrive
try {
final results = await _repository.searchCustomers(
_sessionCubit.state.company!.id!,
query,
);
emit(
state.copyWith(
status: CustomersListStatus.success,
customers: results,
),
);
} catch (e) {
emit(
state.copyWith(
status: CustomersListStatus.failure,
errorMessage: e.toString(),
),
);
}
});
}
// Pulizia della memoria quando il Cubit viene distrutto
@override
Future<void> close() {
_searchDebounce?.cancel();
return super.close();
}
}

View File

@@ -1,6 +1,6 @@
part of 'customers_cubit.dart'; part of 'customers_list_cubit.dart';
enum CustomersStatus { enum CustomersListStatus {
initial, initial,
loading, loading,
filesLoading, filesLoading,
@@ -9,26 +9,26 @@ enum CustomersStatus {
failure, failure,
} }
class CustomersState extends Equatable { class CustomersListState extends Equatable {
final CustomersStatus status; final CustomersListStatus status;
final List<CustomerModel> customers; final List<CustomerModel> customers;
final CustomerModel? lastCreatedCustomer; final CustomerModel? lastCreatedCustomer;
final String? errorMessage; final String? errorMessage;
const CustomersState({ const CustomersListState({
this.status = CustomersStatus.initial, this.status = CustomersListStatus.initial,
this.customers = const [], this.customers = const [],
this.lastCreatedCustomer, this.lastCreatedCustomer,
this.errorMessage, this.errorMessage,
}); });
CustomersState copyWith({ CustomersListState copyWith({
CustomersStatus? status, CustomersListStatus? status,
List<CustomerModel>? customers, List<CustomerModel>? customers,
CustomerModel? lastCreatedCustomer, CustomerModel? lastCreatedCustomer,
String? errorMessage, String? errorMessage,
}) { }) {
return CustomersState( return CustomersListState(
status: status ?? this.status, status: status ?? this.status,
customers: customers ?? this.customers, customers: customers ?? this.customers,
lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer, lastCreatedCustomer: lastCreatedCustomer ?? this.lastCreatedCustomer,

View File

@@ -1,20 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/widgets/flux_text_field.dart'; import 'package:flux/core/widgets/flux_text_field.dart';
import 'package:flux/core/widgets/shared_forms/attachments_section.dart';
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
import 'package:flux/features/customers/blocs/customer_form_cubit.dart'; import 'package:flux/features/customers/blocs/customer_form_cubit.dart';
import 'package:flux/features/customers/models/customer_model.dart'; // Uso il tuo widget! import 'package:flux/features/customers/models/customer_model.dart'; // Uso il tuo widget!
class CustomerForm extends StatefulWidget { class CustomerFormScreen extends StatefulWidget {
final CustomerModel? customer; final CustomerModel? customer;
final String? customerId; final String? customerId;
const CustomerForm({super.key, this.customer, this.customerId}); const CustomerFormScreen({super.key, this.customer, this.customerId});
@override @override
State<CustomerForm> createState() => _CustomerFormState(); State<CustomerFormScreen> createState() => _CustomerFormScreenState();
} }
class _CustomerFormState extends State<CustomerForm> { class _CustomerFormScreenState extends State<CustomerFormScreen> {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
// Controller inizializzati con i dati del cliente (se presenti) // Controller inizializzati con i dati del cliente (se presenti)
@@ -27,10 +29,18 @@ class _CustomerFormState extends State<CustomerForm> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// 1. Lanciamo l'inizializzazione (che se è sincrona, finirà istantaneamente)
context.read<CustomerFormCubit>().initForm( context.read<CustomerFormCubit>().initForm(
customerId: widget.customerId, customerId: widget.customerId,
existingCustomer: widget.customer, existingCustomer: widget.customer,
); );
// 2. Leggiamo lo stato SUBITO DOPO. Se è già ready, non aspettiamo il listener!
final currentState = context.read<CustomerFormCubit>().state;
if (currentState.status == CustomerFormStatus.ready && !_isInitialized) {
_syncTextControllers(currentState.customer);
}
} }
@override @override
@@ -144,6 +154,17 @@ class _CustomerFormState extends State<CustomerForm> {
.read<CustomerFormCubit>() .read<CustomerFormCubit>()
.updateDoNotDisturb(v), .updateDoNotDisturb(v),
), ),
const Divider(height: 32),
BlocProvider<AttachmentsBloc>(
create: (context) => AttachmentsBloc(
parentType: AttachmentParentType.customer,
parentId: state.customer.id,
),
child: SharedAttachmentsSection(
parentType: AttachmentParentType.customer,
parentId: state.customer.id,
),
),
const SizedBox(height: 24), const SizedBox(height: 24),
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,

View File

@@ -3,9 +3,8 @@ 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/routes/routes.dart'; import 'package:flux/core/routes/routes.dart';
import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/features/customers/blocs/customers_cubit.dart'; import 'package:flux/features/customers/blocs/customers_list_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/customer_form.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
class CustomersListScreen extends StatefulWidget { class CustomersListScreen extends StatefulWidget {
@@ -27,14 +26,14 @@ class _CustomersListScreenState extends State<CustomersListScreen> {
void _loadInitialCustomers() { void _loadInitialCustomers() {
final companyId = context.read<SessionCubit>().state.company?.id; final companyId = context.read<SessionCubit>().state.company?.id;
if (companyId != null) { if (companyId != null) {
context.read<CustomersCubit>().loadCustomers(); context.read<CustomersListCubit>().loadCustomers();
} }
} }
void _onSearch(String query) { void _onSearch(String query) {
final companyId = context.read<SessionCubit>().state.company?.id; final companyId = context.read<SessionCubit>().state.company?.id;
if (companyId != null) { if (companyId != null) {
context.read<CustomersCubit>().searchCustomers(query); context.read<CustomersListCubit>().searchCustomers(query);
} }
} }
@@ -53,7 +52,12 @@ class _CustomersListScreenState extends State<CustomersListScreen> {
Padding( Padding(
padding: const EdgeInsets.only(right: 16), padding: const EdgeInsets.only(right: 16),
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () => openCustomerForm(context: context), onPressed: () {
context.pushNamed(
Routes.customerForm,
pathParameters: {'id': 'new'},
);
},
icon: const Icon(Icons.person_add_alt_1_rounded, size: 20), icon: const Icon(Icons.person_add_alt_1_rounded, size: 20),
label: const Text('NUOVO'), label: const Text('NUOVO'),
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@@ -87,9 +91,9 @@ class _CustomersListScreenState extends State<CustomersListScreen> {
// LISTA CLIENTI // LISTA CLIENTI
Expanded( Expanded(
child: BlocBuilder<CustomersCubit, CustomersState>( child: BlocBuilder<CustomersListCubit, CustomersListState>(
builder: (context, state) { builder: (context, state) {
if (state.status == CustomersStatus.loading && if (state.status == CustomersListStatus.loading &&
state.customers.isEmpty) { state.customers.isEmpty) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
@@ -214,8 +218,16 @@ class _CustomerTile extends StatelessWidget {
), ),
), ),
trailing: IconButton( trailing: IconButton(
onPressed: () => onPressed: () async {
openCustomerForm(context: context, customer: customer), final CustomersListCubit customersCubit = context
.read<CustomersListCubit>();
await context.pushNamed(
Routes.customerForm,
pathParameters: {'id': customer.id!},
extra: customer,
);
customersCubit.loadCustomers();
},
icon: Icon(Icons.edit_note_rounded, color: context.accent), icon: Icon(Icons.edit_note_rounded, color: context.accent),
), ),
), ),
@@ -224,7 +236,7 @@ class _CustomerTile extends StatelessWidget {
} }
/// Funzione unica per gestire Creazione e Modifica /// Funzione unica per gestire Creazione e Modifica
void openCustomerForm({ /* void openCustomerForm({
CustomerModel? customer, CustomerModel? customer,
required BuildContext context, required BuildContext context,
}) { }) {
@@ -257,4 +269,4 @@ void openCustomerForm({
), ),
), ),
); );
} } */

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/customers/blocs/customers_cubit.dart'; import 'package:flux/features/customers/blocs/customer_form_cubit.dart';
class QuickCustomerDialog extends StatefulWidget { class QuickCustomerDialog extends StatefulWidget {
final String initialQuery; final String initialQuery;
@@ -43,13 +43,11 @@ class _QuickCustomerDialogState extends State<QuickCustomerDialog> {
// Chiamata al Cubit (aggiorna i parametri in base a come li hai definiti) // Chiamata al Cubit (aggiorna i parametri in base a come li hai definiti)
final newCustomer = await context final newCustomer = await context
.read<CustomersCubit>() .read<CustomerFormCubit>()
.quickCreateCustomer( .quickCreateCustomer(
name: _nameCtrl.text.trim(), name: _nameCtrl.text.trim(),
phone: _phoneCtrl.text.trim(), phone: _phoneCtrl.text.trim(),
// Aggiungi questi se li hai inseriti nel tuo CustomerCubit: email: _emailCtrl.text.trim(),
// email: _emailCtrl.text.trim(),
// note: _noteCtrl.text.trim(),
); );
setState(() => _isLoading = false); setState(() => _isLoading = false);

View File

@@ -84,6 +84,14 @@ class _OnboardingScreenState extends State<OnboardingScreen> {
}, },
builder: (context, state) { builder: (context, state) {
return Scaffold( return Scaffold(
appBar: AppBar(
actions: [
IconButton.filled(
onPressed: () => context.read<SessionCubit>().signOut(),
icon: const Icon(Icons.logout),
),
],
),
body: SafeArea( body: SafeArea(
child: Stack( child: Stack(
children: [ children: [

View File

@@ -5,8 +5,7 @@ import 'package:flux/features/operations/blocs/operation_form_cubit.dart';
import 'package:flux/features/operations/models/operation_model.dart'; import 'package:flux/features/operations/models/operation_model.dart';
import 'package:flux/core/widgets/shared_forms/customer_section.dart'; import 'package:flux/core/widgets/shared_forms/customer_section.dart';
import 'package:flux/features/operations/ui/widgets/details_section.dart'; import 'package:flux/features/operations/ui/widgets/details_section.dart';
import 'package:flux/core/widgets/shared_forms/attachments_section.dart'; import 'package:flux/core/widgets/shared_forms/shared_files_section.dart'; // <- Cambiato ad un file unico per coerenza col ticket
import 'package:flux/core/widgets/shared_forms/staff_section.dart';
class OperationFormScreen extends StatefulWidget { class OperationFormScreen extends StatefulWidget {
final String? operationId; final String? operationId;
@@ -35,6 +34,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
'MNP', 'MNP',
'NIP', 'NIP',
'UNICA', 'UNICA',
'FWA',
'TELEPASS', 'TELEPASS',
'Energy', 'Energy',
'Fin', 'Fin',
@@ -47,10 +47,17 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// 1. Lanciamo l'inizializzazione sincrona/asincrona
context.read<OperationFormCubit>().initForm( context.read<OperationFormCubit>().initForm(
existingOperation: widget.existingOperation, existingOperation: widget.existingOperation,
operationId: widget.operationId, operationId: widget.operationId,
); );
// 2. Lettura immediata dello stato (come fatto per il customer!)
final currentState = context.read<OperationFormCubit>().state;
if (currentState.status == OperationFormStatus.ready && !_isInitialized) {
_syncTextControllers(currentState.operation);
}
} }
@override @override
@@ -75,16 +82,6 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
if (_freeTextDescriptionController.text.isEmpty) { if (_freeTextDescriptionController.text.isEmpty) {
_freeTextDescriptionController.text = model.description ?? ''; _freeTextDescriptionController.text = model.description ?? '';
} }
// Se è una nuova pratica (draft), impostiamo di default il target su OK per comodità UI
if (model.id == null && model.status == OperationStatus.draft) {
// Usiamo addPostFrameCallback per non interferire con il build attuale
WidgetsBinding.instance.addPostFrameCallback((_) {
// Supponendo tu aggiunga la possibilità di aggiornare lo status nel metodo updateFields del Cubit
// context.read<OperationFormCubit>().updateFields(status: OperationStatus.ok);
});
}
_isInitialized = true; _isInitialized = true;
} }
@@ -103,6 +100,9 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
}) { }) {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
_flushControllersToCubit(); _flushControllersToCubit();
// Aggiorniamo prima lo stato bersaglio nel cubit
context.read<OperationFormCubit>().updateFields(status: targetStatus);
// Poi chiamiamo il salvataggio
context.read<OperationFormCubit>().saveOperation( context.read<OperationFormCubit>().saveOperation(
targetStatus: targetStatus, targetStatus: targetStatus,
keepAdding: keepAdding, keepAdding: keepAdding,
@@ -114,7 +114,8 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
if (!_formKey.currentState!.validate()) return null; if (!_formKey.currentState!.validate()) return null;
_flushControllersToCubit(); _flushControllersToCubit();
final attachmentsBloc = context.read<AttachmentsBloc>(); final attachmentsBloc = context.read<AttachmentsBloc>();
// Presumo tu abbia creato il metodo saveOperationDraft() nel Cubit!
// Assicurati che questo metodo esista nel Cubit (come per il Ticket)
final newId = await context.read<OperationFormCubit>().saveOperationDraft(); final newId = await context.read<OperationFormCubit>().saveOperationDraft();
if (newId != null && context.mounted) { if (newId != null && context.mounted) {
attachmentsBloc.add(ParentEntitySavedEvent(newId)); attachmentsBloc.add(ParentEntitySavedEvent(newId));
@@ -122,7 +123,6 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
return newId; return newId;
} }
// Helper per assegnare un colore agli stati
Color _getStatusColor(OperationStatus status) { Color _getStatusColor(OperationStatus status) {
switch (status) { switch (status) {
case OperationStatus.success: case OperationStatus.success:
@@ -158,6 +158,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
); );
_freeTextSubtypeController.clear(); _freeTextSubtypeController.clear();
_freeTextDescriptionController.clear(); _freeTextDescriptionController.clear();
_referenceController.clear();
} else if (state.status == OperationFormStatus.failure) { } else if (state.status == OperationFormStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
SnackBar( SnackBar(
@@ -176,8 +177,6 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
); );
} }
// Determiniamo lo stato da mostrare nel form.
// Se è una bozza appena creata, mostriamo visivamente "OK" come default per il salvataggio.
final displayStatus = final displayStatus =
state.operation.status == OperationStatus.draft && state.operation.status == OperationStatus.draft &&
state.operation.id == null state.operation.id == null
@@ -191,7 +190,6 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
? 'Nuova Pratica - Operatore: ${state.operation.staffDisplayName}' ? 'Nuova Pratica - Operatore: ${state.operation.staffDisplayName}'
: 'Modifica Pratica - Operatore: ${state.operation.staffDisplayName}', : 'Modifica Pratica - Operatore: ${state.operation.staffDisplayName}',
), ),
// Mettiamo un piccolo indicatore visivo anche nella AppBar se non è OK
actions: actions:
displayStatus != OperationStatus.success && displayStatus != OperationStatus.success &&
displayStatus != OperationStatus.draft displayStatus != OperationStatus.draft
@@ -214,64 +212,54 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
), ),
body: Form( body: Form(
key: _formKey, key: _formKey,
child: FocusTraversalGroup(
policy: WidgetOrderTraversalPolicy(),
child: LayoutBuilder( child: LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final isUltraWide = constraints.maxWidth > 1400; final isUltraWide = constraints.maxWidth > 1400;
final isDesktop = constraints.maxWidth > 900; final isDesktop = constraints.maxWidth > 900;
if (isUltraWide) {
return _buildUltraWide(state, theme); return SingleChildScrollView(
} else if (isDesktop) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
flex: 7,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: _buildMainFormContent( child: Center(
theme, child: ConstrainedBox(
constraints: BoxConstraints(
maxWidth: isUltraWide
? 1600
: (isDesktop ? 1200 : 800),
),
child: _buildResponsiveLayout(
isUltraWide,
isDesktop,
state, state,
displayStatus, displayStatus,
), ),
), ),
), ),
VerticalDivider(width: 1, color: theme.dividerColor),
Expanded(
flex: 3,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: _buildNotesSection(isDesktop: true),
),
),
],
); );
} else {
return SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMainFormContent(theme, state, displayStatus),
const Divider(height: 32),
_buildNotesSection(isDesktop: false),
const SizedBox(height: 80),
],
),
);
}
}, },
), ),
), ),
),
bottomNavigationBar: SafeArea( bottomNavigationBar: SafeArea(
child: Padding( child: Container(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: theme.scaffoldBackgroundColor,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 10,
offset: const Offset(0, -3),
),
],
),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
flex: 1, flex: 1,
child: ElevatedButton( child: ElevatedButton(
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
// Se c'è un KO o un blocco, cambiamo il colore del bottone principale per attirare l'attenzione
backgroundColor: backgroundColor:
displayStatus != OperationStatus.success && displayStatus != OperationStatus.success &&
displayStatus != OperationStatus.draft displayStatus != OperationStatus.draft
@@ -287,8 +275,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
? null ? null
: () => _saveOperation( : () => _saveOperation(
keepAdding: false, keepAdding: false,
targetStatus: targetStatus: displayStatus,
displayStatus, // <-- Usiamo lo stato selezionato nel form!
), ),
child: state.status == OperationFormStatus.saving child: state.status == OperationFormStatus.saving
? const SizedBox( ? const SizedBox(
@@ -302,7 +289,6 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
: const Text('Salva ed Esci'), : const Text('Salva ed Esci'),
), ),
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded( Expanded(
flex: 1, flex: 1,
@@ -311,8 +297,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
? null ? null
: () => _saveOperation( : () => _saveOperation(
keepAdding: true, keepAdding: true,
targetStatus: targetStatus: displayStatus,
displayStatus, // <-- Usiamo lo stato selezionato nel form!
), ),
child: const Text( child: const Text(
'Salva e Aggiungi Altro', 'Salva e Aggiungi Altro',
@@ -329,72 +314,100 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
); );
} }
Widget _buildUltraWide(OperationFormState state, ThemeData theme) { // --- LOGICA DI IMPAGINAZIONE RESPONSIVE ---
Widget _buildResponsiveLayout(
bool isUltraWide,
bool isDesktop,
OperationFormState state,
OperationStatus displayStatus,
) {
if (isUltraWide) {
// 3 COLONNE
return Row( return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
flex: 4,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
children: [_cardAnagrafica(state), _cardEsito(state)],
),
),
const SizedBox(width: 24),
Expanded(child: Column(children: [_cardDettagli(state)])),
const SizedBox(width: 24),
Expanded(
child: Column(children: [_cardNote(state), _cardAllegati(state)]),
),
],
);
} else if (isDesktop) {
// 2 COLONNE
return Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_buildStaffSection(state),
const Divider(height: 50),
_buildOperationStatusSection(state),
const Divider(height: 32),
_buildCustomerSection(state),
const SizedBox(height: 16),
_buildReferenceSection(state),
const Divider(height: 50),
_buildOperationTypeSection(state),
const SizedBox(height: 16),
_buildQuantitySection(state),
const Divider(height: 50),
_buildDetailsSection(state),
const Divider(height: 50),
],
),
),
),
VerticalDivider(width: 1, color: theme.dividerColor),
Expanded( Expanded(
flex: 3, child: Column(
child: Padding( children: [
padding: const EdgeInsets.all(16.0), _cardAnagrafica(state),
child: _buildNotesSection(isDesktop: true), _cardEsito(state),
), _cardAllegati(state),
), ],
VerticalDivider(width: 1, color: theme.dividerColor), ),
Expanded( ),
flex: 3, const SizedBox(width: 24),
child: SingleChildScrollView( Expanded(
padding: const EdgeInsets.all(16.0), child: Column(children: [_cardDettagli(state), _cardNote(state)]),
child: _buildAttachmentSection(state), ),
), ],
), );
], } else {
); // 1 COLONNA (Mobile)
} return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
Widget _buildStaffSection(OperationFormState state) { children: [
return StaffSection( _cardAnagrafica(state),
staffId: state.operation.staffId, _cardEsito(state),
staffName: state.operation.staffDisplayName, _cardDettagli(state),
onStaffSelected: (staff) => { _cardNote(state),
context.read<OperationFormCubit>().updateFields( _cardAllegati(state),
staffId: staff.id, ],
staffDisplayName: staff.name, );
), }
}, }
);
} // --- LE CARD MODULARIZZATE E COLORATE ---
Widget _buildOperationStatusSection(OperationFormState state) { Widget _cardAnagrafica(OperationFormState state) {
return Column( return _buildCard(
title: 'Cliente e Riferimento',
icon: Icons.person,
themeColor: Colors.indigo,
children: [
SharedCustomerSection(
customer: state.operation.customer,
onCustomerSelected: (customer) => context
.read<OperationFormCubit>()
.updateFields(customer: customer),
),
const SizedBox(height: 16),
TextFormField(
controller: _referenceController,
decoration: const InputDecoration(
labelText: 'Riferimento (es. Telefono, Targa...)',
prefixIcon: Icon(Icons.tag),
),
validator: (v) =>
v == null || v.isEmpty ? 'Inserisci un riferimento' : null,
),
],
);
}
Widget _cardEsito(OperationFormState state) {
return _buildCard(
title: 'Esito Pratica',
icon: Icons.fact_check,
themeColor: _getStatusColor(state.operation.status),
children: [ children: [
_buildSectionTitle('Esito / Stato Operazione'),
Container( Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -416,12 +429,8 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
Icons.arrow_drop_down, Icons.arrow_drop_down,
color: _getStatusColor(state.operation.status), color: _getStatusColor(state.operation.status),
), ),
items: OperationStatus.values items: OperationStatus.values.map((status) {
/* .where( return DropdownMenuItem(
(s) => s != OperationStatus.draft,
) // Nascondiamo 'Bozza' dal menu */
.map(
(status) => DropdownMenuItem(
value: status, value: status,
child: Row( child: Row(
children: [ children: [
@@ -442,16 +451,13 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
), ),
], ],
), ),
), );
) }).toList(),
.toList(),
onChanged: (newStatus) { onChanged: (newStatus) {
if (newStatus != null) { if (newStatus != null)
// Assicurati che il metodo updateFields nel tuo Cubit accetti anche 'status'
context.read<OperationFormCubit>().updateFields( context.read<OperationFormCubit>().updateFields(
status: newStatus, status: newStatus,
); );
}
}, },
), ),
), ),
@@ -459,37 +465,20 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
state.operation.status == OperationStatus.success state.operation.status == OperationStatus.success
? 'Lascia OK se la pratica è stata caricata con successo.' ? 'Lascia OK se caricata con successo.'
: 'Attenzione: la pratica verrà salvata come ${state.operation.status.displayName}.', : 'Attenzione: pratica salvata in stato anomalo.',
style: TextStyle(fontSize: 12, color: Colors.grey.shade600), style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
), ),
], ],
); );
} }
Widget _buildCustomerSection(OperationFormState state) { Widget _cardDettagli(OperationFormState state) {
return SharedCustomerSection( return _buildCard(
customer: state.operation.customer, title: 'Dettagli Servizio',
onCustomerSelected: (customer) { icon: Icons.design_services,
context.read<OperationFormCubit>().updateFields(customer: customer); themeColor: Colors.deepOrange,
},
);
}
Widget _buildReferenceSection(OperationFormState state) {
return TextFormField(
controller: _referenceController,
decoration: const InputDecoration(
labelText: 'Riferimento (es. numero di telefono, targa...)',
prefixIcon: Icon(Icons.tag),
),
);
}
Widget _buildOperationTypeSection(OperationFormState state) {
return Column(
children: [ children: [
_buildSectionTitle('Cosa stiamo facendo?'),
Wrap( Wrap(
spacing: 8.0, spacing: 8.0,
runSpacing: 8.0, runSpacing: 8.0,
@@ -498,104 +487,140 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
label: Text(type), label: Text(type),
selected: state.operation.type == type, selected: state.operation.type == type,
onSelected: (selected) { onSelected: (selected) {
if (selected) { if (selected)
context.read<OperationFormCubit>().setTypeWithSmartDefault( context.read<OperationFormCubit>().setTypeWithSmartDefault(
type, type,
); );
}
}, },
); );
}).toList(), }).toList(),
), ),
], const Divider(height: 32),
); Row(
}
Widget _buildDetailsSection(OperationFormState state) {
return Column(
children: [ children: [
_buildSectionTitle('Dettagli Servizio'), const Text(
OperationDetailsSection( 'Quantità:',
currentOp: state.operation, style: TextStyle(fontWeight: FontWeight.bold),
currentType: state.operation.type,
freeTextSubtypeController: _freeTextSubtypeController,
freeTextDescriptionController: _freeTextDescriptionController,
durationQuickPicks: _buildDurationQuickPicks(state.operation),
), ),
], const Spacer(),
);
}
Widget _buildQuantitySection(OperationFormState state) {
return Row(
children: [
const Text('Quantità: '),
IconButton( IconButton(
icon: const Icon(Icons.remove), icon: const Icon(Icons.remove_circle_outline),
onPressed: () { onPressed: state.operation.quantity > 1
final q = state.operation.quantity; ? () => context.read<OperationFormCubit>().updateFields(
if (q > 1) { quantity: state.operation.quantity - 1,
context.read<OperationFormCubit>().updateFields(quantity: q - 1); )
} : null,
},
), ),
Text( Text(
'${state.operation.quantity}', '${state.operation.quantity}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold), style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
), ),
IconButton( IconButton(
icon: const Icon(Icons.add), icon: const Icon(Icons.add_circle_outline),
onPressed: () { onPressed: () => context.read<OperationFormCubit>().updateFields(
final q = state.operation.quantity; quantity: state.operation.quantity + 1,
context.read<OperationFormCubit>().updateFields(quantity: q + 1); ),
}, ),
],
),
const Divider(height: 32),
OperationDetailsSection(
currentOp: state.operation,
currentType: state.operation.type,
freeTextSubtypeController: _freeTextSubtypeController,
freeTextDescriptionController: _freeTextDescriptionController,
durationQuickPicks: _buildDurationQuickPicks(),
), ),
], ],
); );
} }
Widget _buildAttachmentSection(OperationFormState state) { Widget _cardNote(OperationFormState state) {
return SharedAttachmentsSection( return _buildCard(
parentType: AttachmentParentType.operation, title: 'Note Interne',
parentId: state.operation.id, icon: Icons.notes,
titleForUpload: state.operation.customer?.name ?? 'Nuova pratica', themeColor: Colors.teal,
onGenerateIdForQr: _generateIdForQr,
);
}
Widget _buildMainFormContent(
ThemeData theme,
OperationFormState state,
OperationStatus displayStatus, {
bool showFiles = true,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
/* _buildStaffSection(state), TextFormField(
const Divider(height: 50), */ controller: _noteController,
_buildOperationStatusSection(state), maxLines: 5,
const Divider(height: 32), decoration: InputDecoration(
_buildCustomerSection(state), hintText: 'Incolla seriali, ICCID, IBAN...',
const SizedBox(height: 16), alignLabelWithHint: true,
_buildReferenceSection(state), fillColor: Colors.teal.withValues(alpha: 0.05),
const Divider(height: 50), filled: true,
_buildOperationTypeSection(state), border: OutlineInputBorder(
const SizedBox(height: 16), borderRadius: BorderRadius.circular(12),
_buildQuantitySection(state), borderSide: BorderSide.none,
const Divider(height: 50), ),
_buildDetailsSection(state), ),
const Divider(height: 50), ),
// QUANTITÀ
const Divider(height: 32),
if (showFiles) ...[_buildAttachmentSection(state)],
], ],
); );
} }
Widget _buildDurationQuickPicks(OperationModel? currentOp) { Widget _cardAllegati(OperationFormState state) {
return _buildCard(
title: 'Allegati e Documenti',
icon: Icons.attach_file,
themeColor: Colors.deepPurple,
children: [
SharedFilesSection(
titleNameForUpload: state.operation.customer?.name ?? 'Nuova Pratica',
onGenerateIdForQr: _generateIdForQr,
),
],
);
}
// --- WIDGET BASE PER LA CARD ---
Widget _buildCard({
required String title,
required IconData icon,
required Color themeColor,
required List<Widget> children,
}) {
return Card(
margin: const EdgeInsets.only(bottom: 24),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: themeColor.withValues(alpha: 0.3), width: 1),
),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: themeColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: themeColor),
),
const SizedBox(width: 12),
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: themeColor,
),
),
],
),
const Divider(height: 32),
...children,
],
),
),
);
}
Widget _buildDurationQuickPicks() {
final durations = [3, 6, 12, 24, 30, 36, 48]; final durations = [3, 6, 12, 24, 30, 36, 48];
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -636,46 +661,4 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
], ],
); );
} }
Widget _buildNotesSection({required bool isDesktop}) {
final title = _buildSectionTitle('Note Interne');
final noteField = TextFormField(
controller: _noteController,
keyboardType: TextInputType.multiline,
minLines: isDesktop ? null : 5,
maxLines: null,
expands: isDesktop,
textAlignVertical: TextAlignVertical.top,
decoration: const InputDecoration(
hintText: 'Incolla qui seriali, ICCID, IBAN, indirizzi...',
alignLabelWithHint: true,
border: OutlineInputBorder(),
),
);
return isDesktop
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title,
const SizedBox(height: 8),
Expanded(child: noteField),
],
)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [title, const SizedBox(height: 8), noteField],
);
}
Widget _buildSectionTitle(String title) {
return Padding(
padding: const EdgeInsets.only(bottom: 12.0),
child: Text(
title,
style: Theme.of(
context,
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),
);
}
} }

View File

@@ -33,7 +33,7 @@ class OperationDetailsSection extends StatelessWidget {
switch (operationType) { switch (operationType) {
case 'AL' || 'MNP': case 'AL' || 'MNP':
return provider.roles.contains(ProviderRole.mobile); return provider.roles.contains(ProviderRole.mobile);
case 'NIP': case 'NIP' || 'FWA':
return provider.roles.contains(ProviderRole.landline); return provider.roles.contains(ProviderRole.landline);
case 'UNICA': case 'UNICA':
return provider.roles.contains(ProviderRole.landline) || return provider.roles.contains(ProviderRole.landline) ||

View File

@@ -160,7 +160,7 @@ class TicketShippingCubit extends Cubit<TicketShippingState> {
await _repository.createShipmentWithPdf( await _repository.createShipmentWithPdf(
document: state.document, document: state.document,
pdfBytes: pdfBytes, pdfBytes: pdfBytes,
newTicketStatus: newTicketStatus.value, newTicketStatus: newTicketStatus,
); );
await Printing.layoutPdf( await Printing.layoutPdf(
onLayout: (format) async => pdfBytes, onLayout: (format) async => pdfBytes,

View File

@@ -6,6 +6,7 @@ import 'package:flux/features/attachments/data/attachments_repository.dart';
import 'package:flux/features/tickets/models/shipping_document_model.dart'; import 'package:flux/features/tickets/models/shipping_document_model.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/master_data/providers/models/provider_role.dart'; import 'package:flux/features/master_data/providers/models/provider_role.dart';
import 'package:flux/features/tickets/models/ticket_model.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:supabase_flutter/supabase_flutter.dart' hide Bucket; import 'package:supabase_flutter/supabase_flutter.dart' hide Bucket;
@@ -90,7 +91,7 @@ class TicketsShippingRepository {
Future<void> createShipmentWithPdf({ Future<void> createShipmentWithPdf({
required ShippingDocumentModel document, required ShippingDocumentModel document,
required Uint8List pdfBytes, required Uint8List pdfBytes,
required String newTicketStatus, required TicketStatus newTicketStatus,
}) async { }) async {
try { try {
final attachmentsRepo = GetIt.I.get<AttachmentsRepository>(); final attachmentsRepo = GetIt.I.get<AttachmentsRepository>();
@@ -124,8 +125,8 @@ class TicketsShippingRepository {
await _supabase await _supabase
.from('ticket') .from('ticket')
.update({ .update({
'ticket_status': newTicketStatus, 'ticket_status': newTicketStatus.value,
'shipment_document_id': documentId, 'shipping_document_id': documentId,
}) })
.inFilter('id', document.ticketIds); .inFilter('id', document.ticketIds);
} catch (e) { } catch (e) {

View File

@@ -27,7 +27,7 @@ import 'package:flux/core/data/core_repository.dart';
import 'package:flux/core/routes/app_router.dart'; import 'package:flux/core/routes/app_router.dart';
import 'package:flux/core/theme/theme.dart'; import 'package:flux/core/theme/theme.dart';
import 'package:flux/core/theme/bloc/theme_bloc.dart'; import 'package:flux/core/theme/bloc/theme_bloc.dart';
import 'package:flux/features/customers/blocs/customers_cubit.dart'; import 'package:flux/features/customers/blocs/customers_list_cubit.dart';
import 'package:flux/features/customers/data/customer_repository.dart'; import 'package:flux/features/customers/data/customer_repository.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/data/product_repository.dart'; import 'package:flux/features/master_data/products/data/product_repository.dart';
@@ -59,7 +59,7 @@ void main() async {
// Cubit delle feature // Cubit delle feature
BlocProvider<StoreCubit>(create: (_) => StoreCubit()), BlocProvider<StoreCubit>(create: (_) => StoreCubit()),
BlocProvider<CustomersCubit>(create: (_) => CustomersCubit()), BlocProvider<CustomersListCubit>(create: (_) => CustomersListCubit()),
BlocProvider<ProductsCubit>(create: (_) => ProductsCubit()), BlocProvider<ProductsCubit>(create: (_) => ProductsCubit()),
BlocProvider<StaffCubit>( BlocProvider<StaffCubit>(
create: (_) => StaffCubit() create: (_) => StaffCubit()