682 lines
23 KiB
Dart
682 lines
23 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_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/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';
|
|
|
|
class OperationFormScreen extends StatefulWidget {
|
|
final String? operationId;
|
|
final OperationModel? existingOperation;
|
|
|
|
const OperationFormScreen({
|
|
super.key,
|
|
this.operationId,
|
|
this.existingOperation,
|
|
});
|
|
|
|
@override
|
|
State<OperationFormScreen> createState() => _OperationFormScreenState();
|
|
}
|
|
|
|
class _OperationFormScreenState extends State<OperationFormScreen> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
|
|
final _referenceController = TextEditingController();
|
|
final _noteController = TextEditingController();
|
|
final _freeTextSubtypeController = TextEditingController();
|
|
final _freeTextDescriptionController = TextEditingController();
|
|
|
|
final List<String> _availableTypes = [
|
|
'AL',
|
|
'MNP',
|
|
'NIP',
|
|
'UNICA',
|
|
'TELEPASS',
|
|
'Energy',
|
|
'Fin',
|
|
'Entertainment',
|
|
'Custom',
|
|
];
|
|
|
|
bool _isInitialized = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
context.read<OperationFormCubit>().initForm(
|
|
existingOperation: widget.existingOperation,
|
|
operationId: widget.operationId,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_referenceController.dispose();
|
|
_noteController.dispose();
|
|
_freeTextSubtypeController.dispose();
|
|
_freeTextDescriptionController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _syncTextControllers(OperationModel model) {
|
|
if (_referenceController.text.isEmpty) {
|
|
_referenceController.text = model.reference;
|
|
}
|
|
if (_noteController.text.isEmpty) {
|
|
_noteController.text = model.note;
|
|
}
|
|
if (_freeTextSubtypeController.text.isEmpty) {
|
|
_freeTextSubtypeController.text = model.subtype ?? '';
|
|
}
|
|
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 _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()) {
|
|
_flushControllersToCubit();
|
|
context.read<OperationFormCubit>().saveOperation(
|
|
targetStatus: targetStatus,
|
|
keepAdding: keepAdding,
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<String?> _generateIdForQr() async {
|
|
if (!_formKey.currentState!.validate()) return null;
|
|
_flushControllersToCubit();
|
|
final attachmentsBloc = context.read<AttachmentsBloc>();
|
|
// Presumo tu abbia creato il metodo saveOperationDraft() nel Cubit!
|
|
final newId = await context.read<OperationFormCubit>().saveOperationDraft();
|
|
if (newId != null && context.mounted) {
|
|
attachmentsBloc.add(ParentEntitySavedEvent(newId));
|
|
}
|
|
return newId;
|
|
}
|
|
|
|
// Helper per assegnare un colore agli stati
|
|
Color _getStatusColor(OperationStatus status) {
|
|
switch (status) {
|
|
case OperationStatus.success:
|
|
return Colors.green;
|
|
case OperationStatus.waitingForAction:
|
|
return Colors.orange;
|
|
case OperationStatus.waitingForSupport:
|
|
return Colors.blue;
|
|
case OperationStatus.failure:
|
|
return Colors.red;
|
|
case OperationStatus.draft:
|
|
return Colors.grey;
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return BlocConsumer<OperationFormCubit, OperationFormState>(
|
|
listenWhen: (previous, current) => previous.status != current.status,
|
|
listener: (context, state) {
|
|
if (state.status == OperationFormStatus.ready && !_isInitialized) {
|
|
_syncTextControllers(state.operation);
|
|
}
|
|
if (state.status == OperationFormStatus.success) {
|
|
Navigator.of(context).pop();
|
|
} else if (state.status == OperationFormStatus.successAndAddAnother) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Operazione salvata! Inserisci la prossima'),
|
|
),
|
|
);
|
|
_freeTextSubtypeController.clear();
|
|
_freeTextDescriptionController.clear();
|
|
} else if (state.status == OperationFormStatus.failure) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(state.errorMessage ?? 'Errore di salvataggio'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
builder: (context, state) {
|
|
if (!_isInitialized &&
|
|
(widget.operationId != null || widget.existingOperation != null) &&
|
|
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.operation.id == null
|
|
? 'Nuova Pratica - Operatore: ${state.operation.staffDisplayName}'
|
|
: 'Modifica Pratica - Operatore: ${state.operation.staffDisplayName}',
|
|
),
|
|
// 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,
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final isUltraWide = constraints.maxWidth > 1400;
|
|
final isDesktop = constraints.maxWidth > 900;
|
|
if (isUltraWide) {
|
|
return _buildUltraWide(state, theme);
|
|
} else if (isDesktop) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
flex: 7,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: _buildMainFormContent(
|
|
theme,
|
|
state,
|
|
displayStatus,
|
|
),
|
|
),
|
|
),
|
|
VerticalDivider(width: 1, color: theme.dividerColor),
|
|
Expanded(
|
|
flex: 3,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: _buildNotesSection(isDesktop: true),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
} else {
|
|
return SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildMainFormContent(theme, state, displayStatus),
|
|
const Divider(height: 32),
|
|
_buildNotesSection(isDesktop: false),
|
|
const SizedBox(height: 80),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
},
|
|
),
|
|
),
|
|
bottomNavigationBar: SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
flex: 1,
|
|
child: ElevatedButton(
|
|
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,
|
|
targetStatus:
|
|
displayStatus, // <-- Usiamo lo stato selezionato nel form!
|
|
),
|
|
child: state.status == OperationFormStatus.saving
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
),
|
|
)
|
|
: const Text('Salva ed Esci'),
|
|
),
|
|
),
|
|
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
flex: 1,
|
|
child: OutlinedButton(
|
|
onPressed: state.status == OperationFormStatus.saving
|
|
? null
|
|
: () => _saveOperation(
|
|
keepAdding: true,
|
|
targetStatus:
|
|
displayStatus, // <-- Usiamo lo stato selezionato nel form!
|
|
),
|
|
child: const Text(
|
|
'Salva e Aggiungi Altro',
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildUltraWide(OperationFormState state, ThemeData theme) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
flex: 4,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildStaffSection(state),
|
|
const Divider(height: 50),
|
|
_buildOperationStatusSection(state),
|
|
const Divider(height: 32),
|
|
_buildCustomerSection(state),
|
|
const SizedBox(height: 16),
|
|
_buildReferenceSection(state),
|
|
const Divider(height: 50),
|
|
_buildOperationTypeSection(state),
|
|
const SizedBox(height: 16),
|
|
_buildQuantitySection(state),
|
|
const Divider(height: 50),
|
|
_buildDetailsSection(state),
|
|
const Divider(height: 50),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
VerticalDivider(width: 1, color: theme.dividerColor),
|
|
Expanded(
|
|
flex: 3,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: _buildNotesSection(isDesktop: true),
|
|
),
|
|
),
|
|
VerticalDivider(width: 1, color: theme.dividerColor),
|
|
Expanded(
|
|
flex: 3,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: _buildAttachmentSection(state),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildStaffSection(OperationFormState state) {
|
|
return StaffSection(
|
|
staffId: state.operation.staffId,
|
|
staffName: state.operation.staffDisplayName,
|
|
onStaffSelected: (staff) => {
|
|
context.read<OperationFormCubit>().updateFields(
|
|
staffId: staff.id,
|
|
staffDisplayName: staff.name,
|
|
),
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildOperationStatusSection(OperationFormState state) {
|
|
return Column(
|
|
children: [
|
|
_buildSectionTitle('Esito / Stato Operazione'),
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
decoration: BoxDecoration(
|
|
color: _getStatusColor(
|
|
state.operation.status,
|
|
).withValues(alpha: 0.1),
|
|
border: Border.all(
|
|
color: _getStatusColor(
|
|
state.operation.status,
|
|
).withValues(alpha: 0.3),
|
|
),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: DropdownButtonHideUnderline(
|
|
child: DropdownButton<OperationStatus>(
|
|
isExpanded: true,
|
|
value: state.operation.status,
|
|
icon: Icon(
|
|
Icons.arrow_drop_down,
|
|
color: _getStatusColor(state.operation.status),
|
|
),
|
|
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(
|
|
state.operation.status == OperationStatus.success
|
|
? 'Lascia OK se la pratica è stata caricata con successo.'
|
|
: 'Attenzione: la pratica verrà salvata come ${state.operation.status.displayName}.',
|
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildCustomerSection(OperationFormState state) {
|
|
return SharedCustomerSection(
|
|
customer: state.operation.customer,
|
|
onCustomerSelected: (customer) {
|
|
context.read<OperationFormCubit>().updateFields(customer: customer);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildReferenceSection(OperationFormState state) {
|
|
return TextFormField(
|
|
controller: _referenceController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Riferimento (es. numero di telefono, targa...)',
|
|
prefixIcon: Icon(Icons.tag),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildOperationTypeSection(OperationFormState state) {
|
|
return Column(
|
|
children: [
|
|
_buildSectionTitle('Cosa stiamo facendo?'),
|
|
Wrap(
|
|
spacing: 8.0,
|
|
runSpacing: 8.0,
|
|
children: _availableTypes.map((type) {
|
|
return ChoiceChip(
|
|
label: Text(type),
|
|
selected: state.operation.type == type,
|
|
onSelected: (selected) {
|
|
if (selected) {
|
|
context.read<OperationFormCubit>().setTypeWithSmartDefault(
|
|
type,
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}).toList(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailsSection(OperationFormState state) {
|
|
return Column(
|
|
children: [
|
|
_buildSectionTitle('Dettagli Servizio'),
|
|
OperationDetailsSection(
|
|
currentOp: state.operation,
|
|
currentType: state.operation.type,
|
|
freeTextSubtypeController: _freeTextSubtypeController,
|
|
freeTextDescriptionController: _freeTextDescriptionController,
|
|
durationQuickPicks: _buildDurationQuickPicks(state.operation),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildQuantitySection(OperationFormState state) {
|
|
return Row(
|
|
children: [
|
|
const Text('Quantità: '),
|
|
IconButton(
|
|
icon: const Icon(Icons.remove),
|
|
onPressed: () {
|
|
final q = state.operation.quantity;
|
|
if (q > 1) {
|
|
context.read<OperationFormCubit>().updateFields(quantity: q - 1);
|
|
}
|
|
},
|
|
),
|
|
Text(
|
|
'${state.operation.quantity}',
|
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.add),
|
|
onPressed: () {
|
|
final q = state.operation.quantity;
|
|
context.read<OperationFormCubit>().updateFields(quantity: q + 1);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildAttachmentSection(OperationFormState state) {
|
|
return SharedAttachmentsSection(
|
|
parentType: AttachmentParentType.operation,
|
|
parentId: state.operation.id,
|
|
titleForUpload: state.operation.customer?.name ?? 'Nuova pratica',
|
|
onGenerateIdForQr: _generateIdForQr,
|
|
);
|
|
}
|
|
|
|
Widget _buildMainFormContent(
|
|
ThemeData theme,
|
|
OperationFormState state,
|
|
OperationStatus displayStatus, {
|
|
bool showFiles = true,
|
|
}) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
/* _buildStaffSection(state),
|
|
const Divider(height: 50), */
|
|
_buildOperationStatusSection(state),
|
|
const Divider(height: 32),
|
|
_buildCustomerSection(state),
|
|
const SizedBox(height: 16),
|
|
_buildReferenceSection(state),
|
|
const Divider(height: 50),
|
|
_buildOperationTypeSection(state),
|
|
const SizedBox(height: 16),
|
|
_buildQuantitySection(state),
|
|
const Divider(height: 50),
|
|
_buildDetailsSection(state),
|
|
const Divider(height: 50),
|
|
|
|
// QUANTITÀ
|
|
const Divider(height: 32),
|
|
|
|
if (showFiles) ...[_buildAttachmentSection(state)],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildDurationQuickPicks(OperationModel? currentOp) {
|
|
final durations = [3, 6, 12, 24, 30, 36, 48];
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Imposta durata rapida (mesi):",
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.grey,
|
|
),
|
|
),
|
|
const SizedBox(height: 8),
|
|
SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: Row(
|
|
children: durations.map((months) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(right: 8.0),
|
|
child: ActionChip(
|
|
label: Text("$months m"),
|
|
backgroundColor: Colors.blue.withValues(alpha: 0.05),
|
|
onPressed: () {
|
|
final now = DateTime.now();
|
|
context.read<OperationFormCubit>().updateFields(
|
|
expirationDate: DateTime(
|
|
now.year,
|
|
now.month + months,
|
|
now.day,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildNotesSection({required bool isDesktop}) {
|
|
final title = _buildSectionTitle('Note Interne');
|
|
final noteField = TextFormField(
|
|
controller: _noteController,
|
|
keyboardType: TextInputType.multiline,
|
|
minLines: isDesktop ? null : 5,
|
|
maxLines: null,
|
|
expands: isDesktop,
|
|
textAlignVertical: TextAlignVertical.top,
|
|
decoration: const InputDecoration(
|
|
hintText: 'Incolla qui seriali, ICCID, IBAN, indirizzi...',
|
|
alignLabelWithHint: true,
|
|
border: OutlineInputBorder(),
|
|
),
|
|
);
|
|
return isDesktop
|
|
? Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
title,
|
|
const SizedBox(height: 8),
|
|
Expanded(child: noteField),
|
|
],
|
|
)
|
|
: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [title, const SizedBox(height: 8), noteField],
|
|
);
|
|
}
|
|
|
|
Widget _buildSectionTitle(String title) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12.0),
|
|
child: Text(
|
|
title,
|
|
style: Theme.of(
|
|
context,
|
|
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
|
|
),
|
|
);
|
|
}
|
|
}
|