2026-05-04 15:36:42 +02:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
2026-05-07 16:28:01 +02:00
|
|
|
import 'package:flux/features/attachments/blocs/attachments_bloc.dart';
|
2026-05-08 12:28:14 +02:00
|
|
|
import 'package:flux/features/operations/blocs/operation_form_cubit.dart';
|
2026-05-04 15:36:42 +02:00
|
|
|
import 'package:flux/features/operations/models/operation_model.dart';
|
2026-05-07 16:28:01 +02:00
|
|
|
import 'package:flux/core/widgets/shared_forms/customer_section.dart';
|
2026-05-04 15:36:42 +02:00
|
|
|
import 'package:flux/features/operations/ui/widgets/details_section.dart';
|
2026-05-19 10:32:01 +02:00
|
|
|
import 'package:flux/core/widgets/shared_forms/shared_files_section.dart'; // <- Cambiato ad un file unico per coerenza col ticket
|
2026-05-04 15:36:42 +02:00
|
|
|
|
|
|
|
|
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',
|
2026-05-19 10:32:01 +02:00
|
|
|
'FWA',
|
2026-05-04 15:36:42 +02:00
|
|
|
'TELEPASS',
|
|
|
|
|
'Energy',
|
|
|
|
|
'Fin',
|
|
|
|
|
'Entertainment',
|
|
|
|
|
'Custom',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
bool _isInitialized = false;
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
2026-05-19 10:32:01 +02:00
|
|
|
// 1. Lanciamo l'inizializzazione sincrona/asincrona
|
2026-05-08 12:28:14 +02:00
|
|
|
context.read<OperationFormCubit>().initForm(
|
2026-05-04 15:36:42 +02:00
|
|
|
existingOperation: widget.existingOperation,
|
|
|
|
|
operationId: widget.operationId,
|
|
|
|
|
);
|
2026-05-19 10:32:01 +02:00
|
|
|
|
|
|
|
|
// 2. Lettura immediata dello stato (come fatto per il customer!)
|
|
|
|
|
final currentState = context.read<OperationFormCubit>().state;
|
|
|
|
|
if (currentState.status == OperationFormStatus.ready && !_isInitialized) {
|
|
|
|
|
_syncTextControllers(currentState.operation);
|
|
|
|
|
}
|
2026-05-04 15:36:42 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_referenceController.dispose();
|
|
|
|
|
_noteController.dispose();
|
|
|
|
|
_freeTextSubtypeController.dispose();
|
2026-05-08 12:28:14 +02:00
|
|
|
_freeTextDescriptionController.dispose();
|
2026-05-04 15:36:42 +02:00
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void _syncTextControllers(OperationModel model) {
|
2026-05-08 12:28:14 +02:00
|
|
|
if (_referenceController.text.isEmpty) {
|
2026-05-04 15:36:42 +02:00
|
|
|
_referenceController.text = model.reference;
|
|
|
|
|
}
|
2026-05-08 12:28:14 +02:00
|
|
|
if (_noteController.text.isEmpty) {
|
2026-05-04 15:36:42 +02:00
|
|
|
_noteController.text = model.note;
|
|
|
|
|
}
|
2026-05-08 12:28:14 +02:00
|
|
|
if (_freeTextSubtypeController.text.isEmpty) {
|
|
|
|
|
_freeTextSubtypeController.text = model.subtype ?? '';
|
2026-05-04 15:36:42 +02:00
|
|
|
}
|
2026-05-08 12:28:14 +02:00
|
|
|
if (_freeTextDescriptionController.text.isEmpty) {
|
|
|
|
|
_freeTextDescriptionController.text = model.description ?? '';
|
2026-05-04 15:36:42 +02:00
|
|
|
}
|
|
|
|
|
_isInitialized = true;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 12:28:14 +02:00
|
|
|
void _flushControllersToCubit() {
|
|
|
|
|
context.read<OperationFormCubit>().updateFields(
|
|
|
|
|
reference: _referenceController.text,
|
|
|
|
|
note: _noteController.text,
|
|
|
|
|
subtype: _freeTextSubtypeController.text,
|
|
|
|
|
description: _freeTextDescriptionController.text,
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-04 15:36:42 +02:00
|
|
|
|
2026-05-08 12:28:14 +02:00
|
|
|
void _saveOperation({
|
|
|
|
|
required OperationStatus targetStatus,
|
|
|
|
|
required bool keepAdding,
|
|
|
|
|
}) {
|
|
|
|
|
if (_formKey.currentState!.validate()) {
|
|
|
|
|
_flushControllersToCubit();
|
2026-05-19 10:32:01 +02:00
|
|
|
// Aggiorniamo prima lo stato bersaglio nel cubit
|
|
|
|
|
context.read<OperationFormCubit>().updateFields(status: targetStatus);
|
|
|
|
|
// Poi chiamiamo il salvataggio
|
2026-05-08 12:28:14 +02:00
|
|
|
context.read<OperationFormCubit>().saveOperation(
|
|
|
|
|
targetStatus: targetStatus,
|
|
|
|
|
keepAdding: keepAdding,
|
2026-05-04 15:36:42 +02:00
|
|
|
);
|
2026-05-08 12:28:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-04 15:36:42 +02:00
|
|
|
|
2026-05-08 12:28:14 +02:00
|
|
|
Future<String?> _generateIdForQr() async {
|
|
|
|
|
if (!_formKey.currentState!.validate()) return null;
|
|
|
|
|
_flushControllersToCubit();
|
|
|
|
|
final attachmentsBloc = context.read<AttachmentsBloc>();
|
2026-05-19 10:32:01 +02:00
|
|
|
|
|
|
|
|
// Assicurati che questo metodo esista nel Cubit (come per il Ticket)
|
2026-05-08 12:28:14 +02:00
|
|
|
final newId = await context.read<OperationFormCubit>().saveOperationDraft();
|
|
|
|
|
if (newId != null && context.mounted) {
|
|
|
|
|
attachmentsBloc.add(ParentEntitySavedEvent(newId));
|
|
|
|
|
}
|
|
|
|
|
return newId;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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;
|
2026-05-04 15:36:42 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
final theme = Theme.of(context);
|
|
|
|
|
|
2026-05-08 12:28:14 +02:00
|
|
|
return BlocConsumer<OperationFormCubit, OperationFormState>(
|
|
|
|
|
listenWhen: (previous, current) => previous.status != current.status,
|
2026-05-04 15:36:42 +02:00
|
|
|
listener: (context, state) {
|
2026-05-08 12:28:14 +02:00
|
|
|
if (state.status == OperationFormStatus.ready && !_isInitialized) {
|
|
|
|
|
_syncTextControllers(state.operation);
|
2026-05-04 15:36:42 +02:00
|
|
|
}
|
2026-05-08 12:28:14 +02:00
|
|
|
if (state.status == OperationFormStatus.success) {
|
2026-05-04 15:36:42 +02:00
|
|
|
Navigator.of(context).pop();
|
2026-05-08 12:28:14 +02:00
|
|
|
} else if (state.status == OperationFormStatus.successAndAddAnother) {
|
2026-05-04 15:36:42 +02:00
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
|
const SnackBar(
|
2026-05-08 12:28:14 +02:00
|
|
|
content: Text('Operazione salvata! Inserisci la prossima'),
|
2026-05-04 15:36:42 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
_freeTextSubtypeController.clear();
|
|
|
|
|
_freeTextDescriptionController.clear();
|
2026-05-19 10:32:01 +02:00
|
|
|
_referenceController.clear();
|
2026-05-08 12:28:14 +02:00
|
|
|
} else if (state.status == OperationFormStatus.failure) {
|
2026-05-04 15:36:42 +02:00
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
|
SnackBar(
|
2026-05-08 12:28:14 +02:00
|
|
|
content: Text(state.errorMessage ?? 'Errore di salvataggio'),
|
|
|
|
|
backgroundColor: Colors.red,
|
2026-05-04 15:36:42 +02:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
builder: (context, state) {
|
|
|
|
|
if (!_isInitialized &&
|
|
|
|
|
(widget.operationId != null || widget.existingOperation != null) &&
|
2026-05-08 12:28:14 +02:00
|
|
|
state.status == OperationFormStatus.loading) {
|
2026-05-04 15:36:42 +02:00
|
|
|
return const Scaffold(
|
|
|
|
|
body: Center(child: CircularProgressIndicator()),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-08 12:28:14 +02:00
|
|
|
final displayStatus =
|
|
|
|
|
state.operation.status == OperationStatus.draft &&
|
|
|
|
|
state.operation.id == null
|
|
|
|
|
? OperationStatus.success
|
|
|
|
|
: state.operation.status;
|
|
|
|
|
|
2026-05-04 15:36:42 +02:00
|
|
|
return Scaffold(
|
|
|
|
|
appBar: AppBar(
|
|
|
|
|
title: Text(
|
2026-05-13 15:41:35 +02:00
|
|
|
state.operation.id == null
|
|
|
|
|
? 'Nuova Pratica - Operatore: ${state.operation.staffDisplayName}'
|
|
|
|
|
: 'Modifica Pratica - Operatore: ${state.operation.staffDisplayName}',
|
2026-05-04 15:36:42 +02:00
|
|
|
),
|
2026-05-08 12:28:14 +02:00
|
|
|
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,
|
2026-05-04 15:36:42 +02:00
|
|
|
),
|
|
|
|
|
body: Form(
|
|
|
|
|
key: _formKey,
|
2026-05-19 10:32:01 +02:00
|
|
|
child: FocusTraversalGroup(
|
|
|
|
|
policy: WidgetOrderTraversalPolicy(),
|
|
|
|
|
child: LayoutBuilder(
|
|
|
|
|
builder: (context, constraints) {
|
|
|
|
|
final isUltraWide = constraints.maxWidth > 1400;
|
|
|
|
|
final isDesktop = constraints.maxWidth > 900;
|
|
|
|
|
|
|
|
|
|
return SingleChildScrollView(
|
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
|
|
|
|
child: Center(
|
|
|
|
|
child: ConstrainedBox(
|
|
|
|
|
constraints: BoxConstraints(
|
|
|
|
|
maxWidth: isUltraWide
|
|
|
|
|
? 1600
|
|
|
|
|
: (isDesktop ? 1200 : 800),
|
2026-05-04 15:36:42 +02:00
|
|
|
),
|
2026-05-19 10:32:01 +02:00
|
|
|
child: _buildResponsiveLayout(
|
|
|
|
|
isUltraWide,
|
|
|
|
|
isDesktop,
|
|
|
|
|
state,
|
|
|
|
|
displayStatus,
|
2026-05-04 15:36:42 +02:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-05-19 10:32:01 +02:00
|
|
|
},
|
|
|
|
|
),
|
2026-05-04 15:36:42 +02:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
bottomNavigationBar: SafeArea(
|
2026-05-19 10:32:01 +02:00
|
|
|
child: Container(
|
2026-05-04 15:36:42 +02:00
|
|
|
padding: const EdgeInsets.all(16.0),
|
2026-05-19 10:32:01 +02:00
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: theme.scaffoldBackgroundColor,
|
|
|
|
|
boxShadow: [
|
|
|
|
|
BoxShadow(
|
|
|
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
|
|
|
blurRadius: 10,
|
|
|
|
|
offset: const Offset(0, -3),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-05-04 15:36:42 +02:00
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
flex: 1,
|
|
|
|
|
child: ElevatedButton(
|
2026-05-08 12:28:14 +02:00
|
|
|
style: ElevatedButton.styleFrom(
|
|
|
|
|
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
|
2026-05-04 15:36:42 +02:00
|
|
|
? null
|
2026-05-08 12:28:14 +02:00
|
|
|
: () => _saveOperation(
|
|
|
|
|
keepAdding: false,
|
2026-05-19 10:32:01 +02:00
|
|
|
targetStatus: displayStatus,
|
2026-05-08 12:28:14 +02:00
|
|
|
),
|
|
|
|
|
child: state.status == OperationFormStatus.saving
|
2026-05-04 15:36:42 +02:00
|
|
|
? const SizedBox(
|
|
|
|
|
width: 20,
|
|
|
|
|
height: 20,
|
|
|
|
|
child: CircularProgressIndicator(
|
|
|
|
|
color: Colors.white,
|
|
|
|
|
strokeWidth: 2,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
: const Text('Salva ed Esci'),
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-05-12 11:14:48 +02:00
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Expanded(
|
|
|
|
|
flex: 1,
|
|
|
|
|
child: OutlinedButton(
|
|
|
|
|
onPressed: state.status == OperationFormStatus.saving
|
|
|
|
|
? null
|
|
|
|
|
: () => _saveOperation(
|
|
|
|
|
keepAdding: true,
|
2026-05-19 10:32:01 +02:00
|
|
|
targetStatus: displayStatus,
|
2026-05-12 11:14:48 +02:00
|
|
|
),
|
|
|
|
|
child: const Text(
|
|
|
|
|
'Salva e Aggiungi Altro',
|
|
|
|
|
textAlign: TextAlign.center,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-05-04 15:36:42 +02:00
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 10:32:01 +02:00
|
|
|
// --- LOGICA DI IMPAGINAZIONE RESPONSIVE ---
|
|
|
|
|
Widget _buildResponsiveLayout(
|
|
|
|
|
bool isUltraWide,
|
|
|
|
|
bool isDesktop,
|
|
|
|
|
OperationFormState state,
|
|
|
|
|
OperationStatus displayStatus,
|
|
|
|
|
) {
|
|
|
|
|
if (isUltraWide) {
|
|
|
|
|
// 3 COLONNE
|
2026-05-19 10:41:13 +02:00
|
|
|
return Column(
|
2026-05-19 10:32:01 +02:00
|
|
|
children: [
|
2026-05-19 10:41:13 +02:00
|
|
|
Row(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [_cardAnagrafica(state), _cardEsito(state)],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 24),
|
|
|
|
|
Expanded(child: Column(children: [_cardDettagli(state)])),
|
|
|
|
|
const SizedBox(width: 24),
|
|
|
|
|
Expanded(child: Column(children: [_cardNote(state)])),
|
|
|
|
|
],
|
2026-05-19 10:32:01 +02:00
|
|
|
),
|
2026-05-19 10:41:13 +02:00
|
|
|
_cardAllegati(state),
|
2026-05-19 10:32:01 +02:00
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
} else if (isDesktop) {
|
|
|
|
|
// 2 COLONNE
|
|
|
|
|
return Row(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Expanded(
|
2026-05-10 14:09:57 +02:00
|
|
|
child: Column(
|
|
|
|
|
children: [
|
2026-05-19 10:32:01 +02:00
|
|
|
_cardAnagrafica(state),
|
|
|
|
|
_cardEsito(state),
|
|
|
|
|
_cardAllegati(state),
|
2026-05-10 14:09:57 +02:00
|
|
|
],
|
2026-05-07 16:28:01 +02:00
|
|
|
),
|
2026-05-10 14:09:57 +02:00
|
|
|
),
|
2026-05-19 10:32:01 +02:00
|
|
|
const SizedBox(width: 24),
|
|
|
|
|
Expanded(
|
|
|
|
|
child: Column(children: [_cardDettagli(state), _cardNote(state)]),
|
2026-05-10 14:09:57 +02:00
|
|
|
),
|
2026-05-19 10:32:01 +02:00
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
// 1 COLONNA (Mobile)
|
|
|
|
|
return Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
|
|
|
|
_cardAnagrafica(state),
|
|
|
|
|
_cardEsito(state),
|
|
|
|
|
_cardDettagli(state),
|
|
|
|
|
_cardNote(state),
|
|
|
|
|
_cardAllegati(state),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- LE CARD MODULARIZZATE E COLORATE ---
|
|
|
|
|
|
|
|
|
|
Widget _cardAnagrafica(OperationFormState state) {
|
|
|
|
|
return _buildCard(
|
|
|
|
|
title: 'Cliente e Riferimento',
|
|
|
|
|
icon: Icons.person,
|
|
|
|
|
themeColor: Colors.indigo,
|
|
|
|
|
children: [
|
|
|
|
|
SharedCustomerSection(
|
|
|
|
|
customer: state.operation.customer,
|
|
|
|
|
onCustomerSelected: (customer) => context
|
|
|
|
|
.read<OperationFormCubit>()
|
|
|
|
|
.updateFields(customer: customer),
|
2026-05-10 14:09:57 +02:00
|
|
|
),
|
2026-05-19 10:32:01 +02:00
|
|
|
const SizedBox(height: 16),
|
|
|
|
|
TextFormField(
|
|
|
|
|
controller: _referenceController,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
labelText: 'Riferimento (es. Telefono, Targa...)',
|
|
|
|
|
prefixIcon: Icon(Icons.tag),
|
2026-05-10 14:09:57 +02:00
|
|
|
),
|
2026-05-19 10:32:01 +02:00
|
|
|
validator: (v) =>
|
|
|
|
|
v == null || v.isEmpty ? 'Inserisci un riferimento' : null,
|
2026-05-10 14:09:57 +02:00
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 10:32:01 +02:00
|
|
|
Widget _cardEsito(OperationFormState state) {
|
|
|
|
|
return _buildCard(
|
|
|
|
|
title: 'Esito Pratica',
|
|
|
|
|
icon: Icons.fact_check,
|
|
|
|
|
themeColor: _getStatusColor(state.operation.status),
|
2026-05-10 14:09:57 +02:00
|
|
|
children: [
|
2026-05-08 12:28:14 +02:00
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
|
|
|
|
decoration: BoxDecoration(
|
2026-05-10 14:09:57 +02:00
|
|
|
color: _getStatusColor(
|
|
|
|
|
state.operation.status,
|
|
|
|
|
).withValues(alpha: 0.1),
|
2026-05-08 12:28:14 +02:00
|
|
|
border: Border.all(
|
2026-05-10 14:09:57 +02:00
|
|
|
color: _getStatusColor(
|
|
|
|
|
state.operation.status,
|
|
|
|
|
).withValues(alpha: 0.3),
|
2026-05-08 12:28:14 +02:00
|
|
|
),
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
),
|
|
|
|
|
child: DropdownButtonHideUnderline(
|
|
|
|
|
child: DropdownButton<OperationStatus>(
|
|
|
|
|
isExpanded: true,
|
2026-05-10 14:09:57 +02:00
|
|
|
value: state.operation.status,
|
2026-05-08 12:28:14 +02:00
|
|
|
icon: Icon(
|
|
|
|
|
Icons.arrow_drop_down,
|
2026-05-10 14:09:57 +02:00
|
|
|
color: _getStatusColor(state.operation.status),
|
2026-05-08 12:28:14 +02:00
|
|
|
),
|
2026-05-19 10:32:01 +02:00
|
|
|
items: OperationStatus.values.map((status) {
|
|
|
|
|
return DropdownMenuItem(
|
|
|
|
|
value: status,
|
|
|
|
|
child: Row(
|
|
|
|
|
children: [
|
|
|
|
|
Icon(
|
|
|
|
|
status == OperationStatus.success
|
|
|
|
|
? Icons.check_circle
|
|
|
|
|
: Icons.error_outline,
|
|
|
|
|
color: _getStatusColor(status),
|
|
|
|
|
size: 20,
|
2026-05-08 12:28:14 +02:00
|
|
|
),
|
2026-05-19 10:32:01 +02:00
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Text(
|
|
|
|
|
status.displayName,
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontWeight: FontWeight.w600,
|
|
|
|
|
color: _getStatusColor(status),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
2026-05-08 12:28:14 +02:00
|
|
|
onChanged: (newStatus) {
|
2026-05-19 10:32:01 +02:00
|
|
|
if (newStatus != null)
|
2026-05-08 12:28:14 +02:00
|
|
|
context.read<OperationFormCubit>().updateFields(
|
|
|
|
|
status: newStatus,
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
|
Text(
|
2026-05-10 14:09:57 +02:00
|
|
|
state.operation.status == OperationStatus.success
|
2026-05-19 10:32:01 +02:00
|
|
|
? 'Lascia OK se caricata con successo.'
|
|
|
|
|
: 'Attenzione: pratica salvata in stato anomalo.',
|
2026-05-08 12:28:14 +02:00
|
|
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
|
|
|
|
),
|
2026-05-10 14:09:57 +02:00
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-08 12:28:14 +02:00
|
|
|
|
2026-05-19 10:32:01 +02:00
|
|
|
Widget _cardDettagli(OperationFormState state) {
|
|
|
|
|
return _buildCard(
|
|
|
|
|
title: 'Dettagli Servizio',
|
|
|
|
|
icon: Icons.design_services,
|
|
|
|
|
themeColor: Colors.deepOrange,
|
2026-05-10 14:09:57 +02:00
|
|
|
children: [
|
2026-05-04 15:36:42 +02:00
|
|
|
Wrap(
|
|
|
|
|
spacing: 8.0,
|
|
|
|
|
runSpacing: 8.0,
|
|
|
|
|
children: _availableTypes.map((type) {
|
|
|
|
|
return ChoiceChip(
|
|
|
|
|
label: Text(type),
|
2026-05-10 14:09:57 +02:00
|
|
|
selected: state.operation.type == type,
|
2026-05-04 15:36:42 +02:00
|
|
|
onSelected: (selected) {
|
2026-05-19 10:32:01 +02:00
|
|
|
if (selected)
|
2026-05-08 12:28:14 +02:00
|
|
|
context.read<OperationFormCubit>().setTypeWithSmartDefault(
|
|
|
|
|
type,
|
|
|
|
|
);
|
2026-05-04 15:36:42 +02:00
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
2026-05-19 10:32:01 +02:00
|
|
|
const Divider(height: 32),
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'Quantità:',
|
|
|
|
|
style: TextStyle(fontWeight: FontWeight.bold),
|
|
|
|
|
),
|
|
|
|
|
const Spacer(),
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.remove_circle_outline),
|
|
|
|
|
onPressed: state.operation.quantity > 1
|
|
|
|
|
? () => context.read<OperationFormCubit>().updateFields(
|
|
|
|
|
quantity: state.operation.quantity - 1,
|
|
|
|
|
)
|
|
|
|
|
: null,
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
'${state.operation.quantity}',
|
|
|
|
|
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
|
|
|
),
|
|
|
|
|
IconButton(
|
|
|
|
|
icon: const Icon(Icons.add_circle_outline),
|
|
|
|
|
onPressed: () => context.read<OperationFormCubit>().updateFields(
|
|
|
|
|
quantity: state.operation.quantity + 1,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const Divider(height: 32),
|
2026-05-15 10:12:05 +02:00
|
|
|
OperationDetailsSection(
|
2026-05-10 14:09:57 +02:00
|
|
|
currentOp: state.operation,
|
|
|
|
|
currentType: state.operation.type,
|
2026-05-04 15:36:42 +02:00
|
|
|
freeTextSubtypeController: _freeTextSubtypeController,
|
|
|
|
|
freeTextDescriptionController: _freeTextDescriptionController,
|
2026-05-19 10:32:01 +02:00
|
|
|
durationQuickPicks: _buildDurationQuickPicks(),
|
2026-05-04 15:36:42 +02:00
|
|
|
),
|
2026-05-10 14:09:57 +02:00
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-05-04 15:36:42 +02:00
|
|
|
|
2026-05-19 10:32:01 +02:00
|
|
|
Widget _cardNote(OperationFormState state) {
|
|
|
|
|
return _buildCard(
|
|
|
|
|
title: 'Note Interne',
|
|
|
|
|
icon: Icons.notes,
|
|
|
|
|
themeColor: Colors.teal,
|
2026-05-10 14:09:57 +02:00
|
|
|
children: [
|
2026-05-19 10:32:01 +02:00
|
|
|
TextFormField(
|
|
|
|
|
controller: _noteController,
|
2026-05-19 10:41:13 +02:00
|
|
|
minLines: 14,
|
|
|
|
|
maxLines: 500,
|
2026-05-19 10:32:01 +02:00
|
|
|
decoration: InputDecoration(
|
|
|
|
|
hintText: 'Incolla seriali, ICCID, IBAN...',
|
|
|
|
|
alignLabelWithHint: true,
|
|
|
|
|
fillColor: Colors.teal.withValues(alpha: 0.05),
|
|
|
|
|
filled: true,
|
|
|
|
|
border: OutlineInputBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(12),
|
|
|
|
|
borderSide: BorderSide.none,
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-05-04 15:36:42 +02:00
|
|
|
),
|
2026-05-10 14:09:57 +02:00
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 10:32:01 +02:00
|
|
|
Widget _cardAllegati(OperationFormState state) {
|
|
|
|
|
return _buildCard(
|
|
|
|
|
title: 'Allegati e Documenti',
|
|
|
|
|
icon: Icons.attach_file,
|
|
|
|
|
themeColor: Colors.deepPurple,
|
|
|
|
|
children: [
|
|
|
|
|
SharedFilesSection(
|
|
|
|
|
titleNameForUpload: state.operation.customer?.name ?? 'Nuova Pratica',
|
|
|
|
|
onGenerateIdForQr: _generateIdForQr,
|
|
|
|
|
),
|
|
|
|
|
],
|
2026-05-10 14:09:57 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 10:32:01 +02:00
|
|
|
// --- WIDGET BASE PER LA CARD ---
|
|
|
|
|
Widget _buildCard({
|
|
|
|
|
required String title,
|
|
|
|
|
required IconData icon,
|
|
|
|
|
required Color themeColor,
|
|
|
|
|
required List<Widget> children,
|
2026-05-10 14:09:57 +02:00
|
|
|
}) {
|
2026-05-19 10:32:01 +02:00
|
|
|
return Card(
|
|
|
|
|
margin: const EdgeInsets.only(bottom: 24),
|
|
|
|
|
elevation: 0,
|
|
|
|
|
shape: RoundedRectangleBorder(
|
|
|
|
|
borderRadius: BorderRadius.circular(16),
|
|
|
|
|
side: BorderSide(color: themeColor.withValues(alpha: 0.3), width: 1),
|
|
|
|
|
),
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.all(20.0),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
Row(
|
|
|
|
|
children: [
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(8),
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: themeColor.withValues(alpha: 0.1),
|
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
|
|
|
|
),
|
|
|
|
|
child: Icon(icon, color: themeColor),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(width: 12),
|
|
|
|
|
Text(
|
|
|
|
|
title,
|
|
|
|
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
|
|
|
|
fontWeight: FontWeight.bold,
|
|
|
|
|
color: themeColor,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
const Divider(height: 32),
|
|
|
|
|
...children,
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-05-04 15:36:42 +02:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-19 10:32:01 +02:00
|
|
|
Widget _buildDurationQuickPicks() {
|
2026-05-04 15:36:42 +02:00
|
|
|
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();
|
2026-05-08 12:28:14 +02:00
|
|
|
context.read<OperationFormCubit>().updateFields(
|
2026-05-04 15:36:42 +02:00
|
|
|
expirationDate: DateTime(
|
|
|
|
|
now.year,
|
|
|
|
|
now.month + months,
|
|
|
|
|
now.day,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|