392 lines
14 KiB
Dart
392 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|||
|
|
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||
|
|
import 'package:flutter_mobx/flutter_mobx.dart';
|
||
|
|
import 'package:form_builder_validators/form_builder_validators.dart';
|
||
|
|
import '../../../../core/theme/app_colors.dart';
|
||
|
|
import '../../../../core/widgets/medical_modal.dart';
|
||
|
|
import '../../../dashboard/data/models/doctor_info_model.dart';
|
||
|
|
import '../store/config_store.dart';
|
||
|
|
|
||
|
|
class ConfigModalView extends StatefulWidget {
|
||
|
|
final ValueChanged<DoctorInfoModel>? onSaved;
|
||
|
|
final ConfigStore? store;
|
||
|
|
|
||
|
|
const ConfigModalView({
|
||
|
|
super.key,
|
||
|
|
this.onSaved,
|
||
|
|
this.store,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<ConfigModalView> createState() => _ConfigModalViewState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _ConfigModalViewState extends State<ConfigModalView> {
|
||
|
|
late final ConfigStore _store;
|
||
|
|
final _formKey = GlobalKey<FormBuilderState>();
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_store = widget.store ?? ConfigStore();
|
||
|
|
if (widget.store == null) {
|
||
|
|
_store.loadConfig();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<void> _handleSave() async {
|
||
|
|
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||
|
|
final values = _formKey.currentState!.value;
|
||
|
|
if (values['nombre_doctor'] != null) {
|
||
|
|
_store.nombreDoctor = values['nombre_doctor'].toString();
|
||
|
|
}
|
||
|
|
if (values['especialidad'] != null) {
|
||
|
|
_store.especialidad = values['especialidad'].toString();
|
||
|
|
}
|
||
|
|
if (values['descrip_especialidad'] != null) {
|
||
|
|
_store.descripEspecialidad = values['descrip_especialidad'].toString();
|
||
|
|
}
|
||
|
|
if (values['descrip_general'] != null) {
|
||
|
|
_store.descripGeneral = values['descrip_general'].toString();
|
||
|
|
}
|
||
|
|
|
||
|
|
final updated = await _store.saveConfig();
|
||
|
|
if (updated != null && mounted) {
|
||
|
|
widget.onSaved?.call(updated);
|
||
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
||
|
|
const SnackBar(
|
||
|
|
content: Row(
|
||
|
|
children: [
|
||
|
|
Icon(Icons.check_circle_rounded, color: Colors.white, size: 18),
|
||
|
|
SizedBox(width: 8),
|
||
|
|
Expanded(
|
||
|
|
child: Text('Configuración del consultorio guardada correctamente'),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
backgroundColor: Color(0xFF00A65A),
|
||
|
|
behavior: SnackBarBehavior.floating,
|
||
|
|
duration: Duration(seconds: 3),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
Navigator.of(context).pop();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
InputDecoration _cleanDecoration({
|
||
|
|
required String label,
|
||
|
|
IconData? icon,
|
||
|
|
bool isRequired = false,
|
||
|
|
String? hintText,
|
||
|
|
}) {
|
||
|
|
return InputDecoration(
|
||
|
|
labelText: isRequired ? '$label *' : label,
|
||
|
|
hintText: hintText,
|
||
|
|
filled: true,
|
||
|
|
fillColor: Colors.white,
|
||
|
|
isDense: true,
|
||
|
|
contentPadding: const EdgeInsets.symmetric(horizontal: 14, vertical: 13),
|
||
|
|
prefixIcon: icon != null
|
||
|
|
? Icon(icon, size: 18, color: isRequired ? AppColors.primary : AppColors.textMuted)
|
||
|
|
: null,
|
||
|
|
prefixIconConstraints: const BoxConstraints(minWidth: 38, minHeight: 38),
|
||
|
|
border: OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(10),
|
||
|
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||
|
|
),
|
||
|
|
enabledBorder: OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(10),
|
||
|
|
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||
|
|
),
|
||
|
|
focusedBorder: OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(10),
|
||
|
|
borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
|
||
|
|
),
|
||
|
|
errorBorder: OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(10),
|
||
|
|
borderSide: const BorderSide(color: AppColors.error),
|
||
|
|
),
|
||
|
|
focusedErrorBorder: OutlineInputBorder(
|
||
|
|
borderRadius: BorderRadius.circular(10),
|
||
|
|
borderSide: const BorderSide(color: AppColors.error, width: 1.5),
|
||
|
|
),
|
||
|
|
labelStyle: TextStyle(
|
||
|
|
fontSize: 13,
|
||
|
|
fontWeight: FontWeight.w600,
|
||
|
|
color: isRequired ? AppColors.primary : AppColors.textSecondary,
|
||
|
|
),
|
||
|
|
hintStyle: const TextStyle(fontSize: 12.5, color: Color(0xFF94A3B8)),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return Observer(
|
||
|
|
builder: (_) {
|
||
|
|
return MedicalModalDialog(
|
||
|
|
titleIcon: Container(
|
||
|
|
padding: const EdgeInsets.all(8),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: AppColors.primaryContainer,
|
||
|
|
borderRadius: BorderRadius.circular(10),
|
||
|
|
),
|
||
|
|
child: const Icon(Icons.settings_suggest_rounded, color: AppColors.primary, size: 22),
|
||
|
|
),
|
||
|
|
title: 'Configuración del Consultorio',
|
||
|
|
subtitle: const Text(
|
||
|
|
'Actualice la información del doctor, especialidad y datos de contacto de la clínica',
|
||
|
|
style: TextStyle(fontSize: 12, color: AppColors.textMuted),
|
||
|
|
),
|
||
|
|
maxWidth: 780,
|
||
|
|
maxHeight: 660,
|
||
|
|
bottomBar: _buildBottomBar(),
|
||
|
|
child: _store.isLoading
|
||
|
|
? const Center(
|
||
|
|
child: Padding(
|
||
|
|
padding: EdgeInsets.all(40),
|
||
|
|
child: CircularProgressIndicator(),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
: _buildForm(),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildBottomBar() {
|
||
|
|
return Container(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||
|
|
decoration: const BoxDecoration(
|
||
|
|
color: Color(0xFFF8FAFC),
|
||
|
|
border: Border(top: BorderSide(color: Color(0xFFE2E8F0))),
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
children: [
|
||
|
|
Container(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white,
|
||
|
|
borderRadius: BorderRadius.circular(6),
|
||
|
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||
|
|
),
|
||
|
|
child: const Row(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
Icon(Icons.keyboard_outlined, size: 12, color: AppColors.textMuted),
|
||
|
|
SizedBox(width: 4),
|
||
|
|
Text(
|
||
|
|
'ESC para salir',
|
||
|
|
style: TextStyle(fontSize: 11, color: AppColors.textMuted, fontWeight: FontWeight.w500),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const Spacer(),
|
||
|
|
TextButton(
|
||
|
|
onPressed: _store.isSaving ? null : () => Navigator.of(context).pop(),
|
||
|
|
style: TextButton.styleFrom(
|
||
|
|
foregroundColor: AppColors.textSecondary,
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||
|
|
),
|
||
|
|
child: const Text('Cancelar', style: TextStyle(fontWeight: FontWeight.w600)),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 12),
|
||
|
|
ElevatedButton(
|
||
|
|
onPressed: _store.isSaving ? null : _handleSave,
|
||
|
|
style: ElevatedButton.styleFrom(
|
||
|
|
backgroundColor: const Color(0xFF00A65A),
|
||
|
|
foregroundColor: Colors.white,
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 12),
|
||
|
|
elevation: 1,
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||
|
|
),
|
||
|
|
child: _store.isSaving
|
||
|
|
? const SizedBox(
|
||
|
|
width: 18,
|
||
|
|
height: 18,
|
||
|
|
child: CircularProgressIndicator(
|
||
|
|
strokeWidth: 2,
|
||
|
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
: const Row(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
Icon(Icons.check_circle_outline_rounded, size: 18),
|
||
|
|
SizedBox(width: 8),
|
||
|
|
Text(
|
||
|
|
'Guardar Configuración',
|
||
|
|
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildForm() {
|
||
|
|
return SingleChildScrollView(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||
|
|
child: FormBuilder(
|
||
|
|
key: _formKey,
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
if (_store.errorMessage != null) ...[
|
||
|
|
Container(
|
||
|
|
padding: const EdgeInsets.all(12),
|
||
|
|
margin: const EdgeInsets.only(bottom: 16),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: AppColors.error.withValues(alpha: 0.1),
|
||
|
|
borderRadius: BorderRadius.circular(8),
|
||
|
|
border: Border.all(color: AppColors.error.withValues(alpha: 0.3)),
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
children: [
|
||
|
|
const Icon(Icons.error_outline_rounded, color: AppColors.error, size: 20),
|
||
|
|
const SizedBox(width: 10),
|
||
|
|
Expanded(
|
||
|
|
child: Text(
|
||
|
|
_store.errorMessage!,
|
||
|
|
style: const TextStyle(fontSize: 12.5, color: AppColors.error, fontWeight: FontWeight.w500),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
|
||
|
|
// Sección 1: Identidad Médica
|
||
|
|
_buildSectionHeader(
|
||
|
|
icon: Icons.person_pin_rounded,
|
||
|
|
title: 'Identidad del Profesional',
|
||
|
|
subtitle: 'Nombre y especialidad principal reflejados en el encabezado y recetas',
|
||
|
|
),
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
Row(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Expanded(
|
||
|
|
flex: 6,
|
||
|
|
child: FormBuilderTextField(
|
||
|
|
name: 'nombre_doctor',
|
||
|
|
initialValue: _store.nombreDoctor,
|
||
|
|
decoration: _cleanDecoration(
|
||
|
|
label: 'Nombre del Doctor',
|
||
|
|
icon: Icons.badge_outlined,
|
||
|
|
isRequired: true,
|
||
|
|
hintText: 'Ej. Dr. Luis A. Castro Sosa',
|
||
|
|
),
|
||
|
|
validator: FormBuilderValidators.compose([
|
||
|
|
FormBuilderValidators.required(errorText: 'Requerido'),
|
||
|
|
FormBuilderValidators.minLength(3, errorText: 'Mínimo 3 caracteres'),
|
||
|
|
]),
|
||
|
|
onChanged: (v) => _store.nombreDoctor = v ?? '',
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 14),
|
||
|
|
Expanded(
|
||
|
|
flex: 5,
|
||
|
|
child: FormBuilderTextField(
|
||
|
|
name: 'especialidad',
|
||
|
|
initialValue: _store.especialidad,
|
||
|
|
decoration: _cleanDecoration(
|
||
|
|
label: 'Especialidad',
|
||
|
|
icon: Icons.medical_services_outlined,
|
||
|
|
isRequired: true,
|
||
|
|
hintText: 'Ej. Cirujano Urólogo',
|
||
|
|
),
|
||
|
|
validator: FormBuilderValidators.compose([
|
||
|
|
FormBuilderValidators.required(errorText: 'Requerido'),
|
||
|
|
FormBuilderValidators.minLength(3, errorText: 'Mínimo 3 caracteres'),
|
||
|
|
]),
|
||
|
|
onChanged: (v) => _store.especialidad = v ?? '',
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
const SizedBox(height: 22),
|
||
|
|
|
||
|
|
// Sección 2: Áreas de Enfoque y Subespecialidades
|
||
|
|
_buildSectionHeader(
|
||
|
|
icon: Icons.local_hospital_outlined,
|
||
|
|
title: 'Áreas de Enfoque y Subespecialidades',
|
||
|
|
subtitle: 'Enfermedades, órganos o áreas de tratamiento de su consulta',
|
||
|
|
),
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
FormBuilderTextField(
|
||
|
|
name: 'descrip_especialidad',
|
||
|
|
initialValue: _store.descripEspecialidad,
|
||
|
|
maxLines: 4,
|
||
|
|
decoration: _cleanDecoration(
|
||
|
|
label: 'Descripción de Especialidades',
|
||
|
|
hintText: 'Ej. Enfermedades Vías Urinarias, Riñones, Vejiga, Próstata, Disfunción eréctil...',
|
||
|
|
),
|
||
|
|
onChanged: (v) => _store.descripEspecialidad = v ?? '',
|
||
|
|
),
|
||
|
|
const SizedBox(height: 22),
|
||
|
|
|
||
|
|
// Sección 3: Datos de Contacto y Clínica
|
||
|
|
_buildSectionHeader(
|
||
|
|
icon: Icons.apartment_rounded,
|
||
|
|
title: 'Datos del Consultorio y Contacto',
|
||
|
|
subtitle: 'Dirección física de la clínica, edificio, teléfonos, celular y correo',
|
||
|
|
),
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
FormBuilderTextField(
|
||
|
|
name: 'descrip_general',
|
||
|
|
initialValue: _store.descripGeneral,
|
||
|
|
maxLines: 4,
|
||
|
|
decoration: _cleanDecoration(
|
||
|
|
label: 'Información General y Contacto',
|
||
|
|
hintText: 'Ej. Instituto materno infantil, Av. Juan Pablo Duarte, Santiago. Tel: 809-583-2394...',
|
||
|
|
),
|
||
|
|
onChanged: (v) => _store.descripGeneral = v ?? '',
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildSectionHeader({
|
||
|
|
required IconData icon,
|
||
|
|
required String title,
|
||
|
|
required String subtitle,
|
||
|
|
}) {
|
||
|
|
return Row(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Icon(icon, size: 18, color: AppColors.primary),
|
||
|
|
const SizedBox(width: 8),
|
||
|
|
Expanded(
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
title,
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 13,
|
||
|
|
fontWeight: FontWeight.w700,
|
||
|
|
color: AppColors.textPrimary,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 2),
|
||
|
|
Text(
|
||
|
|
subtitle,
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 11.5,
|
||
|
|
color: AppColors.textMuted,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|