This commit is contained in:
2026-05-08 12:28:14 +02:00
parent 9793ba8348
commit 42a9506f02
24 changed files with 1266 additions and 959 deletions

View File

@@ -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) {

View File

@@ -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(
create: (context) => AttachmentsBloc( providers: [
parentId: realOperationId, BlocProvider(
parentType: AttachmentParentType.operation, create: (context) => AttachmentsBloc(
), parentId: realOperationId,
parentType: AttachmentParentType.operation,
),
),
BlocProvider(create: (context) => OperationFormCubit()),
],
child: OperationFormScreen( child: OperationFormScreen(
operationId: operationId ?? existingOperation?.id, operationId: realOperationId,
existingOperation: existingOperation, existingOperation: operationFromExtra,
), ),
); );
}, },

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -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,

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@@ -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) {

View File

@@ -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,50 +59,83 @@ 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() {
context.read<OperationFormCubit>().updateFields(
reference: _referenceController.text,
note: _noteController.text,
subtype: _freeTextSubtypeController.text,
description: _freeTextDescriptionController.text,
);
}
void _saveOperation({
required OperationStatus targetStatus,
required bool keepAdding,
}) {
if (_formKey.currentState!.validate()) { if (_formKey.currentState!.validate()) {
final cubit = context.read<OperationsCubit>(); _flushControllersToCubit();
final currentOperation = cubit.state.currentOperation!; context.read<OperationFormCubit>().saveOperation(
targetStatus: targetStatus,
final operationToSave = currentOperation.copyWith( keepAdding: keepAdding,
reference: _referenceController.text,
note: _noteController.text,
subtype: ['Entertainment', 'Custom'].contains(currentOperation.type)
? _freeTextSubtypeController.text
: currentOperation.subtype,
description: ['Energy', 'Custom'].contains(currentOperation.type)
? _freeTextDescriptionController.text
: currentOperation.description,
); );
}
}
cubit.initOperationForm(existingOperation: operationToSave); Future<String?> _generateIdForQr() async {
cubit.saveCurrentOperation( if (!_formKey.currentState!.validate()) return null;
targetStatus: OperationStatus.ok, _flushControllersToCubit();
shouldPop: !keepAdding, 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;
} }
} }
@@ -127,33 +143,27 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
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,

View File

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

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/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,12 +117,10 @@ class DetailsSection extends StatelessWidget {
), ),
), ),
onTap: () { onTap: () {
context context.read<OperationFormCubit>().updateFields(
.read<OperationsCubit>() providerId: provider.id,
.updateOperationFields( providerDisplayName: provider.name,
providerId: provider.id, );
providerDisplayName: provider.name,
);
Navigator.pop(modalContext); Navigator.pop(modalContext);
}, },
); );
@@ -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,
); );
} }

View File

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

View File

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