Reviewed-on: #12 Co-authored-by: Mark M2 Macbook <marco@catelli.it> Co-committed-by: Mark M2 Macbook <marco@catelli.it>
482 lines
16 KiB
Dart
482 lines
16 KiB
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/blocs/operations_cubit.dart';
|
|
import 'package:flux/features/operations/models/operation_model.dart';
|
|
import 'package:flux/features/operations/ui/widgets/customer_section.dart';
|
|
import 'package:flux/features/operations/ui/widgets/details_section.dart';
|
|
import 'package:flux/features/operations/ui/widgets/operation_files_section.dart';
|
|
import 'package:flux/features/operations/ui/widgets/staff_section.dart';
|
|
import 'package:get_it/get_it.dart'; // ASSICURATI DEL PATH
|
|
// import 'package:flux/features/attachments/ui/operation_files_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();
|
|
final cubit = context.read<OperationsCubit>();
|
|
final currentLoggedStaff = GetIt.I
|
|
.get<SessionCubit>()
|
|
.state
|
|
.currentStaffMember!;
|
|
|
|
// 1. Diciamo al Cubit di prepararsi
|
|
cubit.initOperationForm(
|
|
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
|
|
void dispose() {
|
|
_referenceController.dispose();
|
|
_noteController.dispose();
|
|
_freeTextSubtypeController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _syncTextControllers(OperationModel model) {
|
|
if (_referenceController.text.isEmpty && model.reference.isNotEmpty) {
|
|
_referenceController.text = model.reference;
|
|
}
|
|
if (_noteController.text.isEmpty && model.note.isNotEmpty) {
|
|
_noteController.text = model.note;
|
|
}
|
|
if (_freeTextSubtypeController.text.isEmpty &&
|
|
model.subtype != null &&
|
|
model.subtype!.isNotEmpty) {
|
|
_freeTextSubtypeController.text = model.subtype!;
|
|
}
|
|
if (_freeTextDescriptionController.text.isEmpty &&
|
|
model.description != null &&
|
|
model.description!.isNotEmpty) {
|
|
_freeTextDescriptionController.text = model.description!;
|
|
}
|
|
_isInitialized = true;
|
|
}
|
|
|
|
void _saveOperation({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,
|
|
);
|
|
|
|
cubit.initOperationForm(existingOperation: operationToSave);
|
|
cubit.saveCurrentOperation(
|
|
targetStatus: OperationStatus.ok,
|
|
shouldPop: !keepAdding,
|
|
);
|
|
}
|
|
}
|
|
|
|
@override
|
|
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,
|
|
listener: (context, state) {
|
|
if (state.status == OperationsStatus.ready &&
|
|
state.currentOperation != null &&
|
|
!_isInitialized) {
|
|
_syncTextControllers(state.currentOperation!);
|
|
}
|
|
|
|
if (state.status == OperationsStatus.saved) {
|
|
Navigator.of(context).pop();
|
|
} else if (state.status == OperationsStatus.savedNoPop) {
|
|
context.read<OperationsCubit>().prepareNextOperationInBatch();
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(
|
|
content: Text('Servizio aggiunto! Inserisci il prossimo.'),
|
|
),
|
|
);
|
|
_freeTextSubtypeController.clear();
|
|
_freeTextDescriptionController.clear();
|
|
} else if (state.status == OperationsStatus.failure) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(state.errorMessage ?? 'Errore'),
|
|
backgroundColor: theme.colorScheme.error,
|
|
),
|
|
);
|
|
}
|
|
},
|
|
builder: (context, state) {
|
|
if (!_isInitialized &&
|
|
(widget.operationId != null || widget.existingOperation != null) &&
|
|
state.status == OperationsStatus.loading) {
|
|
return const Scaffold(
|
|
body: Center(child: CircularProgressIndicator()),
|
|
);
|
|
}
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(
|
|
state.currentOperation?.id == null
|
|
? 'Nuova Pratica'
|
|
: 'Modifica Pratica',
|
|
),
|
|
),
|
|
body: Form(
|
|
key: _formKey,
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
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,
|
|
showFiles: false,
|
|
),
|
|
),
|
|
),
|
|
VerticalDivider(width: 1, color: theme.dividerColor),
|
|
|
|
// 2. NOTE (30%)
|
|
Expanded(
|
|
flex: 3,
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: _buildNotesSection(isDesktop: true),
|
|
),
|
|
),
|
|
VerticalDivider(width: 1, color: theme.dividerColor),
|
|
|
|
// 3. FILE (30%)
|
|
Expanded(
|
|
flex: 3,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: OperationFilesSection(
|
|
currentOp: state.currentOperation!,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
} else if (isDesktop) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
flex: 7,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: _buildMainFormContent(theme, state),
|
|
),
|
|
),
|
|
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),
|
|
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: OutlinedButton(
|
|
onPressed: state.status == OperationsStatus.saving
|
|
? null
|
|
: () => _saveOperation(keepAdding: true),
|
|
child: const Text(
|
|
'Salva e Aggiungi Altro',
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
flex: 1,
|
|
child: ElevatedButton(
|
|
onPressed: state.status == OperationsStatus.saving
|
|
? null
|
|
: () => _saveOperation(keepAdding: false),
|
|
child: state.status == OperationsStatus.saving
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
),
|
|
)
|
|
: const Text('Salva ed Esci'),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildMainFormContent(
|
|
ThemeData theme,
|
|
OperationsState state, {
|
|
bool showFiles = true,
|
|
}) {
|
|
final currentOp = state.currentOperation;
|
|
final currentType = currentOp?.type ?? 'AL';
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
StaffSection(currentOp: currentOp),
|
|
const Divider(height: 50),
|
|
_buildSectionTitle('Cliente & Riferimento'),
|
|
CustomerSection(currentOp: currentOp),
|
|
const SizedBox(height: 16),
|
|
TextFormField(
|
|
controller: _referenceController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Riferimento (es. numero di telefono, targa...)',
|
|
prefixIcon: Icon(Icons.tag),
|
|
),
|
|
),
|
|
const Divider(height: 32),
|
|
|
|
_buildSectionTitle('Cosa stiamo facendo?'),
|
|
Wrap(
|
|
spacing: 8.0,
|
|
runSpacing: 8.0,
|
|
children: _availableTypes.map((type) {
|
|
return ChoiceChip(
|
|
label: Text(type),
|
|
selected: currentType == type,
|
|
onSelected: (selected) {
|
|
if (selected) {
|
|
context.read<OperationsCubit>().setTypeWithSmartDefault(type);
|
|
}
|
|
},
|
|
);
|
|
}).toList(),
|
|
),
|
|
const Divider(height: 32),
|
|
|
|
_buildSectionTitle('Dettagli Servizio'),
|
|
DetailsSection(
|
|
currentOp: currentOp,
|
|
currentType: currentType,
|
|
freeTextSubtypeController: _freeTextSubtypeController,
|
|
freeTextDescriptionController: _freeTextDescriptionController,
|
|
durationQuickPicks: _buildDurationQuickPicks(currentOp),
|
|
),
|
|
|
|
// QUANTITÀ
|
|
Row(
|
|
children: [
|
|
const Text('Quantità: '),
|
|
IconButton(
|
|
icon: const Icon(Icons.remove),
|
|
onPressed: () {
|
|
final q = currentOp?.quantity ?? 1;
|
|
if (q > 1) {
|
|
context.read<OperationsCubit>().updateOperationFields(
|
|
quantity: q - 1,
|
|
);
|
|
}
|
|
},
|
|
),
|
|
Text(
|
|
'${currentOp?.quantity ?? 1}',
|
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.add),
|
|
onPressed: () {
|
|
final q = currentOp?.quantity ?? 1;
|
|
context.read<OperationsCubit>().updateOperationFields(
|
|
quantity: q + 1,
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
const Divider(height: 32),
|
|
|
|
if (showFiles) ...[OperationFilesSection(currentOp: currentOp!)],
|
|
],
|
|
);
|
|
}
|
|
|
|
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<OperationsCubit>().updateOperationFields(
|
|
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),
|
|
),
|
|
);
|
|
}
|
|
}
|