j
This commit is contained in:
@@ -143,6 +143,10 @@ class SessionCubit extends Cubit<SessionState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void updateCurrentCompany(CompanyModel newCompany) {
|
||||||
|
emit(state.copyWith(company: newCompany));
|
||||||
|
}
|
||||||
|
|
||||||
// --- FUNZIONE EXTRA: CAMBIO NEGOZIO DALLA DASHBOARD ---
|
// --- FUNZIONE EXTRA: CAMBIO NEGOZIO DALLA DASHBOARD ---
|
||||||
Future<void> changeStore(StoreModel newStore) async {
|
Future<void> changeStore(StoreModel newStore) async {
|
||||||
if (newStore.id != null) {
|
if (newStore.id != null) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import 'package:flux/core/widgets/set_password_screen.dart';
|
|||||||
import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart';
|
import 'package:flux/core/widgets/shared_forms/mobile_upload_screen.dart';
|
||||||
import 'package:flux/core/widgets/shared_forms/upload_success_screen.dart';
|
import 'package:flux/core/widgets/shared_forms/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/ui/company_settings_screen.dart';
|
||||||
import 'package:flux/features/customers/blocs/customers_cubit.dart';
|
import 'package:flux/features/customers/blocs/customers_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';
|
||||||
@@ -25,9 +26,10 @@ import 'package:flux/features/master_data/store/ui/stores_screen.dart';
|
|||||||
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
|
import 'package:flux/features/onboarding/blocs/onboarding_cubit.dart';
|
||||||
import 'package:flux/features/onboarding/ui/onboarding_screen.dart';
|
import 'package:flux/features/onboarding/ui/onboarding_screen.dart';
|
||||||
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
||||||
|
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/features/operations/ui/operation_form_screen.dart';
|
import 'package:flux/features/operations/ui/operation_form_screen.dart';
|
||||||
import 'package:flux/features/operations/ui/operations_screen.dart';
|
import 'package:flux/features/operations/ui/operation_list_screen.dart';
|
||||||
import 'package:flux/features/tickets/blocs/ticket_form_cubit.dart';
|
import 'package:flux/features/tickets/blocs/ticket_form_cubit.dart';
|
||||||
import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart';
|
import 'package:flux/features/tickets/blocs/ticket_list_cubit.dart';
|
||||||
import 'package:flux/features/tickets/models/ticket_model.dart';
|
import 'package:flux/features/tickets/models/ticket_model.dart';
|
||||||
@@ -128,6 +130,10 @@ class AppRouter {
|
|||||||
return const ProductsScreen();
|
return const ProductsScreen();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: 'company_settings',
|
||||||
|
builder: (context, state) => const CompanySettingsScreen(),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'staff', // Diventa /master-data/staff
|
path: 'staff', // Diventa /master-data/staff
|
||||||
builder: (context, state) => const StaffScreen(),
|
builder: (context, state) => const StaffScreen(),
|
||||||
@@ -160,7 +166,7 @@ class AppRouter {
|
|||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/operations',
|
path: '/operations',
|
||||||
builder: (context, state) => const OperationsScreen(),
|
builder: (context, state) => const OperationListScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/customers',
|
path: '/customers',
|
||||||
@@ -235,7 +241,6 @@ class AppRouter {
|
|||||||
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/operations/form/:id',
|
path: '/operations/form/:id',
|
||||||
name: 'operation-form',
|
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final String pathId = state.pathParameters['id'] ?? 'new';
|
final String pathId = state.pathParameters['id'] ?? 'new';
|
||||||
final OperationModel? operationFromExtra =
|
final OperationModel? operationFromExtra =
|
||||||
@@ -254,14 +259,19 @@ class AppRouter {
|
|||||||
context.read<ProductsCubit>().loadBrands();
|
context.read<ProductsCubit>().loadBrands();
|
||||||
context.read<StaffCubit>().loadStaffForStore(currentStoreId);
|
context.read<StaffCubit>().loadStaffForStore(currentStoreId);
|
||||||
|
|
||||||
return BlocProvider(
|
return MultiBlocProvider(
|
||||||
|
providers: [
|
||||||
|
BlocProvider(
|
||||||
create: (context) => AttachmentsBloc(
|
create: (context) => AttachmentsBloc(
|
||||||
parentId: realOperationId,
|
parentId: realOperationId,
|
||||||
parentType: AttachmentParentType.operation,
|
parentType: AttachmentParentType.operation,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
BlocProvider(create: (context) => OperationFormCubit()),
|
||||||
|
],
|
||||||
child: OperationFormScreen(
|
child: OperationFormScreen(
|
||||||
operationId: operationId ?? existingOperation?.id,
|
operationId: realOperationId,
|
||||||
existingOperation: existingOperation,
|
existingOperation: operationFromExtra,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:equatable/equatable.dart';
|
|
||||||
import 'package:flux/features/company/data/company_repository.dart';
|
|
||||||
import 'package:flux/features/company/models/company_model.dart';
|
|
||||||
import 'package:get_it/get_it.dart';
|
|
||||||
|
|
||||||
part 'company_events.dart';
|
|
||||||
part 'company_state.dart';
|
|
||||||
|
|
||||||
class CompanyBloc extends Bloc<CompanyEvent, CompanyState> {
|
|
||||||
final CompanyRepository _repository = GetIt.I<CompanyRepository>();
|
|
||||||
CompanyBloc() : super(const CompanyState(status: CompanyStatus.initial)) {
|
|
||||||
on<CreateCompanyRequested>((event, emit) async {
|
|
||||||
emit(const CompanyState(status: CompanyStatus.loading));
|
|
||||||
try {
|
|
||||||
final createdCompany = await _repository.createCompany(event.company);
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: CompanyStatus.success,
|
|
||||||
company: createdCompany,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: CompanyStatus.failure,
|
|
||||||
errorMessage: e.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
part of 'company_bloc.dart';
|
|
||||||
|
|
||||||
// lib/blocs/company/company_event.dart
|
|
||||||
|
|
||||||
abstract class CompanyEvent extends Equatable {
|
|
||||||
const CompanyEvent();
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [];
|
|
||||||
}
|
|
||||||
|
|
||||||
class CreateCompanyRequested extends CompanyEvent {
|
|
||||||
final CompanyModel company;
|
|
||||||
|
|
||||||
const CreateCompanyRequested({required this.company});
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [company];
|
|
||||||
}
|
|
||||||
117
lib/features/company/bloc/company_settings_cubit.dart
Normal file
117
lib/features/company/bloc/company_settings_cubit.dart
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter/foundation.dart'; // Per kIsWeb
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
|
import 'package:flux/features/company/data/company_repository.dart';
|
||||||
|
import 'package:flux/features/company/models/company_model.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
part 'company_settings_state.dart';
|
||||||
|
|
||||||
|
class CompanySettingsCubit extends Cubit<CompanySettingsState> {
|
||||||
|
final CompanyRepository _repository = GetIt.I<CompanyRepository>();
|
||||||
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||||
|
|
||||||
|
CompanySettingsCubit() : super(const CompanySettingsState());
|
||||||
|
|
||||||
|
void initSettings() {
|
||||||
|
final currentCompany = _sessionCubit.state.company;
|
||||||
|
if (currentCompany != null) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
company: currentCompany,
|
||||||
|
status: CompanySettingsStatus.ready,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFields({
|
||||||
|
String? name,
|
||||||
|
String? vatId,
|
||||||
|
String? address,
|
||||||
|
String? city,
|
||||||
|
String? zipCode,
|
||||||
|
String? phone,
|
||||||
|
String? email,
|
||||||
|
}) {
|
||||||
|
if (state.company == null) return;
|
||||||
|
|
||||||
|
final updated = state.company!.copyWith(
|
||||||
|
name: name ?? state.company!.name,
|
||||||
|
vatId: vatId ?? state.company!.vatId,
|
||||||
|
address: address ?? state.company!.address,
|
||||||
|
city: city ?? state.company!.city,
|
||||||
|
zipCode: zipCode ?? state.company!.zipCode,
|
||||||
|
phone: phone ?? state.company!.phone,
|
||||||
|
email: email ?? state.company!.email,
|
||||||
|
);
|
||||||
|
emit(state.copyWith(company: updated));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveSettings() async {
|
||||||
|
if (state.company == null) return;
|
||||||
|
emit(
|
||||||
|
state.copyWith(status: CompanySettingsStatus.saving, errorMessage: null),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Salva i dati su Supabase
|
||||||
|
final updatedCompany = await _repository.updateCompany(state.company!);
|
||||||
|
|
||||||
|
// 2. Aggiorna la sessione globale per riflettere i cambiamenti in tutta l'app
|
||||||
|
_sessionCubit.updateCurrentCompany(updatedCompany);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: CompanySettingsStatus.success,
|
||||||
|
company: updatedCompany,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: CompanySettingsStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metodo per gestire l'upload del logo
|
||||||
|
Future<void> uploadLogo(Uint8List bytes, String fileName) async {
|
||||||
|
if (state.company == null) return;
|
||||||
|
emit(state.copyWith(status: CompanySettingsStatus.uploadingLogo));
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Usa il tuo repository per caricare il file nel bucket 'company_logos'
|
||||||
|
// Il file può essere Uint8List (se sei su Web) o File (se sei su Mobile/Desktop)
|
||||||
|
final publicUrl = await _repository.uploadCompanyLogo(
|
||||||
|
companyId: state.company!.id!,
|
||||||
|
fileBytes: bytes,
|
||||||
|
fileName: fileName,
|
||||||
|
);
|
||||||
|
|
||||||
|
final updatedCompany = state.company!.copyWith(logoUrl: publicUrl);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
company: updatedCompany,
|
||||||
|
status: CompanySettingsStatus.ready,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Chiamiamo il salvataggio per rendere definitivo l'URL nel record della compagnia
|
||||||
|
await saveSettings();
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: CompanySettingsStatus.failure,
|
||||||
|
errorMessage: "Errore caricamento logo: $e",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
lib/features/company/bloc/company_settings_state.dart
Normal file
34
lib/features/company/bloc/company_settings_state.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
part of 'company_settings_cubit.dart';
|
||||||
|
|
||||||
|
class CompanySettingsState {
|
||||||
|
final CompanySettingsStatus status;
|
||||||
|
final CompanyModel? company;
|
||||||
|
final String? errorMessage;
|
||||||
|
|
||||||
|
const CompanySettingsState({
|
||||||
|
this.status = CompanySettingsStatus.initial,
|
||||||
|
this.company,
|
||||||
|
this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
CompanySettingsState copyWith({
|
||||||
|
CompanySettingsStatus? status,
|
||||||
|
CompanyModel? company,
|
||||||
|
String? errorMessage,
|
||||||
|
}) {
|
||||||
|
return CompanySettingsState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
company: company ?? this.company,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CompanySettingsStatus {
|
||||||
|
initial,
|
||||||
|
ready,
|
||||||
|
saving,
|
||||||
|
uploadingLogo,
|
||||||
|
success,
|
||||||
|
failure,
|
||||||
|
}
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
part of 'company_bloc.dart';
|
|
||||||
|
|
||||||
enum CompanyStatus { initial, loading, success, failure }
|
|
||||||
|
|
||||||
class CompanyState extends Equatable {
|
|
||||||
final CompanyStatus status;
|
|
||||||
final String? errorMessage;
|
|
||||||
final CompanyModel? company;
|
|
||||||
|
|
||||||
const CompanyState({required this.status, this.errorMessage, this.company});
|
|
||||||
|
|
||||||
CompanyState copyWith({
|
|
||||||
CompanyStatus? status,
|
|
||||||
String? errorMessage,
|
|
||||||
CompanyModel? company,
|
|
||||||
}) {
|
|
||||||
return CompanyState(
|
|
||||||
status: status ?? this.status,
|
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
|
||||||
company: company ?? this.company,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [status, errorMessage, company];
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import '../models/company_model.dart';
|
import '../models/company_model.dart';
|
||||||
|
|
||||||
@@ -21,6 +23,62 @@ class CompanyRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<CompanyModel> updateCompany(CompanyModel company) async {
|
||||||
|
try {
|
||||||
|
final response = await _supabase
|
||||||
|
.from('company')
|
||||||
|
.update(company.toMap())
|
||||||
|
.eq('id', company.id!)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return CompanyModel.fromMap(response);
|
||||||
|
} on PostgrestException catch (e) {
|
||||||
|
throw e.message;
|
||||||
|
} catch (e) {
|
||||||
|
throw e.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> uploadCompanyLogo({
|
||||||
|
required String companyId,
|
||||||
|
required Uint8List fileBytes,
|
||||||
|
required String fileName,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// 1. Prepariamo il path.
|
||||||
|
// Organizziamo per companyId e aggiungiamo un timestamp per evitare cache del browser
|
||||||
|
// quando l'utente cambia logo più volte.
|
||||||
|
final extension = fileName.split('.').last;
|
||||||
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final filePath = '$companyId/logo_$timestamp.$extension';
|
||||||
|
|
||||||
|
// 2. Caricamento fisico dei bytes
|
||||||
|
// Usiamo uploadBinary che è perfetto per Uint8List
|
||||||
|
await _supabase.storage
|
||||||
|
.from('company_logos')
|
||||||
|
.uploadBinary(
|
||||||
|
filePath,
|
||||||
|
fileBytes,
|
||||||
|
fileOptions: const FileOptions(
|
||||||
|
cacheControl: '3600',
|
||||||
|
upsert:
|
||||||
|
true, // Se esiste già un file con lo stesso nome, lo sovrascrive
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. Otteniamo l'URL pubblico.
|
||||||
|
// Nota: il bucket 'company_logos' deve essere impostato come PUBLIC su Supabase
|
||||||
|
final String publicUrl = _supabase.storage
|
||||||
|
.from('company_logos')
|
||||||
|
.getPublicUrl(filePath);
|
||||||
|
|
||||||
|
return publicUrl;
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception("Errore durante l'upload del logo: $e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<CompanyModel?> getCompany() async {
|
Future<CompanyModel?> getCompany() async {
|
||||||
try {
|
try {
|
||||||
final userId = _supabase.auth.currentUser?.id;
|
final userId = _supabase.auth.currentUser?.id;
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ class CompanyModel extends Equatable {
|
|||||||
final String vatId;
|
final String vatId;
|
||||||
final String fiscalCode;
|
final String fiscalCode;
|
||||||
final String sdi;
|
final String sdi;
|
||||||
final String companyLogo;
|
final String? phone;
|
||||||
|
final String? email;
|
||||||
|
final String? logoUrl;
|
||||||
|
|
||||||
// Stato Pagamenti (Ibride: manuale + Stripe)
|
// Stato Pagamenti (Ibride: manuale + Stripe)
|
||||||
final bool isPaid;
|
final bool isPaid;
|
||||||
@@ -78,7 +80,9 @@ class CompanyModel extends Equatable {
|
|||||||
required this.vatId,
|
required this.vatId,
|
||||||
required this.fiscalCode,
|
required this.fiscalCode,
|
||||||
required this.sdi,
|
required this.sdi,
|
||||||
this.companyLogo = '',
|
this.phone,
|
||||||
|
this.email,
|
||||||
|
this.logoUrl,
|
||||||
this.isPaid = false,
|
this.isPaid = false,
|
||||||
this.paymentExpiration,
|
this.paymentExpiration,
|
||||||
this.subscriptionTier = SubscriptionTier.free,
|
this.subscriptionTier = SubscriptionTier.free,
|
||||||
@@ -100,7 +104,9 @@ class CompanyModel extends Equatable {
|
|||||||
String? vatId,
|
String? vatId,
|
||||||
String? fiscalCode,
|
String? fiscalCode,
|
||||||
String? sdi,
|
String? sdi,
|
||||||
String? companyLogo,
|
String? logoUrl,
|
||||||
|
String? phone,
|
||||||
|
String? email,
|
||||||
bool? isPaid,
|
bool? isPaid,
|
||||||
DateTime? paymentExpiration,
|
DateTime? paymentExpiration,
|
||||||
SubscriptionTier? subscriptionTier,
|
SubscriptionTier? subscriptionTier,
|
||||||
@@ -121,7 +127,9 @@ class CompanyModel extends Equatable {
|
|||||||
vatId: vatId ?? this.vatId,
|
vatId: vatId ?? this.vatId,
|
||||||
fiscalCode: fiscalCode ?? this.fiscalCode,
|
fiscalCode: fiscalCode ?? this.fiscalCode,
|
||||||
sdi: sdi ?? this.sdi,
|
sdi: sdi ?? this.sdi,
|
||||||
companyLogo: companyLogo ?? this.companyLogo,
|
logoUrl: logoUrl ?? this.logoUrl,
|
||||||
|
phone: phone ?? this.phone,
|
||||||
|
email: email ?? this.email,
|
||||||
isPaid: isPaid ?? this.isPaid,
|
isPaid: isPaid ?? this.isPaid,
|
||||||
paymentExpiration: paymentExpiration ?? this.paymentExpiration,
|
paymentExpiration: paymentExpiration ?? this.paymentExpiration,
|
||||||
subscriptionTier: subscriptionTier ?? this.subscriptionTier,
|
subscriptionTier: subscriptionTier ?? this.subscriptionTier,
|
||||||
@@ -163,7 +171,9 @@ class CompanyModel extends Equatable {
|
|||||||
vatId: map['vat_id'] ?? '',
|
vatId: map['vat_id'] ?? '',
|
||||||
fiscalCode: map['fiscal_code'] ?? '',
|
fiscalCode: map['fiscal_code'] ?? '',
|
||||||
sdi: map['sdi'] ?? '',
|
sdi: map['sdi'] ?? '',
|
||||||
companyLogo: map['company_logo'] ?? '',
|
logoUrl: map['company_logo'],
|
||||||
|
phone: map['phone'] ?? '',
|
||||||
|
email: map['email'] ?? '',
|
||||||
isPaid: map['is_paid'] ?? false,
|
isPaid: map['is_paid'] ?? false,
|
||||||
paymentExpiration: map['payment_expiration'] != null
|
paymentExpiration: map['payment_expiration'] != null
|
||||||
? DateTime.tryParse(map['payment_expiration'])
|
? DateTime.tryParse(map['payment_expiration'])
|
||||||
@@ -193,7 +203,9 @@ class CompanyModel extends Equatable {
|
|||||||
'vat_id': vatId,
|
'vat_id': vatId,
|
||||||
'fiscal_code': fiscalCode,
|
'fiscal_code': fiscalCode,
|
||||||
'sdi': sdi,
|
'sdi': sdi,
|
||||||
'company_logo': companyLogo,
|
'company_logo': logoUrl,
|
||||||
|
'phone': phone,
|
||||||
|
'email': 'email',
|
||||||
'is_paid': isPaid,
|
'is_paid': isPaid,
|
||||||
if (paymentExpiration != null)
|
if (paymentExpiration != null)
|
||||||
'payment_expiration': paymentExpiration!.toIso8601String(),
|
'payment_expiration': paymentExpiration!.toIso8601String(),
|
||||||
@@ -221,7 +233,9 @@ class CompanyModel extends Equatable {
|
|||||||
vatId,
|
vatId,
|
||||||
fiscalCode,
|
fiscalCode,
|
||||||
sdi,
|
sdi,
|
||||||
companyLogo,
|
logoUrl,
|
||||||
|
phone,
|
||||||
|
email,
|
||||||
isPaid,
|
isPaid,
|
||||||
paymentExpiration,
|
paymentExpiration,
|
||||||
subscriptionTier,
|
subscriptionTier,
|
||||||
|
|||||||
310
lib/features/company/ui/company_settings_screen.dart
Normal file
310
lib/features/company/ui/company_settings_screen.dart
Normal file
@@ -0,0 +1,310 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/features/company/bloc/company_settings_cubit.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
|
class CompanySettingsScreen extends StatefulWidget {
|
||||||
|
const CompanySettingsScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CompanySettingsScreen> createState() => _CompanySettingsScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CompanySettingsScreenState extends State<CompanySettingsScreen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
final _nameCtrl = TextEditingController();
|
||||||
|
final _vatCtrl = TextEditingController();
|
||||||
|
final _addressCtrl = TextEditingController();
|
||||||
|
final _cityCtrl = TextEditingController();
|
||||||
|
final _zipCtrl = TextEditingController();
|
||||||
|
final _phoneCtrl = TextEditingController();
|
||||||
|
final _emailCtrl = TextEditingController();
|
||||||
|
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
context.read<CompanySettingsCubit>().initSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_nameCtrl.dispose();
|
||||||
|
_vatCtrl.dispose();
|
||||||
|
_addressCtrl.dispose();
|
||||||
|
_cityCtrl.dispose();
|
||||||
|
_zipCtrl.dispose();
|
||||||
|
_phoneCtrl.dispose();
|
||||||
|
_emailCtrl.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncControllers(company) {
|
||||||
|
if (_nameCtrl.text.isEmpty) _nameCtrl.text = company.name ?? '';
|
||||||
|
if (_vatCtrl.text.isEmpty) _vatCtrl.text = company.vatNumber ?? '';
|
||||||
|
if (_addressCtrl.text.isEmpty) _addressCtrl.text = company.address ?? '';
|
||||||
|
if (_cityCtrl.text.isEmpty) _cityCtrl.text = company.city ?? '';
|
||||||
|
if (_zipCtrl.text.isEmpty) _zipCtrl.text = company.zipCode ?? '';
|
||||||
|
if (_phoneCtrl.text.isEmpty) _phoneCtrl.text = company.phone ?? '';
|
||||||
|
if (_emailCtrl.text.isEmpty) _emailCtrl.text = company.email ?? '';
|
||||||
|
_isInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _flushToCubit() {
|
||||||
|
context.read<CompanySettingsCubit>().updateFields(
|
||||||
|
name: _nameCtrl.text,
|
||||||
|
vatId: _vatCtrl.text,
|
||||||
|
address: _addressCtrl.text,
|
||||||
|
city: _cityCtrl.text,
|
||||||
|
zipCode: _zipCtrl.text,
|
||||||
|
phone: _phoneCtrl.text,
|
||||||
|
email: _emailCtrl.text,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pickAndUploadLogo() async {
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final companySettingsCubit = context.read<CompanySettingsCubit>();
|
||||||
|
|
||||||
|
final pickedFile = await picker.pickImage(source: ImageSource.gallery);
|
||||||
|
|
||||||
|
if (pickedFile != null && mounted) {
|
||||||
|
// Passiamo i bytes per compatibilità totale con Flutter Web
|
||||||
|
final bytes = await pickedFile.readAsBytes();
|
||||||
|
companySettingsCubit.uploadLogo(bytes, pickedFile.name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Impostazioni Azienda')),
|
||||||
|
body: BlocConsumer<CompanySettingsCubit, CompanySettingsState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.status == CompanySettingsStatus.ready && !_isInitialized) {
|
||||||
|
_syncControllers(state.company!);
|
||||||
|
}
|
||||||
|
if (state.status == CompanySettingsStatus.success) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Impostazioni salvate con successo!'),
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (state.status == CompanySettingsStatus.failure) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text(state.errorMessage ?? 'Errore'),
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state.company == null) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
final company = state.company!;
|
||||||
|
|
||||||
|
return Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxWidth: 800),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
children: [
|
||||||
|
// --- SEZIONE LOGO ---
|
||||||
|
Center(
|
||||||
|
child: Stack(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
height: 120,
|
||||||
|
width: 120,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.surfaceContainerHighest,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: theme.dividerColor),
|
||||||
|
image: company.logoUrl != null
|
||||||
|
? DecorationImage(
|
||||||
|
image: NetworkImage(company.logoUrl!),
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: company.logoUrl == null
|
||||||
|
? const Icon(
|
||||||
|
Icons.business,
|
||||||
|
size: 50,
|
||||||
|
color: Colors.grey,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
if (state.status ==
|
||||||
|
CompanySettingsStatus.uploadingLogo)
|
||||||
|
const Positioned.fill(
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
|
FloatingActionButton.small(
|
||||||
|
onPressed:
|
||||||
|
state.status ==
|
||||||
|
CompanySettingsStatus.uploadingLogo
|
||||||
|
? null
|
||||||
|
: _pickAndUploadLogo,
|
||||||
|
child: const Icon(Icons.camera_alt),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
|
||||||
|
// --- SEZIONE DATI PRINCIPALI ---
|
||||||
|
Text(
|
||||||
|
'Dati Legali',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _nameCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Ragione Sociale',
|
||||||
|
prefixIcon: Icon(Icons.badge),
|
||||||
|
),
|
||||||
|
validator: (val) => val == null || val.isEmpty
|
||||||
|
? 'Campo obbligatorio'
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _vatCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Partita IVA / C.F.',
|
||||||
|
prefixIcon: Icon(Icons.receipt_long),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// --- SEZIONE INDIRIZZO E CONTATTI ---
|
||||||
|
Text(
|
||||||
|
'Sede e Contatti',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
controller: _addressCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Indirizzo (Via e numero civico)',
|
||||||
|
prefixIcon: Icon(Icons.location_on),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 2,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _cityCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Città',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _zipCtrl,
|
||||||
|
decoration: const InputDecoration(labelText: 'CAP'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _phoneCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Telefono',
|
||||||
|
prefixIcon: Icon(Icons.phone),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: TextFormField(
|
||||||
|
controller: _emailCtrl,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Email',
|
||||||
|
prefixIcon: Icon(Icons.email),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 48),
|
||||||
|
|
||||||
|
// --- PULSANTE SALVATAGGIO ---
|
||||||
|
SizedBox(
|
||||||
|
height: 50,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: state.status == CompanySettingsStatus.saving
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
_flushToCubit();
|
||||||
|
context
|
||||||
|
.read<CompanySettingsCubit>()
|
||||||
|
.saveSettings();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: state.status == CompanySettingsStatus.saving
|
||||||
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 2,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.save),
|
||||||
|
label: const Text(
|
||||||
|
'Salva Impostazioni',
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,328 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
||||||
import 'package:flux/core/utils/extensions.dart';
|
|
||||||
import 'package:flux/features/company/bloc/company_bloc.dart';
|
|
||||||
import 'package:flux/core/blocs/session/session_cubit.dart';
|
|
||||||
import 'package:flux/core/theme/theme.dart';
|
|
||||||
import 'package:flux/core/widgets/flux_text_field.dart';
|
|
||||||
import 'package:flux/features/company/models/company_model.dart';
|
|
||||||
|
|
||||||
class CreateCompanyScreen extends StatefulWidget {
|
|
||||||
const CreateCompanyScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CreateCompanyScreen> createState() => _CreateCompanyScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
// lib/ui/setup/create_company_screen.dart
|
|
||||||
|
|
||||||
class _CreateCompanyScreenState extends State<CreateCompanyScreen> {
|
|
||||||
final _formKey = GlobalKey<FormState>();
|
|
||||||
|
|
||||||
// Controller per i campi obbligatori
|
|
||||||
final _ragioneSocialeController = TextEditingController();
|
|
||||||
final _indirizzoController = TextEditingController();
|
|
||||||
final _capController = TextEditingController();
|
|
||||||
final _cittaController = TextEditingController();
|
|
||||||
final _provinciaController = TextEditingController();
|
|
||||||
final _pIvaController = TextEditingController();
|
|
||||||
final _cfController = TextEditingController();
|
|
||||||
final _univocoController = TextEditingController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
// Ricordati sempre di chiuderli!
|
|
||||||
_ragioneSocialeController.dispose();
|
|
||||||
_indirizzoController.dispose();
|
|
||||||
_capController.dispose();
|
|
||||||
_cittaController.dispose();
|
|
||||||
_provinciaController.dispose();
|
|
||||||
_pIvaController.dispose();
|
|
||||||
_cfController.dispose();
|
|
||||||
_univocoController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onSave() {
|
|
||||||
if (_formKey.currentState!.validate()) {
|
|
||||||
// Recuperiamo l'ID utente attuale da Supabase o dal SessionBloc
|
|
||||||
final userId = context.read<SessionCubit>().state.user!.id;
|
|
||||||
|
|
||||||
final company = CompanyModel(
|
|
||||||
userId: userId,
|
|
||||||
name: _ragioneSocialeController.text.trim(),
|
|
||||||
address: _indirizzoController.text.trim(),
|
|
||||||
zipCode: _capController.text.trim(),
|
|
||||||
city: _cittaController.text.trim(),
|
|
||||||
province: _provinciaController.text.trim(),
|
|
||||||
vatId: _pIvaController.text.trim(),
|
|
||||||
fiscalCode: _cfController.text.trim(),
|
|
||||||
sdi: _univocoController.text.trim().toUpperCase(),
|
|
||||||
// Gli altri campi hanno i default nel modello
|
|
||||||
);
|
|
||||||
|
|
||||||
// Spariamo l'evento al Bloc
|
|
||||||
context.read<CompanyBloc>().add(CreateCompanyRequested(company: company));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text(context.l10n.createCompanyScreenCompanyConfiguration),
|
|
||||||
actions: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.logout_rounded),
|
|
||||||
onPressed: () {
|
|
||||||
// Qui chiami il tuo Bloc dell'autenticazione per fare logout
|
|
||||||
// Esempio se hai un AuthBloc o SessionBloc:
|
|
||||||
//context.read<AuthBloc>().add(LogoutRequested());
|
|
||||||
|
|
||||||
// Se vuoi solo tornare brutalmente alla login per testare il logo:
|
|
||||||
// Navigator.of(context).pushReplacementNamed('/login');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
body: BlocConsumer<CompanyBloc, CompanyState>(
|
|
||||||
listener: (context, state) {
|
|
||||||
if (state.status == CompanyStatus.success && state.company != null) {
|
|
||||||
// 1. Aggiorniamo la singleton con i dati reali (ID incluso)
|
|
||||||
//GetIt.I.get<AppSettings>().setCurrentCompany(state.company);
|
|
||||||
|
|
||||||
// 2. Notifichiamo il SessionBloc per cambiare pagina
|
|
||||||
//context.read<SessionCubit>().add(AppStarted());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.status == CompanyStatus.failure) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
state.errorMessage ?? context.l10n.commonSavingError,
|
|
||||||
),
|
|
||||||
backgroundColor: Colors.redAccent,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
builder: (context, state) {
|
|
||||||
return SafeArea(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(24.0),
|
|
||||||
child: Form(
|
|
||||||
key: _formKey,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildHeader(context),
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
|
|
||||||
// --- SEZIONE 1: IDENTITÀ FISCALE ---
|
|
||||||
_SectionTitle(
|
|
||||||
title: context.l10n.createCompanyScreenFiscalData,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
FluxTextField(
|
|
||||||
label: context.l10n.createCompanyScreenCompanyName,
|
|
||||||
icon: Icons.business,
|
|
||||||
controller: _ragioneSocialeController,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: FluxTextField(
|
|
||||||
label: context.l10n.createCompanyScreenVatId,
|
|
||||||
icon: Icons.numbers,
|
|
||||||
controller: _pIvaController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: FluxTextField(
|
|
||||||
label: context.l10n.createCompanyScreenFiscalCode,
|
|
||||||
icon: Icons.badge_outlined,
|
|
||||||
controller: _cfController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
FluxTextField(
|
|
||||||
label: context.l10n.createCompanyScreenSdiPec,
|
|
||||||
icon: Icons.send_and_archive_outlined,
|
|
||||||
controller: _univocoController,
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
|
|
||||||
// --- SEZIONE 2: SEDE LEGALE ---
|
|
||||||
_SectionTitle(
|
|
||||||
title:
|
|
||||||
context.l10n.createCompanyScreenCompanyLegalAddress,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
FluxTextField(
|
|
||||||
label: context.l10n.commonAddress,
|
|
||||||
icon: Icons.home_work_outlined,
|
|
||||||
controller: _indirizzoController,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
flex: 2,
|
|
||||||
child: FluxTextField(
|
|
||||||
label: context.l10n.commonCity,
|
|
||||||
icon: Icons.location_city,
|
|
||||||
controller: _cittaController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: FluxTextField(
|
|
||||||
label: context.l10n.commonZipCode,
|
|
||||||
icon: Icons.map_outlined,
|
|
||||||
controller: _capController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: FluxTextField(
|
|
||||||
label: context.l10n.commonProvince,
|
|
||||||
icon: Icons.explore_outlined,
|
|
||||||
controller: _provinciaController,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
|
|
||||||
// --- SEZIONE 3: LOGO AZIENDALE ---
|
|
||||||
_SectionTitle(title: 'BRANDING'),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_buildLogoPicker(context),
|
|
||||||
|
|
||||||
const SizedBox(height: 48),
|
|
||||||
|
|
||||||
// --- BOTTONE INVIO ---
|
|
||||||
_buildSubmitButton(context, state),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Placeholder per il futuro caricamento logo
|
|
||||||
Widget _buildLogoPicker(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.accent.withValues(alpha: 0.05),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
// Bordo continuo ma sottile e semitrasparente per un look pulito
|
|
||||||
border: Border.all(
|
|
||||||
color: context.accent.withValues(alpha: 0.3),
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.cloud_upload_outlined, color: context.accent, size: 32),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
context.l10n.createCompanyScreenUploadLogo,
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.primaryText,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
context.l10n.createCompanyScreenWillBeUsedForReceipts,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: TextStyle(color: context.secondaryText, fontSize: 12),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSubmitButton(BuildContext context, CompanyState state) {
|
|
||||||
return SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 56,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: state.status == CompanyStatus.loading
|
|
||||||
? null
|
|
||||||
: () => _onSave(),
|
|
||||||
child: state.status == CompanyStatus.loading
|
|
||||||
? const CircularProgressIndicator()
|
|
||||||
: Text(context.l10n.createCompanyScreenSaveCompany),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildHeader(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: context.accent.withValues(alpha: 0.1),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
Icons.domain_add_rounded,
|
|
||||||
color: context.accent,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
Text(
|
|
||||||
context.l10n.createCompanyScreenSetupYourCompany,
|
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: context.primaryText,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
context.l10n.createCompanyScreenFluxNeedsYourFiscalData,
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.secondaryText,
|
|
||||||
fontSize: 15,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Widget di supporto per i titoli delle sezioni
|
|
||||||
class _SectionTitle extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
const _SectionTitle({required this.title});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(
|
|
||||||
color: context.accent,
|
|
||||||
fontWeight: FontWeight.w800,
|
|
||||||
letterSpacing: 1.2,
|
|
||||||
fontSize: 13,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -186,7 +186,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('/operation-form');
|
context.push('/operations/form/new');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|||||||
@@ -0,0 +1,270 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:flux/core/blocs/session/session_cubit.dart';
|
||||||
|
import 'package:flux/features/operations/data/operations_repository.dart';
|
||||||
|
import 'package:flux/features/operations/models/operation_model.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
|
part 'operation_form_state.dart';
|
||||||
|
|
||||||
|
class OperationFormCubit extends Cubit<OperationFormState> {
|
||||||
|
final OperationsRepository _repository = GetIt.I<OperationsRepository>();
|
||||||
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||||
|
final Uuid _uuid = const Uuid();
|
||||||
|
|
||||||
|
OperationFormCubit()
|
||||||
|
: super(
|
||||||
|
OperationFormState(
|
||||||
|
// Inizializziamo con un modello vuoto di sicurezza
|
||||||
|
operation: OperationModel(
|
||||||
|
storeId: '',
|
||||||
|
companyId: '',
|
||||||
|
reference: '',
|
||||||
|
status: OperationStatus.draft,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> initForm({
|
||||||
|
OperationModel? existingOperation,
|
||||||
|
String? operationId,
|
||||||
|
}) async {
|
||||||
|
emit(state.copyWith(status: OperationFormStatus.loading));
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (existingOperation != null) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
operation: existingOperation,
|
||||||
|
status: OperationFormStatus.ready,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (operationId != null) {
|
||||||
|
// Avendo separato i cubit, se ci passano solo l'ID lo scarichiamo dal DB
|
||||||
|
final operation = await _repository.fetchOperationById(operationId);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
operation: operation,
|
||||||
|
status: OperationFormStatus.ready,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// NUOVA PRATICA: Creiamo un nuovo Batch UUID
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
operation: OperationModel(
|
||||||
|
storeId: _sessionCubit.state.currentStore?.id ?? '',
|
||||||
|
reference: '',
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
companyId: _sessionCubit.state.company!.id!,
|
||||||
|
status: OperationStatus.draft,
|
||||||
|
batchUuid: _uuid.v4(),
|
||||||
|
),
|
||||||
|
status: OperationFormStatus.ready,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: OperationFormStatus.failure,
|
||||||
|
errorMessage: "Errore inizializzazione form: $e",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- LOGICA BATCH ---
|
||||||
|
|
||||||
|
void _prepareNextOperationInBatch() {
|
||||||
|
final current = state.operation;
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: OperationFormStatus.ready, // Torna ready per il nuovo form
|
||||||
|
operation: OperationModel(
|
||||||
|
companyId: current.companyId,
|
||||||
|
storeId: current.storeId,
|
||||||
|
storeDisplayName: current.storeDisplayName,
|
||||||
|
batchUuid: current.batchUuid, // MANTIENE IL COLLEGAMENTO
|
||||||
|
customerId: current.customerId, // MANTIENE IL CLIENTE
|
||||||
|
customerDisplayName: current.customerDisplayName,
|
||||||
|
status: OperationStatus.draft,
|
||||||
|
createdAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SALVATAGGIO ---
|
||||||
|
|
||||||
|
Future<void> saveOperation({
|
||||||
|
required OperationStatus targetStatus,
|
||||||
|
required bool keepAdding,
|
||||||
|
}) async {
|
||||||
|
emit(
|
||||||
|
state.copyWith(status: OperationFormStatus.saving, errorMessage: null),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final operationToSave = state.operation.copyWith(status: targetStatus);
|
||||||
|
final savedOperation = await _repository.saveFullOperation(
|
||||||
|
operation: operationToSave,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (keepAdding) {
|
||||||
|
// Salviamo nella "memoria" del batch le pratiche create finora
|
||||||
|
final updatedBatchList = List<OperationModel>.from(
|
||||||
|
state.savedBatchOperations,
|
||||||
|
)..add(savedOperation);
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: OperationFormStatus.successAndAddAnother,
|
||||||
|
savedBatchOperations: updatedBatchList,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Pulisce i campi per la prossima operazione
|
||||||
|
_prepareNextOperationInBatch();
|
||||||
|
} else {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: OperationFormStatus.success,
|
||||||
|
operation: savedOperation, // Aggiorniamo con l'ID restituito dal DB
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: OperationFormStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> saveOperationDraft() async {
|
||||||
|
try {
|
||||||
|
final operationToSave = state.operation;
|
||||||
|
if (operationToSave.customerId == null ||
|
||||||
|
operationToSave.customerId!.isEmpty) {
|
||||||
|
throw Exception('Seleziona un cliente prima di poter usare il QR');
|
||||||
|
}
|
||||||
|
final savedOperation = await _repository.saveFullOperation(
|
||||||
|
operation: operationToSave,
|
||||||
|
);
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
operation: savedOperation,
|
||||||
|
status: OperationFormStatus.ready,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return savedOperation.id;
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: OperationFormStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- GESTIONE DEI CAMPI IN TEMPO REALE ---
|
||||||
|
|
||||||
|
void updateFields({
|
||||||
|
String? customerId,
|
||||||
|
String? customerDisplayName,
|
||||||
|
String? reference,
|
||||||
|
String? note,
|
||||||
|
String? type,
|
||||||
|
String? providerId,
|
||||||
|
String? providerDisplayName,
|
||||||
|
String? subtype,
|
||||||
|
String? description,
|
||||||
|
DateTime? expirationDate,
|
||||||
|
int? quantity,
|
||||||
|
String? modelId,
|
||||||
|
String? modelDisplayName,
|
||||||
|
String? staffId,
|
||||||
|
String? staffDisplayName,
|
||||||
|
OperationStatus? status,
|
||||||
|
|
||||||
|
bool clearProvider = false,
|
||||||
|
bool clearType = false,
|
||||||
|
bool clearSubtype = false,
|
||||||
|
bool clearDescription = false,
|
||||||
|
bool clearExpiration = false,
|
||||||
|
bool clearQuantity = false,
|
||||||
|
bool clearModel = false,
|
||||||
|
}) {
|
||||||
|
final current = state.operation;
|
||||||
|
|
||||||
|
int? newQuantity;
|
||||||
|
if (clearQuantity) newQuantity = 1;
|
||||||
|
if (quantity != null && quantity <= 0) newQuantity = 0;
|
||||||
|
if (quantity != null && quantity > 0) newQuantity = quantity;
|
||||||
|
|
||||||
|
final updated = current.copyWith(
|
||||||
|
customerId:
|
||||||
|
customerId ??
|
||||||
|
current.customerId, // Se non passo customerId, tengo il vecchio
|
||||||
|
customerDisplayName: customerDisplayName ?? current.customerDisplayName,
|
||||||
|
reference: reference ?? current.reference,
|
||||||
|
note: note ?? current.note,
|
||||||
|
providerId: clearProvider ? null : (providerId ?? current.providerId),
|
||||||
|
providerDisplayName: clearProvider
|
||||||
|
? null
|
||||||
|
: (providerDisplayName ?? current.providerDisplayName),
|
||||||
|
quantity: newQuantity ?? current.quantity,
|
||||||
|
type: clearType ? null : (type ?? current.type),
|
||||||
|
description: clearDescription
|
||||||
|
? null
|
||||||
|
: (description ?? current.description),
|
||||||
|
subtype: clearSubtype ? null : (subtype ?? current.subtype),
|
||||||
|
expirationDate: clearExpiration
|
||||||
|
? null
|
||||||
|
: (expirationDate ?? current.expirationDate),
|
||||||
|
modelId: clearModel ? null : (modelId ?? current.modelId),
|
||||||
|
modelDisplayName: clearModel
|
||||||
|
? null
|
||||||
|
: (modelDisplayName ?? current.modelDisplayName),
|
||||||
|
staffId: staffId ?? current.staffId,
|
||||||
|
staffDisplayName: staffDisplayName ?? current.staffDisplayName,
|
||||||
|
status: status ?? current.status,
|
||||||
|
);
|
||||||
|
|
||||||
|
emit(state.copyWith(operation: updated));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- UTILS ---
|
||||||
|
|
||||||
|
void setTypeWithSmartDefault(String type) {
|
||||||
|
DateTime? defaultDate;
|
||||||
|
final now = DateTime.now();
|
||||||
|
|
||||||
|
if (type == 'Energy') {
|
||||||
|
defaultDate = DateTime(now.year, now.month + 24, now.day);
|
||||||
|
}
|
||||||
|
if (type == 'Fin') {
|
||||||
|
defaultDate = DateTime(now.year, now.month + 30, now.day);
|
||||||
|
}
|
||||||
|
if (type == 'Entertainment') {
|
||||||
|
defaultDate = DateTime(now.year, now.month + 12, now.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFields(
|
||||||
|
type: type,
|
||||||
|
expirationDate: defaultDate,
|
||||||
|
clearProvider: true,
|
||||||
|
clearSubtype: true,
|
||||||
|
clearModel: true,
|
||||||
|
clearQuantity: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,39 +1,48 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
part of 'operation_form_cubit.dart';
|
||||||
import 'package:flux/features/operations/models/operation_model.dart';
|
|
||||||
|
|
||||||
enum OperationFormStatus {
|
enum OperationFormStatus {
|
||||||
initial,
|
initial,
|
||||||
ready,
|
|
||||||
loading,
|
loading,
|
||||||
|
ready,
|
||||||
saving,
|
saving,
|
||||||
success,
|
success,
|
||||||
successAndAddAnother,
|
successAndAddAnother, // Nuovo stato in stile Ticket!
|
||||||
failure,
|
failure,
|
||||||
}
|
}
|
||||||
|
|
||||||
class OperationFormState extends Equatable {
|
class OperationFormState extends Equatable {
|
||||||
final OperationModel operation;
|
|
||||||
final OperationFormStatus status;
|
final OperationFormStatus status;
|
||||||
|
final OperationModel operation;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
// Teniamo traccia delle operazioni salvate in questa sessione (per UI riepilogo)
|
||||||
|
final List<OperationModel> savedBatchOperations;
|
||||||
|
|
||||||
const OperationFormState({
|
const OperationFormState({
|
||||||
required this.operation,
|
|
||||||
this.status = OperationFormStatus.initial,
|
this.status = OperationFormStatus.initial,
|
||||||
|
required this.operation,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.savedBatchOperations = const [],
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [operation, status, errorMessage];
|
|
||||||
|
|
||||||
OperationFormState copyWith({
|
OperationFormState copyWith({
|
||||||
OperationModel? operation,
|
|
||||||
OperationFormStatus? status,
|
OperationFormStatus? status,
|
||||||
|
OperationModel? operation,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
|
List<OperationModel>? savedBatchOperations,
|
||||||
}) {
|
}) {
|
||||||
return OperationFormState(
|
return OperationFormState(
|
||||||
operation: operation ?? this.operation,
|
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
|
operation: operation ?? this.operation,
|
||||||
errorMessage: errorMessage,
|
errorMessage: errorMessage,
|
||||||
|
savedBatchOperations: savedBatchOperations ?? this.savedBatchOperations,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
status,
|
||||||
|
operation,
|
||||||
|
errorMessage,
|
||||||
|
savedBatchOperations,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
81
lib/features/operations/blocs/operation_list_cubit.dart
Normal file
81
lib/features/operations/blocs/operation_list_cubit.dart
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import 'package:equatable/equatable.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/features/operations/data/operations_repository.dart';
|
||||||
|
import 'package:flux/features/operations/models/operation_model.dart';
|
||||||
|
import 'package:get_it/get_it.dart';
|
||||||
|
|
||||||
|
part 'operation_list_state.dart';
|
||||||
|
|
||||||
|
class OperationListCubit extends Cubit<OperationListState> {
|
||||||
|
final OperationsRepository _repository = GetIt.I<OperationsRepository>();
|
||||||
|
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
||||||
|
|
||||||
|
OperationListCubit() : super(const OperationListState());
|
||||||
|
|
||||||
|
Future<void> loadOperations({bool refresh = false}) async {
|
||||||
|
if (state.status == OperationListStatus.loading) return;
|
||||||
|
if (!refresh && state.hasReachedMax) return;
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: OperationListStatus.loading,
|
||||||
|
errorMessage: null,
|
||||||
|
operations: refresh ? [] : state.operations,
|
||||||
|
hasReachedMax: refresh ? false : state.hasReachedMax,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final currentOffset = refresh ? 0 : state.operations.length;
|
||||||
|
final companyId = _sessionCubit.state.company?.id;
|
||||||
|
|
||||||
|
if (companyId == null) {
|
||||||
|
throw Exception("Company ID non trovato nella sessione");
|
||||||
|
}
|
||||||
|
|
||||||
|
final newOperations = await _repository.fetchOperations(
|
||||||
|
companyId: companyId,
|
||||||
|
offset: currentOffset,
|
||||||
|
limit: 50,
|
||||||
|
searchTerm: state.query,
|
||||||
|
dateRange: state.dateRange,
|
||||||
|
);
|
||||||
|
|
||||||
|
final bool reachedMax = newOperations.length < 50;
|
||||||
|
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: OperationListStatus.success,
|
||||||
|
operations: refresh
|
||||||
|
? newOperations
|
||||||
|
: [...state.operations, ...newOperations],
|
||||||
|
hasReachedMax: reachedMax,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
status: OperationListStatus.failure,
|
||||||
|
errorMessage: "Errore nel caricamento operazioni: $e",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateFilters({String? query, DateTimeRange? range}) {
|
||||||
|
emit(
|
||||||
|
state.copyWith(
|
||||||
|
query: query ?? state.query,
|
||||||
|
dateRange: range ?? state.dateRange,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
loadOperations(refresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearFilters() {
|
||||||
|
emit(const OperationListState()); // Resetta tutto allo stato iniziale
|
||||||
|
loadOperations(refresh: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
49
lib/features/operations/blocs/operation_list_state.dart
Normal file
49
lib/features/operations/blocs/operation_list_state.dart
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
part of 'operation_list_cubit.dart';
|
||||||
|
|
||||||
|
enum OperationListStatus { initial, loading, success, failure }
|
||||||
|
|
||||||
|
class OperationListState extends Equatable {
|
||||||
|
final OperationListStatus status;
|
||||||
|
final List<OperationModel> operations;
|
||||||
|
final bool hasReachedMax;
|
||||||
|
final String? errorMessage;
|
||||||
|
final String query;
|
||||||
|
final DateTimeRange? dateRange;
|
||||||
|
|
||||||
|
const OperationListState({
|
||||||
|
this.status = OperationListStatus.initial,
|
||||||
|
this.operations = const [],
|
||||||
|
this.hasReachedMax = false,
|
||||||
|
this.errorMessage,
|
||||||
|
this.query = '',
|
||||||
|
this.dateRange,
|
||||||
|
});
|
||||||
|
|
||||||
|
OperationListState copyWith({
|
||||||
|
OperationListStatus? status,
|
||||||
|
List<OperationModel>? operations,
|
||||||
|
bool? hasReachedMax,
|
||||||
|
String? errorMessage,
|
||||||
|
String? query,
|
||||||
|
DateTimeRange? dateRange,
|
||||||
|
}) {
|
||||||
|
return OperationListState(
|
||||||
|
status: status ?? this.status,
|
||||||
|
operations: operations ?? this.operations,
|
||||||
|
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
query: query ?? this.query,
|
||||||
|
dateRange: dateRange ?? this.dateRange,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [
|
||||||
|
status,
|
||||||
|
operations,
|
||||||
|
hasReachedMax,
|
||||||
|
errorMessage,
|
||||||
|
query,
|
||||||
|
dateRange,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
import 'package:equatable/equatable.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/features/operations/data/operations_repository.dart';
|
|
||||||
import 'package:flux/features/operations/models/operation_model.dart';
|
|
||||||
import 'package:get_it/get_it.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:uuid/uuid.dart';
|
|
||||||
part 'operations_state.dart';
|
|
||||||
|
|
||||||
class OperationsCubit extends Cubit<OperationsState> {
|
|
||||||
final OperationsRepository _repository = GetIt.I<OperationsRepository>();
|
|
||||||
final SessionCubit _sessionCubit = GetIt.I<SessionCubit>();
|
|
||||||
final Uuid _uuid = const Uuid(); // Generatore di UUID per il batch
|
|
||||||
|
|
||||||
OperationsCubit()
|
|
||||||
: super(const OperationsState(status: OperationsStatus.initial));
|
|
||||||
|
|
||||||
// --- CARICAMENTO E PAGINAZIONE ---
|
|
||||||
|
|
||||||
Future<void> loadOperations({bool refresh = false}) async {
|
|
||||||
if (state.status == OperationsStatus.loading) return;
|
|
||||||
if (!refresh && state.hasReachedMax) return;
|
|
||||||
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: OperationsStatus.loading,
|
|
||||||
errorMessage: null,
|
|
||||||
allOperations: refresh ? [] : state.allOperations,
|
|
||||||
hasReachedMax: refresh ? false : state.hasReachedMax,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
final currentOffset = refresh ? 0 : state.allOperations.length;
|
|
||||||
final companyId = _sessionCubit.state.company?.id;
|
|
||||||
|
|
||||||
if (companyId == null) {
|
|
||||||
throw Exception("Company ID non trovato nella sessione");
|
|
||||||
}
|
|
||||||
|
|
||||||
final newOperations = await _repository.fetchOperations(
|
|
||||||
companyId: companyId,
|
|
||||||
offset: currentOffset,
|
|
||||||
limit: 50,
|
|
||||||
searchTerm: state.query,
|
|
||||||
dateRange: state.dateRange,
|
|
||||||
);
|
|
||||||
|
|
||||||
final bool reachedMax = newOperations.length < 50;
|
|
||||||
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: OperationsStatus.ready,
|
|
||||||
allOperations: refresh
|
|
||||||
? newOperations
|
|
||||||
: [...state.allOperations, ...newOperations],
|
|
||||||
hasReachedMax: reachedMax,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: OperationsStatus.failure,
|
|
||||||
errorMessage: "Errore nel caricamento operazioni: $e",
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- GESTIONE FILTRI ---
|
|
||||||
|
|
||||||
void updateFilters({String? query, DateTimeRange? range}) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
query: query ?? state.query,
|
|
||||||
dateRange: range ?? state.dateRange,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
loadOperations(refresh: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearFilters() {
|
|
||||||
emit(state.copyWith(query: '', dateRange: null));
|
|
||||||
loadOperations(refresh: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
void initOperationForm({
|
|
||||||
OperationModel? existingOperation,
|
|
||||||
String? operationId,
|
|
||||||
String? staffId,
|
|
||||||
String? staffDisplayName,
|
|
||||||
}) async {
|
|
||||||
if (existingOperation != null) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
currentOperation: existingOperation,
|
|
||||||
status: OperationsStatus.ready,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else if (operationId != null) {
|
|
||||||
OperationModel? operationModel = state.allOperations.firstWhereOrNull(
|
|
||||||
(s) => s.id == operationId,
|
|
||||||
);
|
|
||||||
operationModel ??= await _repository.fetchOperationById(operationId);
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
currentOperation: operationModel,
|
|
||||||
status: OperationsStatus.ready,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// NUOVA PRATICA: Creiamo un nuovo Batch UUID
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
currentOperation: OperationModel(
|
|
||||||
storeId: _sessionCubit.state.currentStore?.id ?? '',
|
|
||||||
reference: '',
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
companyId: _sessionCubit.state.company!.id!,
|
|
||||||
status: OperationStatus.draft,
|
|
||||||
batchUuid: _uuid.v4(), // <-- GENERIAMO IL BATCH UNIVOCO
|
|
||||||
),
|
|
||||||
status: OperationsStatus.ready,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// MAGIA PURA: Prepara il form per inserire un altro servizio nella stessa pratica.
|
|
||||||
/// Mantiene il Cliente, il Batch e lo Store, ma svuota il resto.
|
|
||||||
void prepareNextOperationInBatch() {
|
|
||||||
if (state.currentOperation == null) return;
|
|
||||||
|
|
||||||
final current = state.currentOperation!;
|
|
||||||
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: OperationsStatus.ready,
|
|
||||||
currentOperation: OperationModel(
|
|
||||||
companyId: current.companyId,
|
|
||||||
storeId: current.storeId,
|
|
||||||
storeDisplayName: current.storeDisplayName,
|
|
||||||
batchUuid: current.batchUuid, // <-- MANTIENE IL COLLEGAMENTO
|
|
||||||
customerId: current.customerId, // <-- MANTIENE IL CLIENTE
|
|
||||||
customerDisplayName: current.customerDisplayName,
|
|
||||||
status: OperationStatus.draft,
|
|
||||||
createdAt: DateTime.now(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- PERSISTENZA ---
|
|
||||||
|
|
||||||
Future<void> saveCurrentOperation({
|
|
||||||
required OperationStatus targetStatus,
|
|
||||||
bool shouldPop = true,
|
|
||||||
}) async {
|
|
||||||
if (state.currentOperation == null) return;
|
|
||||||
|
|
||||||
emit(state.copyWith(status: OperationsStatus.saving, errorMessage: null));
|
|
||||||
try {
|
|
||||||
final operationToSave = state.currentOperation!.copyWith(
|
|
||||||
status: targetStatus,
|
|
||||||
);
|
|
||||||
|
|
||||||
final updatedOperation = await _repository.saveFullOperation(
|
|
||||||
operation: operationToSave,
|
|
||||||
);
|
|
||||||
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
// Se non facciamo Pop (es. l'utente vuole aggiungere un altro servizio), non killiamo l'operazione corrente
|
|
||||||
status: shouldPop
|
|
||||||
? OperationsStatus.saved
|
|
||||||
: OperationsStatus.savedNoPop,
|
|
||||||
currentOperation: shouldPop ? null : updatedOperation,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ricarica in background per la dashboard
|
|
||||||
loadOperations(refresh: true);
|
|
||||||
} catch (e) {
|
|
||||||
emit(
|
|
||||||
state.copyWith(
|
|
||||||
status: OperationsStatus.failure,
|
|
||||||
errorMessage: e.toString(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- RECUPERO OPERAZIONI DELLO STESSO BATCH (Per UI di riepilogo) ---
|
|
||||||
|
|
||||||
/// Puoi usare questa funzione se nella UI vuoi mostrare "Hai inserito 3 servizi in questa pratica"
|
|
||||||
List<OperationModel> getOperationsInCurrentBatch() {
|
|
||||||
if (state.currentOperation == null) return [];
|
|
||||||
final currentBatch = state.currentOperation!.batchUuid;
|
|
||||||
|
|
||||||
// Filtriamo dalla lista caricata (o potresti fare una query diretta a Supabase se preferisci)
|
|
||||||
return state.allOperations
|
|
||||||
.where(
|
|
||||||
(op) =>
|
|
||||||
op.batchUuid == currentBatch &&
|
|
||||||
op.id != state.currentOperation!.id,
|
|
||||||
)
|
|
||||||
.toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- GESTIONE DELLO STATO DEL FORM IN TEMPO REALE ---
|
|
||||||
void updateOperationFields({
|
|
||||||
String? customerId,
|
|
||||||
String? customerDisplayName,
|
|
||||||
String? type,
|
|
||||||
String? providerId,
|
|
||||||
String? providerDisplayName,
|
|
||||||
String? subtype,
|
|
||||||
String? description,
|
|
||||||
DateTime? expirationDate,
|
|
||||||
int? quantity,
|
|
||||||
String? modelId,
|
|
||||||
String? modelDisplayName,
|
|
||||||
String? staffId,
|
|
||||||
String? staffDisplayName,
|
|
||||||
// Aggiungiamo questi flag per forzare la pulizia dei campi quando cambi tipo
|
|
||||||
bool clearProvider = false,
|
|
||||||
bool clearType = false,
|
|
||||||
bool clearSubtype = false,
|
|
||||||
bool clearDescription = false,
|
|
||||||
bool clearExpiration = false,
|
|
||||||
bool clearQuantity = false,
|
|
||||||
bool clearModel = false,
|
|
||||||
}) {
|
|
||||||
if (state.currentOperation == null) return;
|
|
||||||
|
|
||||||
final current = state.currentOperation!;
|
|
||||||
|
|
||||||
// Creiamo il modello aggiornato
|
|
||||||
// ATTENZIONE: adatta questa logica in base a come è scritto il tuo copyWith!
|
|
||||||
int? newQuantity;
|
|
||||||
if (clearQuantity) {
|
|
||||||
newQuantity = 1;
|
|
||||||
}
|
|
||||||
if (quantity != null && quantity <= 0) {
|
|
||||||
newQuantity = 0;
|
|
||||||
}
|
|
||||||
if (quantity != null && quantity > 0) {
|
|
||||||
newQuantity = quantity;
|
|
||||||
}
|
|
||||||
final updated = current.copyWith(
|
|
||||||
customerId: customerId,
|
|
||||||
customerDisplayName: customerDisplayName,
|
|
||||||
|
|
||||||
// Se clearProvider è true, forziamo una stringa vuota (o null se il tuo modello lo supporta)
|
|
||||||
providerId: clearProvider ? null : (providerId ?? current.providerId),
|
|
||||||
providerDisplayName: clearProvider
|
|
||||||
? null
|
|
||||||
: (providerDisplayName ?? current.providerDisplayName),
|
|
||||||
quantity: newQuantity,
|
|
||||||
type: clearType ? null : (type ?? current.type),
|
|
||||||
description: clearDescription
|
|
||||||
? null
|
|
||||||
: (description ?? current.description),
|
|
||||||
subtype: clearSubtype ? null : (subtype ?? current.subtype),
|
|
||||||
expirationDate: clearExpiration
|
|
||||||
? null
|
|
||||||
: (expirationDate ?? current.expirationDate),
|
|
||||||
modelId: clearModel ? null : (modelId ?? current.modelId),
|
|
||||||
modelDisplayName: clearModel
|
|
||||||
? null
|
|
||||||
: (modelDisplayName ?? current.modelDisplayName),
|
|
||||||
staffId: staffId ?? current.staffId,
|
|
||||||
staffDisplayName: staffDisplayName ?? current.staffDisplayName,
|
|
||||||
);
|
|
||||||
|
|
||||||
emit(state.copyWith(currentOperation: updated));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metodo di utilità per calcolare la data X mesi da oggi
|
|
||||||
DateTime _calculateMonths(int months) {
|
|
||||||
final now = DateTime.now();
|
|
||||||
return DateTime(now.year, now.month + months, now.day);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quando l'utente seleziona un tipo, impostiamo il default
|
|
||||||
void setTypeWithSmartDefault(String type) {
|
|
||||||
DateTime? defaultDate;
|
|
||||||
|
|
||||||
if (type == 'Energy') defaultDate = _calculateMonths(24);
|
|
||||||
if (type == 'Fin') defaultDate = _calculateMonths(30);
|
|
||||||
if (type == 'Entertainment') defaultDate = _calculateMonths(12);
|
|
||||||
|
|
||||||
updateOperationFields(
|
|
||||||
type: type,
|
|
||||||
expirationDate: defaultDate,
|
|
||||||
clearProvider: true,
|
|
||||||
clearSubtype: true,
|
|
||||||
clearModel: true,
|
|
||||||
clearQuantity: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
part of 'operations_cubit.dart';
|
|
||||||
|
|
||||||
enum OperationsStatus {
|
|
||||||
initial,
|
|
||||||
loading,
|
|
||||||
ready,
|
|
||||||
saving,
|
|
||||||
saved,
|
|
||||||
savedNoPop,
|
|
||||||
success,
|
|
||||||
failure,
|
|
||||||
}
|
|
||||||
|
|
||||||
class OperationsState extends Equatable {
|
|
||||||
final OperationsStatus status;
|
|
||||||
final List<OperationModel> allOperations;
|
|
||||||
final OperationModel? currentOperation; // La bozza che stiamo editando
|
|
||||||
final String? errorMessage;
|
|
||||||
final String query;
|
|
||||||
final DateTimeRange? dateRange;
|
|
||||||
final bool hasReachedMax;
|
|
||||||
final bool isSavingDraft;
|
|
||||||
|
|
||||||
const OperationsState({
|
|
||||||
required this.status,
|
|
||||||
this.allOperations = const [],
|
|
||||||
this.currentOperation,
|
|
||||||
this.errorMessage,
|
|
||||||
this.query = '',
|
|
||||||
this.dateRange,
|
|
||||||
this.hasReachedMax = false,
|
|
||||||
this.isSavingDraft = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
OperationsState copyWith({
|
|
||||||
OperationsStatus? status,
|
|
||||||
List<OperationModel>? allOperations,
|
|
||||||
OperationModel? currentOperation,
|
|
||||||
String? errorMessage,
|
|
||||||
String? query,
|
|
||||||
DateTimeRange? dateRange,
|
|
||||||
bool? hasReachedMax,
|
|
||||||
bool? isSavingDraft,
|
|
||||||
}) {
|
|
||||||
return OperationsState(
|
|
||||||
status: status ?? this.status,
|
|
||||||
allOperations: allOperations ?? this.allOperations,
|
|
||||||
currentOperation: currentOperation ?? this.currentOperation,
|
|
||||||
errorMessage: errorMessage,
|
|
||||||
query: query ?? this.query,
|
|
||||||
dateRange: dateRange ?? this.dateRange,
|
|
||||||
hasReachedMax: hasReachedMax ?? this.hasReachedMax,
|
|
||||||
isSavingDraft: isSavingDraft ?? this.isSavingDraft,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
List<Object?> get props => [
|
|
||||||
status,
|
|
||||||
allOperations,
|
|
||||||
currentOperation,
|
|
||||||
errorMessage,
|
|
||||||
query,
|
|
||||||
dateRange,
|
|
||||||
hasReachedMax,
|
|
||||||
isSavingDraft,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -3,13 +3,11 @@ import 'package:flux/core/utils/extensions.dart';
|
|||||||
import 'package:flux/features/attachments/models/attachment_model.dart';
|
import 'package:flux/features/attachments/models/attachment_model.dart';
|
||||||
|
|
||||||
enum OperationStatus {
|
enum OperationStatus {
|
||||||
ok('ok'),
|
success('success', 'OK'),
|
||||||
waitingforaction('waiting_for_action'),
|
waitingForAction('waiting_for_action', 'In attesa di azione'),
|
||||||
waitingforsupport('waiting_for_support'),
|
waitingForSupport('waiting_for_support', 'In attesa di supporto'),
|
||||||
waitingfordeployment('waiting_for_deployment'),
|
failure('failure', 'KO'),
|
||||||
ko('ko'),
|
draft('draft', 'Bozza');
|
||||||
draft('draft'),
|
|
||||||
canceled('canceled');
|
|
||||||
|
|
||||||
static OperationStatus fromString(String value) {
|
static OperationStatus fromString(String value) {
|
||||||
final normalizedValue = value.replaceAll('_', '').toLowerCase();
|
final normalizedValue = value.replaceAll('_', '').toLowerCase();
|
||||||
@@ -19,8 +17,9 @@ enum OperationStatus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final String supabaseName;
|
final String supabaseName;
|
||||||
|
final String displayName;
|
||||||
|
|
||||||
const OperationStatus(this.supabaseName);
|
const OperationStatus(this.supabaseName, this.displayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
class OperationModel extends Equatable {
|
class OperationModel extends Equatable {
|
||||||
@@ -163,8 +162,8 @@ class OperationModel extends Equatable {
|
|||||||
attachments,
|
attachments,
|
||||||
];
|
];
|
||||||
|
|
||||||
factory OperationModel.empty({required String companyId}) {
|
factory OperationModel.empty() {
|
||||||
return OperationModel(id: null, createdAt: null, companyId: companyId);
|
return OperationModel(id: null, createdAt: null, companyId: '');
|
||||||
}
|
}
|
||||||
|
|
||||||
factory OperationModel.fromMap(Map<String, dynamic> map) {
|
factory OperationModel.fromMap(Map<String, dynamic> map) {
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
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/blocs/session/session_cubit.dart';
|
import 'package:flux/core/widgets/shared_forms/shared_files_section.dart';
|
||||||
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
||||||
import 'package:flux/features/operations/blocs/operations_cubit.dart';
|
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/attachments_section.dart';
|
||||||
import 'package:flux/core/widgets/shared_forms/staff_section.dart';
|
import 'package:flux/core/widgets/shared_forms/staff_section.dart';
|
||||||
import 'package:get_it/get_it.dart';
|
|
||||||
|
|
||||||
class OperationFormScreen extends StatefulWidget {
|
class OperationFormScreen extends StatefulWidget {
|
||||||
final String? operationId;
|
final String? operationId;
|
||||||
@@ -49,26 +48,10 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
final cubit = context.read<OperationsCubit>();
|
context.read<OperationFormCubit>().initForm(
|
||||||
final currentLoggedStaff = GetIt.I
|
|
||||||
.get<SessionCubit>()
|
|
||||||
.state
|
|
||||||
.currentStaffMember!;
|
|
||||||
|
|
||||||
// 1. Diciamo al Cubit di prepararsi
|
|
||||||
cubit.initOperationForm(
|
|
||||||
existingOperation: widget.existingOperation,
|
existingOperation: widget.existingOperation,
|
||||||
operationId: widget.operationId,
|
operationId: widget.operationId,
|
||||||
staffId: currentLoggedStaff.id,
|
|
||||||
staffDisplayName: currentLoggedStaff.name,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. IL TRUCCO MAGICO:
|
|
||||||
// Se abbiamo passato existingOperation, il Cubit si è appena aggiornato.
|
|
||||||
// Lo stato è già pronto, quindi sincronizziamo i controller SUBITO!
|
|
||||||
if (cubit.state.currentOperation != null) {
|
|
||||||
_syncTextControllers(cubit.state.currentOperation!);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -76,84 +59,111 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
_referenceController.dispose();
|
_referenceController.dispose();
|
||||||
_noteController.dispose();
|
_noteController.dispose();
|
||||||
_freeTextSubtypeController.dispose();
|
_freeTextSubtypeController.dispose();
|
||||||
|
_freeTextDescriptionController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _syncTextControllers(OperationModel model) {
|
void _syncTextControllers(OperationModel model) {
|
||||||
if (_referenceController.text.isEmpty && model.reference.isNotEmpty) {
|
if (_referenceController.text.isEmpty) {
|
||||||
_referenceController.text = model.reference;
|
_referenceController.text = model.reference;
|
||||||
}
|
}
|
||||||
if (_noteController.text.isEmpty && model.note.isNotEmpty) {
|
if (_noteController.text.isEmpty) {
|
||||||
_noteController.text = model.note;
|
_noteController.text = model.note;
|
||||||
}
|
}
|
||||||
if (_freeTextSubtypeController.text.isEmpty &&
|
if (_freeTextSubtypeController.text.isEmpty) {
|
||||||
model.subtype != null &&
|
_freeTextSubtypeController.text = model.subtype ?? '';
|
||||||
model.subtype!.isNotEmpty) {
|
|
||||||
_freeTextSubtypeController.text = model.subtype!;
|
|
||||||
}
|
}
|
||||||
if (_freeTextDescriptionController.text.isEmpty &&
|
if (_freeTextDescriptionController.text.isEmpty) {
|
||||||
model.description != null &&
|
_freeTextDescriptionController.text = model.description ?? '';
|
||||||
model.description!.isNotEmpty) {
|
|
||||||
_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _saveOperation({required bool keepAdding}) {
|
void _flushControllersToCubit() {
|
||||||
if (_formKey.currentState!.validate()) {
|
context.read<OperationFormCubit>().updateFields(
|
||||||
final cubit = context.read<OperationsCubit>();
|
|
||||||
final currentOperation = cubit.state.currentOperation!;
|
|
||||||
|
|
||||||
final operationToSave = currentOperation.copyWith(
|
|
||||||
reference: _referenceController.text,
|
reference: _referenceController.text,
|
||||||
note: _noteController.text,
|
note: _noteController.text,
|
||||||
subtype: ['Entertainment', 'Custom'].contains(currentOperation.type)
|
subtype: _freeTextSubtypeController.text,
|
||||||
? _freeTextSubtypeController.text
|
description: _freeTextDescriptionController.text,
|
||||||
: currentOperation.subtype,
|
|
||||||
description: ['Energy', 'Custom'].contains(currentOperation.type)
|
|
||||||
? _freeTextDescriptionController.text
|
|
||||||
: currentOperation.description,
|
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
cubit.initOperationForm(existingOperation: operationToSave);
|
void _saveOperation({
|
||||||
cubit.saveCurrentOperation(
|
required OperationStatus targetStatus,
|
||||||
targetStatus: OperationStatus.ok,
|
required bool keepAdding,
|
||||||
shouldPop: !keepAdding,
|
}) {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
_flushControllersToCubit();
|
||||||
|
context.read<OperationFormCubit>().saveOperation(
|
||||||
|
targetStatus: targetStatus,
|
||||||
|
keepAdding: keepAdding,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<String?> _generateIdForQr() async {
|
||||||
|
if (!_formKey.currentState!.validate()) return null;
|
||||||
|
_flushControllersToCubit();
|
||||||
|
final attachmentsBloc = context.read<AttachmentsBloc>();
|
||||||
|
// Presumo tu abbia creato il metodo saveOperationDraft() nel Cubit!
|
||||||
|
final newId = await context.read<OperationFormCubit>().saveOperationDraft();
|
||||||
|
if (newId != null && context.mounted) {
|
||||||
|
attachmentsBloc.add(ParentEntitySavedEvent(newId));
|
||||||
|
}
|
||||||
|
return newId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper per assegnare un colore agli stati
|
||||||
|
Color _getStatusColor(OperationStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case OperationStatus.success:
|
||||||
|
return Colors.green;
|
||||||
|
case OperationStatus.waitingForAction:
|
||||||
|
return Colors.orange;
|
||||||
|
case OperationStatus.waitingForSupport:
|
||||||
|
return Colors.blue;
|
||||||
|
case OperationStatus.failure:
|
||||||
|
return Colors.red;
|
||||||
|
case OperationStatus.draft:
|
||||||
|
return Colors.grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
return BlocConsumer<OperationsCubit, OperationsState>(
|
return BlocConsumer<OperationFormCubit, OperationFormState>(
|
||||||
listenWhen: (previous, current) =>
|
listenWhen: (previous, current) => previous.status != current.status,
|
||||||
previous.status != current.status ||
|
|
||||||
previous.currentOperation?.id != current.currentOperation?.id,
|
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.status == OperationsStatus.ready &&
|
if (state.status == OperationFormStatus.ready && !_isInitialized) {
|
||||||
state.currentOperation != null &&
|
_syncTextControllers(state.operation);
|
||||||
!_isInitialized) {
|
|
||||||
_syncTextControllers(state.currentOperation!);
|
|
||||||
}
|
}
|
||||||
|
if (state.status == OperationFormStatus.success) {
|
||||||
if (state.status == OperationsStatus.saved) {
|
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
} else if (state.status == OperationsStatus.savedNoPop) {
|
} else if (state.status == OperationFormStatus.successAndAddAnother) {
|
||||||
context.read<OperationsCubit>().prepareNextOperationInBatch();
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Servizio aggiunto! Inserisci il prossimo.'),
|
content: Text('Operazione salvata! Inserisci la prossima'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
_freeTextSubtypeController.clear();
|
_freeTextSubtypeController.clear();
|
||||||
_freeTextDescriptionController.clear();
|
_freeTextDescriptionController.clear();
|
||||||
} else if (state.status == OperationsStatus.failure) {
|
} else if (state.status == OperationFormStatus.failure) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(state.errorMessage ?? 'Errore'),
|
content: Text(state.errorMessage ?? 'Errore di salvataggio'),
|
||||||
backgroundColor: theme.colorScheme.error,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -161,19 +171,45 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (!_isInitialized &&
|
if (!_isInitialized &&
|
||||||
(widget.operationId != null || widget.existingOperation != null) &&
|
(widget.operationId != null || widget.existingOperation != null) &&
|
||||||
state.status == OperationsStatus.loading) {
|
state.status == OperationFormStatus.loading) {
|
||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
body: Center(child: CircularProgressIndicator()),
|
body: Center(child: CircularProgressIndicator()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determiniamo lo stato da mostrare nel form.
|
||||||
|
// Se è una bozza appena creata, mostriamo visivamente "OK" come default per il salvataggio.
|
||||||
|
final displayStatus =
|
||||||
|
state.operation.status == OperationStatus.draft &&
|
||||||
|
state.operation.id == null
|
||||||
|
? OperationStatus.success
|
||||||
|
: state.operation.status;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
title: Text(
|
||||||
state.currentOperation?.id == null
|
state.operation.id == null ? 'Nuova Pratica' : 'Modifica Pratica',
|
||||||
? 'Nuova Pratica'
|
|
||||||
: 'Modifica Pratica',
|
|
||||||
),
|
),
|
||||||
|
// Mettiamo un piccolo indicatore visivo anche nella AppBar se non è OK
|
||||||
|
actions:
|
||||||
|
displayStatus != OperationStatus.success &&
|
||||||
|
displayStatus != OperationStatus.draft
|
||||||
|
? [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 16.0),
|
||||||
|
child: Chip(
|
||||||
|
label: Text(
|
||||||
|
displayStatus.displayName,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
backgroundColor: _getStatusColor(displayStatus),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
),
|
),
|
||||||
body: Form(
|
body: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
@@ -182,26 +218,22 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
final isUltraWide = constraints.maxWidth > 1400;
|
final isUltraWide = constraints.maxWidth > 1400;
|
||||||
final isDesktop = constraints.maxWidth > 900;
|
final isDesktop = constraints.maxWidth > 900;
|
||||||
if (isUltraWide) {
|
if (isUltraWide) {
|
||||||
// --- LAYOUT 3 COLONNE (Schermi giganti) ---
|
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 1. FORM PRINCIPALE (40%)
|
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 4,
|
flex: 4,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
// Attenzione: devi togliere la sezione file dal _buildMainFormContent!
|
|
||||||
child: _buildMainFormContent(
|
child: _buildMainFormContent(
|
||||||
theme,
|
theme,
|
||||||
state,
|
state,
|
||||||
|
displayStatus,
|
||||||
showFiles: false,
|
showFiles: false,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
VerticalDivider(width: 1, color: theme.dividerColor),
|
VerticalDivider(width: 1, color: theme.dividerColor),
|
||||||
|
|
||||||
// 2. NOTE (30%)
|
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -210,15 +242,15 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
VerticalDivider(width: 1, color: theme.dividerColor),
|
VerticalDivider(width: 1, color: theme.dividerColor),
|
||||||
|
|
||||||
// 3. FILE (30%)
|
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 3,
|
flex: 3,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: SharedAttachmentsSection(
|
child: SharedFilesSection(
|
||||||
parentType: AttachmentParentType.operation,
|
titleNameForUpload:
|
||||||
parentId: state.currentOperation?.id,
|
state.operation.customerDisplayName ??
|
||||||
|
'Nuova operazione',
|
||||||
|
onGenerateIdForQr: _generateIdForQr,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -232,7 +264,11 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
flex: 7,
|
flex: 7,
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: _buildMainFormContent(theme, state),
|
child: _buildMainFormContent(
|
||||||
|
theme,
|
||||||
|
state,
|
||||||
|
displayStatus,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
VerticalDivider(width: 1, color: theme.dividerColor),
|
VerticalDivider(width: 1, color: theme.dividerColor),
|
||||||
@@ -251,7 +287,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildMainFormContent(theme, state),
|
_buildMainFormContent(theme, state, displayStatus),
|
||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
_buildNotesSection(isDesktop: false),
|
_buildNotesSection(isDesktop: false),
|
||||||
const SizedBox(height: 80),
|
const SizedBox(height: 80),
|
||||||
@@ -270,9 +306,13 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 1,
|
flex: 1,
|
||||||
child: OutlinedButton(
|
child: OutlinedButton(
|
||||||
onPressed: state.status == OperationsStatus.saving
|
onPressed: state.status == OperationFormStatus.saving
|
||||||
? null
|
? null
|
||||||
: () => _saveOperation(keepAdding: true),
|
: () => _saveOperation(
|
||||||
|
keepAdding: true,
|
||||||
|
targetStatus:
|
||||||
|
displayStatus, // <-- Usiamo lo stato selezionato nel form!
|
||||||
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'Salva e Aggiungi Altro',
|
'Salva e Aggiungi Altro',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
@@ -283,10 +323,27 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
flex: 1,
|
flex: 1,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: state.status == OperationsStatus.saving
|
style: ElevatedButton.styleFrom(
|
||||||
|
// Se c'è un KO o un blocco, cambiamo il colore del bottone principale per attirare l'attenzione
|
||||||
|
backgroundColor:
|
||||||
|
displayStatus != OperationStatus.success &&
|
||||||
|
displayStatus != OperationStatus.draft
|
||||||
|
? _getStatusColor(displayStatus)
|
||||||
|
: null,
|
||||||
|
foregroundColor:
|
||||||
|
displayStatus != OperationStatus.success &&
|
||||||
|
displayStatus != OperationStatus.draft
|
||||||
|
? Colors.white
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
onPressed: state.status == OperationFormStatus.saving
|
||||||
? null
|
? null
|
||||||
: () => _saveOperation(keepAdding: false),
|
: () => _saveOperation(
|
||||||
child: state.status == OperationsStatus.saving
|
keepAdding: false,
|
||||||
|
targetStatus:
|
||||||
|
displayStatus, // <-- Usiamo lo stato selezionato nel form!
|
||||||
|
),
|
||||||
|
child: state.status == OperationFormStatus.saving
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
@@ -309,32 +366,102 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
|
|
||||||
Widget _buildMainFormContent(
|
Widget _buildMainFormContent(
|
||||||
ThemeData theme,
|
ThemeData theme,
|
||||||
OperationsState state, {
|
OperationFormState state,
|
||||||
|
OperationStatus displayStatus, {
|
||||||
bool showFiles = true,
|
bool showFiles = true,
|
||||||
}) {
|
}) {
|
||||||
final currentOp = state.currentOperation;
|
final currentOp = state.operation;
|
||||||
final currentType = currentOp?.type ?? 'AL';
|
final currentType = currentOp.type;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
StaffSection(
|
StaffSection(
|
||||||
staffId: currentOp?.staffId,
|
staffId: currentOp.staffId,
|
||||||
staffName: currentOp?.staffDisplayName,
|
staffName: currentOp.staffDisplayName,
|
||||||
onStaffSelected: (staff) => {
|
onStaffSelected: (staff) => {
|
||||||
context.read<OperationsCubit>().updateOperationFields(
|
context.read<OperationFormCubit>().updateFields(
|
||||||
staffId: staff.id,
|
staffId: staff.id,
|
||||||
staffDisplayName: staff.name,
|
staffDisplayName: staff.name,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(height: 50),
|
const Divider(height: 50),
|
||||||
|
|
||||||
|
// --- SEZIONE STATO OPERAZIONE ---
|
||||||
|
_buildSectionTitle('Esito / Stato Operazione'),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _getStatusColor(displayStatus).withValues(alpha: 0.1),
|
||||||
|
border: Border.all(
|
||||||
|
color: _getStatusColor(displayStatus).withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<OperationStatus>(
|
||||||
|
isExpanded: true,
|
||||||
|
value: displayStatus,
|
||||||
|
icon: Icon(
|
||||||
|
Icons.arrow_drop_down,
|
||||||
|
color: _getStatusColor(displayStatus),
|
||||||
|
),
|
||||||
|
items: OperationStatus.values
|
||||||
|
.where(
|
||||||
|
(s) => s != OperationStatus.draft,
|
||||||
|
) // Nascondiamo 'Bozza' dal menu
|
||||||
|
.map(
|
||||||
|
(status) => DropdownMenuItem(
|
||||||
|
value: status,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
status == OperationStatus.success
|
||||||
|
? Icons.check_circle
|
||||||
|
: Icons.error_outline,
|
||||||
|
color: _getStatusColor(status),
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
status.displayName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: _getStatusColor(status),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
onChanged: (newStatus) {
|
||||||
|
if (newStatus != null) {
|
||||||
|
// Assicurati che il metodo updateFields nel tuo Cubit accetti anche 'status'
|
||||||
|
context.read<OperationFormCubit>().updateFields(
|
||||||
|
status: newStatus,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
displayStatus == OperationStatus.success
|
||||||
|
? 'Lascia OK se la pratica è stata caricata con successo.'
|
||||||
|
: 'Attenzione: la pratica verrà salvata come ${displayStatus.displayName}.',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||||
|
),
|
||||||
|
const Divider(height: 32),
|
||||||
|
|
||||||
_buildSectionTitle('Cliente & Riferimento'),
|
_buildSectionTitle('Cliente & Riferimento'),
|
||||||
SharedCustomerSection(
|
SharedCustomerSection(
|
||||||
customerId: currentOp?.customerId,
|
customerId: currentOp.customerId,
|
||||||
customerName: currentOp?.customerDisplayName,
|
customerName: currentOp.customerDisplayName,
|
||||||
onCustomerSelected: (customer) {
|
onCustomerSelected: (customer) {
|
||||||
context.read<OperationsCubit>().updateOperationFields(
|
context.read<OperationFormCubit>().updateFields(
|
||||||
customerId: customer.id,
|
customerId: customer.id,
|
||||||
customerDisplayName: customer.name,
|
customerDisplayName: customer.name,
|
||||||
);
|
);
|
||||||
@@ -360,7 +487,9 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
selected: currentType == type,
|
selected: currentType == type,
|
||||||
onSelected: (selected) {
|
onSelected: (selected) {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
context.read<OperationsCubit>().setTypeWithSmartDefault(type);
|
context.read<OperationFormCubit>().setTypeWithSmartDefault(
|
||||||
|
type,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -384,23 +513,23 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(Icons.remove),
|
icon: const Icon(Icons.remove),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final q = currentOp?.quantity ?? 1;
|
final q = currentOp.quantity;
|
||||||
if (q > 1) {
|
if (q > 1) {
|
||||||
context.read<OperationsCubit>().updateOperationFields(
|
context.read<OperationFormCubit>().updateFields(
|
||||||
quantity: q - 1,
|
quantity: q - 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${currentOp?.quantity ?? 1}',
|
'${currentOp.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),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final q = currentOp?.quantity ?? 1;
|
final q = currentOp.quantity;
|
||||||
context.read<OperationsCubit>().updateOperationFields(
|
context.read<OperationFormCubit>().updateFields(
|
||||||
quantity: q + 1,
|
quantity: q + 1,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -410,9 +539,14 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
|
|
||||||
if (showFiles) ...[
|
if (showFiles) ...[
|
||||||
SharedAttachmentsSection(
|
/* SharedAttachmentsSection(
|
||||||
parentType: AttachmentParentType.operation,
|
parentType: AttachmentParentType.operation,
|
||||||
parentId: currentOp?.id,
|
parentId: currentOp.id,
|
||||||
|
), */
|
||||||
|
SharedFilesSection(
|
||||||
|
titleNameForUpload:
|
||||||
|
state.operation.customerDisplayName ?? 'Nuova pratica',
|
||||||
|
onGenerateIdForQr: _generateIdForQr,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -444,7 +578,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
|
|||||||
backgroundColor: Colors.blue.withValues(alpha: 0.05),
|
backgroundColor: Colors.blue.withValues(alpha: 0.05),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
context.read<OperationsCubit>().updateOperationFields(
|
context.read<OperationFormCubit>().updateFields(
|
||||||
expirationDate: DateTime(
|
expirationDate: DateTime(
|
||||||
now.year,
|
now.year,
|
||||||
now.month + months,
|
now.month + months,
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flux/features/operations/blocs/operations_cubit.dart';
|
import 'package:flux/features/operations/blocs/operation_list_cubit.dart';
|
||||||
import 'package:flux/features/operations/models/operation_model.dart';
|
import 'package:flux/features/operations/models/operation_model.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
// Importa i tuoi modelli e cubit
|
// Importa i tuoi modelli e cubit
|
||||||
|
|
||||||
class OperationsScreen extends StatefulWidget {
|
class OperationListScreen extends StatefulWidget {
|
||||||
const OperationsScreen({super.key});
|
const OperationListScreen({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<OperationsScreen> createState() => _OperationsScreenState();
|
State<OperationListScreen> createState() => _OperationListScreenState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _OperationsScreenState extends State<OperationsScreen> {
|
class _OperationListScreenState extends State<OperationListScreen> {
|
||||||
final ScrollController _scrollController = ScrollController();
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -20,13 +20,11 @@ class _OperationsScreenState extends State<OperationsScreen> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
// Agganciamo il listener per la paginazione (Scroll Infinito)
|
// Agganciamo il listener per la paginazione (Scroll Infinito)
|
||||||
_scrollController.addListener(_onScroll);
|
_scrollController.addListener(_onScroll);
|
||||||
// Carichiamo i servizi iniziali
|
|
||||||
context.read<OperationsCubit>().loadOperations();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onScroll() {
|
void _onScroll() {
|
||||||
if (_isBottom) {
|
if (_isBottom) {
|
||||||
context.read<OperationsCubit>().loadOperations();
|
context.read<OperationListCubit>().loadOperations();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,16 +57,16 @@ class _OperationsScreenState extends State<OperationsScreen> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: BlocBuilder<OperationsCubit, OperationsState>(
|
body: BlocBuilder<OperationListCubit, OperationListState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
// 1. Stato di caricamento iniziale
|
// 1. Stato di caricamento iniziale
|
||||||
if (state.status == OperationsStatus.loading &&
|
if (state.status == OperationListStatus.loading &&
|
||||||
state.allOperations.isEmpty) {
|
state.operations.isEmpty) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Lista vuota
|
// 2. Lista vuota
|
||||||
if (state.allOperations.isEmpty) {
|
if (state.operations.isEmpty) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -77,7 +75,7 @@ class _OperationsScreenState extends State<OperationsScreen> {
|
|||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () => context
|
onPressed: () => context
|
||||||
.read<OperationsCubit>()
|
.read<OperationListCubit>()
|
||||||
.loadOperations(refresh: true),
|
.loadOperations(refresh: true),
|
||||||
child: const Text("Riprova"),
|
child: const Text("Riprova"),
|
||||||
),
|
),
|
||||||
@@ -88,16 +86,17 @@ class _OperationsScreenState extends State<OperationsScreen> {
|
|||||||
|
|
||||||
// 3. La Lista (con Pull-to-refresh)
|
// 3. La Lista (con Pull-to-refresh)
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () =>
|
onRefresh: () => context.read<OperationListCubit>().loadOperations(
|
||||||
context.read<OperationsCubit>().loadOperations(refresh: true),
|
refresh: true,
|
||||||
|
),
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
padding: const EdgeInsets.only(bottom: 80), // Spazio per il FAB
|
padding: const EdgeInsets.only(bottom: 80), // Spazio per il FAB
|
||||||
itemCount: state.hasReachedMax
|
itemCount: state.hasReachedMax
|
||||||
? state.allOperations.length
|
? state.operations.length
|
||||||
: state.allOperations.length + 1,
|
: state.operations.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index >= state.allOperations.length) {
|
if (index >= state.operations.length) {
|
||||||
return const Center(
|
return const Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(16.0),
|
padding: EdgeInsets.all(16.0),
|
||||||
@@ -106,7 +105,7 @@ class _OperationsScreenState extends State<OperationsScreen> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final operation = state.allOperations[index];
|
final operation = state.operations[index];
|
||||||
return _buildOperationCard(context, operation);
|
return _buildOperationCard(context, operation);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -173,17 +172,16 @@ class _OperationsScreenState extends State<OperationsScreen> {
|
|||||||
Widget _buildOperationStatus(OperationStatus status) {
|
Widget _buildOperationStatus(OperationStatus status) {
|
||||||
Color color;
|
Color color;
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case OperationStatus.canceled || OperationStatus.ko:
|
case OperationStatus.failure:
|
||||||
color = Colors.grey.shade800;
|
color = Colors.grey.shade800;
|
||||||
break;
|
break;
|
||||||
case OperationStatus.waitingforaction || OperationStatus.draft:
|
case OperationStatus.waitingForAction || OperationStatus.draft:
|
||||||
color = Colors.orange;
|
color = Colors.orange;
|
||||||
break;
|
break;
|
||||||
case OperationStatus.ok:
|
case OperationStatus.success:
|
||||||
color = Colors.green;
|
color = Colors.green;
|
||||||
break;
|
break;
|
||||||
case OperationStatus.waitingfordeployment ||
|
case OperationStatus.waitingForSupport:
|
||||||
OperationStatus.waitingforsupport:
|
|
||||||
color = Colors.blue;
|
color = Colors.blue;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -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/widgets/shared_forms/model_section.dart';
|
import 'package:flux/core/widgets/shared_forms/model_section.dart';
|
||||||
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
|
import 'package:flux/features/master_data/providers/blocs/provider_cubit.dart';
|
||||||
import 'package:flux/features/operations/blocs/operations_cubit.dart';
|
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';
|
||||||
|
|
||||||
class DetailsSection extends StatelessWidget {
|
class DetailsSection extends StatelessWidget {
|
||||||
@@ -117,9 +117,7 @@ class DetailsSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context
|
context.read<OperationFormCubit>().updateFields(
|
||||||
.read<OperationsCubit>()
|
|
||||||
.updateOperationFields(
|
|
||||||
providerId: provider.id,
|
providerId: provider.id,
|
||||||
providerDisplayName: provider.name,
|
providerDisplayName: provider.name,
|
||||||
);
|
);
|
||||||
@@ -190,9 +188,7 @@ class DetailsSection extends StatelessWidget {
|
|||||||
].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
|
].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
if (val != null) {
|
if (val != null) {
|
||||||
context.read<OperationsCubit>().updateOperationFields(
|
context.read<OperationFormCubit>().updateFields(subtype: val);
|
||||||
subtype: val,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -215,7 +211,7 @@ class DetailsSection extends StatelessWidget {
|
|||||||
modelId: currentOp?.modelId,
|
modelId: currentOp?.modelId,
|
||||||
modelName: currentOp?.modelDisplayName,
|
modelName: currentOp?.modelDisplayName,
|
||||||
onModelSelected: (id, name) {
|
onModelSelected: (id, name) {
|
||||||
context.read<OperationsCubit>().updateOperationFields(
|
context.read<OperationFormCubit>().updateFields(
|
||||||
modelId: id,
|
modelId: id,
|
||||||
modelDisplayName: name,
|
modelDisplayName: name,
|
||||||
);
|
);
|
||||||
@@ -271,7 +267,7 @@ class DetailsSection extends StatelessWidget {
|
|||||||
lastDate: DateTime.now().add(const Duration(days: 3650)),
|
lastDate: DateTime.now().add(const Duration(days: 3650)),
|
||||||
);
|
);
|
||||||
if (date != null && context.mounted) {
|
if (date != null && context.mounted) {
|
||||||
context.read<OperationsCubit>().updateOperationFields(
|
context.read<OperationFormCubit>().updateFields(
|
||||||
expirationDate: date,
|
expirationDate: date,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,13 +107,15 @@ class _TicketFormScreenState extends State<TicketFormScreen> {
|
|||||||
// 2. Sincronizziamo i testi scritti a mano nel Cubit
|
// 2. Sincronizziamo i testi scritti a mano nel Cubit
|
||||||
_flushControllersToCubit();
|
_flushControllersToCubit();
|
||||||
|
|
||||||
|
final attachmentsBloc = context.read<AttachmentsBloc>();
|
||||||
|
|
||||||
// 3. Facciamo il salvataggio silenzioso
|
// 3. Facciamo il salvataggio silenzioso
|
||||||
final newId = await context.read<TicketFormCubit>().saveTicketDraft();
|
final newId = await context.read<TicketFormCubit>().saveTicketDraft();
|
||||||
|
|
||||||
if (newId != null && context.mounted) {
|
if (newId != null && context.mounted) {
|
||||||
// 4. IL TOCCO DI CLASSE: Diciamo all'AttachmentsBloc che ora la pratica ha un ID!
|
// 4. IL TOCCO DI CLASSE: Diciamo all'AttachmentsBloc che ora la pratica ha un ID!
|
||||||
// Questo farà partire l'upload automatico di eventuali file "in bozza"
|
// Questo farà partire l'upload automatico di eventuali file "in bozza"
|
||||||
context.read<AttachmentsBloc>().add(ParentEntitySavedEvent(newId));
|
attachmentsBloc.add(ParentEntitySavedEvent(newId));
|
||||||
}
|
}
|
||||||
|
|
||||||
return newId;
|
return newId;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||||
import 'package:flux/features/attachments/data/attachments_repository.dart';
|
import 'package:flux/features/attachments/data/attachments_repository.dart';
|
||||||
import 'package:flux/features/auth/bloc/auth_cubit.dart';
|
import 'package:flux/features/auth/bloc/auth_cubit.dart';
|
||||||
|
import 'package:flux/features/operations/blocs/operation_list_cubit.dart';
|
||||||
import 'package:flux/features/operations/data/operations_repository.dart';
|
import 'package:flux/features/operations/data/operations_repository.dart';
|
||||||
import 'package:flux/features/tickets/data/ticket_repository.dart';
|
import 'package:flux/features/tickets/data/ticket_repository.dart';
|
||||||
import 'package:flux/l10n/app_localizations.dart';
|
import 'package:flux/l10n/app_localizations.dart';
|
||||||
@@ -28,7 +29,6 @@ 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/operations_cubit.dart';
|
|
||||||
import 'package:flux/features/settings/settings.dart';
|
import 'package:flux/features/settings/settings.dart';
|
||||||
import 'package:flutter_web_plugins/url_strategy.dart';
|
import 'package:flutter_web_plugins/url_strategy.dart';
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ void main() async {
|
|||||||
BlocProvider<CustomersCubit>(create: (_) => CustomersCubit()),
|
BlocProvider<CustomersCubit>(create: (_) => CustomersCubit()),
|
||||||
BlocProvider<ProductsCubit>(create: (_) => ProductsCubit()),
|
BlocProvider<ProductsCubit>(create: (_) => ProductsCubit()),
|
||||||
BlocProvider<StaffCubit>(create: (_) => StaffCubit()),
|
BlocProvider<StaffCubit>(create: (_) => StaffCubit()),
|
||||||
BlocProvider<OperationsCubit>(create: (_) => OperationsCubit()),
|
BlocProvider<OperationListCubit>(create: (_) => OperationListCubit()),
|
||||||
BlocProvider<ProvidersCubit>(create: (_) => ProvidersCubit()),
|
BlocProvider<ProvidersCubit>(create: (_) => ProvidersCubit()),
|
||||||
],
|
],
|
||||||
child: const FluxApp(),
|
child: const FluxApp(),
|
||||||
|
|||||||
Reference in New Issue
Block a user