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

@@ -1,14 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flux/core/blocs/session/session_cubit.dart';
import 'package:flux/core/widgets/shared_forms/shared_files_section.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/core/widgets/shared_forms/customer_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/staff_section.dart';
import 'package:get_it/get_it.dart';
class OperationFormScreen extends StatefulWidget {
final String? operationId;
@@ -49,26 +48,10 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
@override
void initState() {
super.initState();
final cubit = context.read<OperationsCubit>();
final currentLoggedStaff = GetIt.I
.get<SessionCubit>()
.state
.currentStaffMember!;
// 1. Diciamo al Cubit di prepararsi
cubit.initOperationForm(
context.read<OperationFormCubit>().initForm(
existingOperation: widget.existingOperation,
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
@@ -76,50 +59,83 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
_referenceController.dispose();
_noteController.dispose();
_freeTextSubtypeController.dispose();
_freeTextDescriptionController.dispose();
super.dispose();
}
void _syncTextControllers(OperationModel model) {
if (_referenceController.text.isEmpty && model.reference.isNotEmpty) {
if (_referenceController.text.isEmpty) {
_referenceController.text = model.reference;
}
if (_noteController.text.isEmpty && model.note.isNotEmpty) {
if (_noteController.text.isEmpty) {
_noteController.text = model.note;
}
if (_freeTextSubtypeController.text.isEmpty &&
model.subtype != null &&
model.subtype!.isNotEmpty) {
_freeTextSubtypeController.text = model.subtype!;
if (_freeTextSubtypeController.text.isEmpty) {
_freeTextSubtypeController.text = model.subtype ?? '';
}
if (_freeTextDescriptionController.text.isEmpty &&
model.description != null &&
model.description!.isNotEmpty) {
_freeTextDescriptionController.text = model.description!;
if (_freeTextDescriptionController.text.isEmpty) {
_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;
}
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()) {
final cubit = context.read<OperationsCubit>();
final currentOperation = cubit.state.currentOperation!;
final operationToSave = currentOperation.copyWith(
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,
_flushControllersToCubit();
context.read<OperationFormCubit>().saveOperation(
targetStatus: targetStatus,
keepAdding: keepAdding,
);
}
}
cubit.initOperationForm(existingOperation: operationToSave);
cubit.saveCurrentOperation(
targetStatus: OperationStatus.ok,
shouldPop: !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;
}
}
@@ -127,33 +143,27 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
Widget build(BuildContext context) {
final theme = Theme.of(context);
return BlocConsumer<OperationsCubit, OperationsState>(
listenWhen: (previous, current) =>
previous.status != current.status ||
previous.currentOperation?.id != current.currentOperation?.id,
return BlocConsumer<OperationFormCubit, OperationFormState>(
listenWhen: (previous, current) => previous.status != current.status,
listener: (context, state) {
if (state.status == OperationsStatus.ready &&
state.currentOperation != null &&
!_isInitialized) {
_syncTextControllers(state.currentOperation!);
if (state.status == OperationFormStatus.ready && !_isInitialized) {
_syncTextControllers(state.operation);
}
if (state.status == OperationsStatus.saved) {
if (state.status == OperationFormStatus.success) {
Navigator.of(context).pop();
} else if (state.status == OperationsStatus.savedNoPop) {
context.read<OperationsCubit>().prepareNextOperationInBatch();
} else if (state.status == OperationFormStatus.successAndAddAnother) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Servizio aggiunto! Inserisci il prossimo.'),
content: Text('Operazione salvata! Inserisci la prossima'),
),
);
_freeTextSubtypeController.clear();
_freeTextDescriptionController.clear();
} else if (state.status == OperationsStatus.failure) {
} else if (state.status == OperationFormStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? 'Errore'),
backgroundColor: theme.colorScheme.error,
content: Text(state.errorMessage ?? 'Errore di salvataggio'),
backgroundColor: Colors.red,
),
);
}
@@ -161,19 +171,45 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
builder: (context, state) {
if (!_isInitialized &&
(widget.operationId != null || widget.existingOperation != null) &&
state.status == OperationsStatus.loading) {
state.status == OperationFormStatus.loading) {
return const Scaffold(
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(
appBar: AppBar(
title: Text(
state.currentOperation?.id == null
? 'Nuova Pratica'
: 'Modifica Pratica',
state.operation.id == null ? '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(
key: _formKey,
@@ -182,26 +218,22 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
final isUltraWide = constraints.maxWidth > 1400;
final isDesktop = constraints.maxWidth > 900;
if (isUltraWide) {
// --- LAYOUT 3 COLONNE (Schermi giganti) ---
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 1. FORM PRINCIPALE (40%)
Expanded(
flex: 4,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
// Attenzione: devi togliere la sezione file dal _buildMainFormContent!
child: _buildMainFormContent(
theme,
state,
displayStatus,
showFiles: false,
),
),
),
VerticalDivider(width: 1, color: theme.dividerColor),
// 2. NOTE (30%)
Expanded(
flex: 3,
child: Padding(
@@ -210,15 +242,15 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
),
),
VerticalDivider(width: 1, color: theme.dividerColor),
// 3. FILE (30%)
Expanded(
flex: 3,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: SharedAttachmentsSection(
parentType: AttachmentParentType.operation,
parentId: state.currentOperation?.id,
child: SharedFilesSection(
titleNameForUpload:
state.operation.customerDisplayName ??
'Nuova operazione',
onGenerateIdForQr: _generateIdForQr,
),
),
),
@@ -232,7 +264,11 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
flex: 7,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: _buildMainFormContent(theme, state),
child: _buildMainFormContent(
theme,
state,
displayStatus,
),
),
),
VerticalDivider(width: 1, color: theme.dividerColor),
@@ -251,7 +287,7 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildMainFormContent(theme, state),
_buildMainFormContent(theme, state, displayStatus),
const Divider(height: 32),
_buildNotesSection(isDesktop: false),
const SizedBox(height: 80),
@@ -270,9 +306,13 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
Expanded(
flex: 1,
child: OutlinedButton(
onPressed: state.status == OperationsStatus.saving
onPressed: state.status == OperationFormStatus.saving
? null
: () => _saveOperation(keepAdding: true),
: () => _saveOperation(
keepAdding: true,
targetStatus:
displayStatus, // <-- Usiamo lo stato selezionato nel form!
),
child: const Text(
'Salva e Aggiungi Altro',
textAlign: TextAlign.center,
@@ -283,10 +323,27 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
Expanded(
flex: 1,
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
: () => _saveOperation(keepAdding: false),
child: state.status == OperationsStatus.saving
: () => _saveOperation(
keepAdding: false,
targetStatus:
displayStatus, // <-- Usiamo lo stato selezionato nel form!
),
child: state.status == OperationFormStatus.saving
? const SizedBox(
width: 20,
height: 20,
@@ -309,32 +366,102 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
Widget _buildMainFormContent(
ThemeData theme,
OperationsState state, {
OperationFormState state,
OperationStatus displayStatus, {
bool showFiles = true,
}) {
final currentOp = state.currentOperation;
final currentType = currentOp?.type ?? 'AL';
final currentOp = state.operation;
final currentType = currentOp.type;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
StaffSection(
staffId: currentOp?.staffId,
staffName: currentOp?.staffDisplayName,
staffId: currentOp.staffId,
staffName: currentOp.staffDisplayName,
onStaffSelected: (staff) => {
context.read<OperationsCubit>().updateOperationFields(
context.read<OperationFormCubit>().updateFields(
staffId: staff.id,
staffDisplayName: staff.name,
),
},
),
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'),
SharedCustomerSection(
customerId: currentOp?.customerId,
customerName: currentOp?.customerDisplayName,
customerId: currentOp.customerId,
customerName: currentOp.customerDisplayName,
onCustomerSelected: (customer) {
context.read<OperationsCubit>().updateOperationFields(
context.read<OperationFormCubit>().updateFields(
customerId: customer.id,
customerDisplayName: customer.name,
);
@@ -360,7 +487,9 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
selected: currentType == type,
onSelected: (selected) {
if (selected) {
context.read<OperationsCubit>().setTypeWithSmartDefault(type);
context.read<OperationFormCubit>().setTypeWithSmartDefault(
type,
);
}
},
);
@@ -384,23 +513,23 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
IconButton(
icon: const Icon(Icons.remove),
onPressed: () {
final q = currentOp?.quantity ?? 1;
final q = currentOp.quantity;
if (q > 1) {
context.read<OperationsCubit>().updateOperationFields(
context.read<OperationFormCubit>().updateFields(
quantity: q - 1,
);
}
},
),
Text(
'${currentOp?.quantity ?? 1}',
'${currentOp.quantity}',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
final q = currentOp?.quantity ?? 1;
context.read<OperationsCubit>().updateOperationFields(
final q = currentOp.quantity;
context.read<OperationFormCubit>().updateFields(
quantity: q + 1,
);
},
@@ -410,9 +539,14 @@ class _OperationFormScreenState extends State<OperationFormScreen> {
const Divider(height: 32),
if (showFiles) ...[
SharedAttachmentsSection(
/* SharedAttachmentsSection(
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),
onPressed: () {
final now = DateTime.now();
context.read<OperationsCubit>().updateOperationFields(
context.read<OperationFormCubit>().updateFields(
expirationDate: DateTime(
now.year,
now.month + months,

View File

@@ -1,18 +1,18 @@
import 'package:flutter/material.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:go_router/go_router.dart';
// Importa i tuoi modelli e cubit
class OperationsScreen extends StatefulWidget {
const OperationsScreen({super.key});
class OperationListScreen extends StatefulWidget {
const OperationListScreen({super.key});
@override
State<OperationsScreen> createState() => _OperationsScreenState();
State<OperationListScreen> createState() => _OperationListScreenState();
}
class _OperationsScreenState extends State<OperationsScreen> {
class _OperationListScreenState extends State<OperationListScreen> {
final ScrollController _scrollController = ScrollController();
@override
@@ -20,13 +20,11 @@ class _OperationsScreenState extends State<OperationsScreen> {
super.initState();
// Agganciamo il listener per la paginazione (Scroll Infinito)
_scrollController.addListener(_onScroll);
// Carichiamo i servizi iniziali
context.read<OperationsCubit>().loadOperations();
}
void _onScroll() {
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) {
// 1. Stato di caricamento iniziale
if (state.status == OperationsStatus.loading &&
state.allOperations.isEmpty) {
if (state.status == OperationListStatus.loading &&
state.operations.isEmpty) {
return const Center(child: CircularProgressIndicator());
}
// 2. Lista vuota
if (state.allOperations.isEmpty) {
if (state.operations.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -77,7 +75,7 @@ class _OperationsScreenState extends State<OperationsScreen> {
const SizedBox(height: 10),
ElevatedButton(
onPressed: () => context
.read<OperationsCubit>()
.read<OperationListCubit>()
.loadOperations(refresh: true),
child: const Text("Riprova"),
),
@@ -88,16 +86,17 @@ class _OperationsScreenState extends State<OperationsScreen> {
// 3. La Lista (con Pull-to-refresh)
return RefreshIndicator(
onRefresh: () =>
context.read<OperationsCubit>().loadOperations(refresh: true),
onRefresh: () => context.read<OperationListCubit>().loadOperations(
refresh: true,
),
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.only(bottom: 80), // Spazio per il FAB
itemCount: state.hasReachedMax
? state.allOperations.length
: state.allOperations.length + 1,
? state.operations.length
: state.operations.length + 1,
itemBuilder: (context, index) {
if (index >= state.allOperations.length) {
if (index >= state.operations.length) {
return const Center(
child: Padding(
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);
},
),
@@ -173,17 +172,16 @@ class _OperationsScreenState extends State<OperationsScreen> {
Widget _buildOperationStatus(OperationStatus status) {
Color color;
switch (status) {
case OperationStatus.canceled || OperationStatus.ko:
case OperationStatus.failure:
color = Colors.grey.shade800;
break;
case OperationStatus.waitingforaction || OperationStatus.draft:
case OperationStatus.waitingForAction || OperationStatus.draft:
color = Colors.orange;
break;
case OperationStatus.ok:
case OperationStatus.success:
color = Colors.green;
break;
case OperationStatus.waitingfordeployment ||
OperationStatus.waitingforsupport:
case OperationStatus.waitingForSupport:
color = Colors.blue;
break;
}

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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/operations/blocs/operations_cubit.dart';
import 'package:flux/features/operations/blocs/operation_form_cubit.dart';
import 'package:flux/features/operations/models/operation_model.dart';
class DetailsSection extends StatelessWidget {
@@ -117,12 +117,10 @@ class DetailsSection extends StatelessWidget {
),
),
onTap: () {
context
.read<OperationsCubit>()
.updateOperationFields(
providerId: provider.id,
providerDisplayName: provider.name,
);
context.read<OperationFormCubit>().updateFields(
providerId: provider.id,
providerDisplayName: provider.name,
);
Navigator.pop(modalContext);
},
);
@@ -190,9 +188,7 @@ class DetailsSection extends StatelessWidget {
].map((s) => DropdownMenuItem(value: s, child: Text(s))).toList(),
onChanged: (val) {
if (val != null) {
context.read<OperationsCubit>().updateOperationFields(
subtype: val,
);
context.read<OperationFormCubit>().updateFields(subtype: val);
}
},
),
@@ -215,7 +211,7 @@ class DetailsSection extends StatelessWidget {
modelId: currentOp?.modelId,
modelName: currentOp?.modelDisplayName,
onModelSelected: (id, name) {
context.read<OperationsCubit>().updateOperationFields(
context.read<OperationFormCubit>().updateFields(
modelId: id,
modelDisplayName: name,
);
@@ -271,7 +267,7 @@ class DetailsSection extends StatelessWidget {
lastDate: DateTime.now().add(const Duration(days: 3650)),
);
if (date != null && context.mounted) {
context.read<OperationsCubit>().updateOperationFields(
context.read<OperationFormCubit>().updateFields(
expirationDate: date,
);
}