first commit
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import '../../data/models/patient_model.dart';
|
||||
import '../../data/services/patient_service.dart';
|
||||
|
||||
part 'patient_store.g.dart';
|
||||
|
||||
// ignore: library_private_types_in_public_api
|
||||
class PatientStore = _PatientStoreBase with _$PatientStore;
|
||||
|
||||
abstract class _PatientStoreBase with Store {
|
||||
final PatientService service;
|
||||
|
||||
_PatientStoreBase({PatientService? patientService})
|
||||
: service = patientService ?? PatientService();
|
||||
|
||||
@observable
|
||||
int? id;
|
||||
|
||||
@observable
|
||||
bool isLoading = false;
|
||||
|
||||
@observable
|
||||
bool isSaving = false;
|
||||
|
||||
@observable
|
||||
String? errorMessage;
|
||||
|
||||
@observable
|
||||
String? successMessage;
|
||||
|
||||
// Campos Demográficos Superiores
|
||||
@observable
|
||||
String fecReg = '';
|
||||
|
||||
@observable
|
||||
String cedula = '';
|
||||
|
||||
@observable
|
||||
String nombre = '';
|
||||
|
||||
@observable
|
||||
String apellido = '';
|
||||
|
||||
@observable
|
||||
String direccion = '';
|
||||
|
||||
@observable
|
||||
String telefono = '';
|
||||
|
||||
@observable
|
||||
String ocupacion = '';
|
||||
|
||||
@observable
|
||||
String edad = '';
|
||||
|
||||
@observable
|
||||
String sexo = 'Masculino';
|
||||
|
||||
@observable
|
||||
String seguro = 'Sí';
|
||||
|
||||
@observable
|
||||
String estCivil = 'Soltero';
|
||||
|
||||
// Pestañas Clínicas
|
||||
@observable
|
||||
String motCons = '';
|
||||
|
||||
@observable
|
||||
String fecUltcon = '';
|
||||
|
||||
@observable
|
||||
String hisEnfAct = '';
|
||||
|
||||
// Antecedentes Personales Patológicos
|
||||
@observable
|
||||
String datNi = 'Ninguna';
|
||||
|
||||
@observable
|
||||
String datAdo = 'Ninguna';
|
||||
|
||||
@observable
|
||||
String datAdu = 'Ninguna';
|
||||
|
||||
@observable
|
||||
String datAlerg = 'Ninguna';
|
||||
|
||||
@observable
|
||||
String datTran = 'ninguna';
|
||||
|
||||
@observable
|
||||
String datCir = 'ninguna';
|
||||
|
||||
@observable
|
||||
String datAc = 'ninguno';
|
||||
|
||||
@observable
|
||||
String datOtrosp = 'ninguno';
|
||||
|
||||
// Antecedentes No Patológicos
|
||||
@observable
|
||||
String datMad = 'Viva y sana';
|
||||
|
||||
@observable
|
||||
String datEsp = 'Viva y sana';
|
||||
|
||||
@observable
|
||||
String datPadre = 'Vivo y sano';
|
||||
|
||||
@observable
|
||||
String datHijos = 'Vivos y sanos';
|
||||
|
||||
@observable
|
||||
String datHer = 'Vivos y sanos';
|
||||
|
||||
@observable
|
||||
String datOtrosnp = 'Ningunos';
|
||||
|
||||
// Diagnósticos
|
||||
@observable
|
||||
String metDiagnRad = '';
|
||||
|
||||
@observable
|
||||
String diagnDesc = '';
|
||||
|
||||
@observable
|
||||
String diagnDef = '';
|
||||
|
||||
// Laboratorios y Tratamiento
|
||||
@observable
|
||||
String datLab = '';
|
||||
|
||||
@observable
|
||||
String tratamiento = '';
|
||||
|
||||
// Próxima Cita
|
||||
@observable
|
||||
String fecProxcita = '';
|
||||
|
||||
// Estado de Sala de Espera
|
||||
@observable
|
||||
String estadoEspera = 'no_presente';
|
||||
|
||||
@computed
|
||||
bool get isEditing => id != null;
|
||||
|
||||
@computed
|
||||
bool get isValid =>
|
||||
nombre.trim().isNotEmpty &&
|
||||
apellido.trim().isNotEmpty &&
|
||||
sexo.trim().isNotEmpty &&
|
||||
seguro.trim().isNotEmpty;
|
||||
|
||||
@computed
|
||||
int? get diasParaProximaCita {
|
||||
if (fecProxcita.isEmpty) return null;
|
||||
try {
|
||||
DateTime? target;
|
||||
if (fecProxcita.contains('-')) {
|
||||
target = DateTime.tryParse(fecProxcita);
|
||||
} else if (fecProxcita.contains('/')) {
|
||||
final parts = fecProxcita.split('/');
|
||||
if (parts.length == 3) {
|
||||
final day = int.parse(parts[0]);
|
||||
final month = int.parse(parts[1]);
|
||||
final year = int.parse(parts[2]);
|
||||
target = DateTime(year, month, day);
|
||||
}
|
||||
}
|
||||
if (target == null) return null;
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
final cleanTarget = DateTime(target.year, target.month, target.day);
|
||||
return cleanTarget.difference(today).inDays;
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void initForCreate() {
|
||||
id = null;
|
||||
errorMessage = null;
|
||||
successMessage = null;
|
||||
isLoading = false;
|
||||
isSaving = false;
|
||||
|
||||
final now = DateTime.now();
|
||||
fecReg = DateFormat('dd/MM/yyyy').format(now);
|
||||
fecUltcon = DateFormat('dd/MM/yyyy').format(now);
|
||||
cedula = '';
|
||||
nombre = '';
|
||||
apellido = '';
|
||||
direccion = '';
|
||||
telefono = '';
|
||||
ocupacion = '';
|
||||
edad = '';
|
||||
sexo = 'Masculino';
|
||||
seguro = 'Sí';
|
||||
estCivil = 'Soltero';
|
||||
|
||||
motCons = '';
|
||||
hisEnfAct = '';
|
||||
datNi = 'Ninguna';
|
||||
datAdo = 'Ninguna';
|
||||
datAdu = 'Ninguna';
|
||||
datAlerg = 'Ninguna';
|
||||
datTran = 'ninguna';
|
||||
datCir = 'ninguna';
|
||||
datAc = 'ninguno';
|
||||
datOtrosp = 'ninguno';
|
||||
|
||||
datMad = 'Viva y sana';
|
||||
datEsp = 'Viva y sana';
|
||||
datPadre = 'Vivo y sano';
|
||||
datHijos = 'Vivos y sanos';
|
||||
datHer = 'Vivos y sanos';
|
||||
datOtrosnp = 'Ningunos';
|
||||
|
||||
metDiagnRad = '';
|
||||
diagnDesc = '';
|
||||
diagnDef = '';
|
||||
datLab = '';
|
||||
tratamiento = '';
|
||||
fecProxcita = '';
|
||||
estadoEspera = 'no_presente';
|
||||
}
|
||||
|
||||
@action
|
||||
void populateFromModel(PatientModel p) {
|
||||
id = p.id;
|
||||
fecReg = p.fecReg;
|
||||
fecUltcon = p.fecUltcon;
|
||||
cedula = p.cedula;
|
||||
nombre = p.nombre;
|
||||
apellido = p.apellido;
|
||||
direccion = p.direccion;
|
||||
telefono = p.telefono;
|
||||
ocupacion = p.ocupacion;
|
||||
edad = p.edad;
|
||||
sexo = p.sexo.isNotEmpty ? p.sexo : 'Masculino';
|
||||
seguro = p.seguro.isNotEmpty ? p.seguro : 'Sí';
|
||||
estCivil = p.estCivil.isNotEmpty ? p.estCivil : 'Soltero';
|
||||
|
||||
motCons = p.motCons;
|
||||
hisEnfAct = p.hisEnfAct;
|
||||
datNi = p.datNi.isNotEmpty ? p.datNi : 'Ninguna';
|
||||
datAdo = p.datAdo.isNotEmpty ? p.datAdo : 'Ninguna';
|
||||
datAdu = p.datAdu.isNotEmpty ? p.datAdu : 'Ninguna';
|
||||
datAlerg = p.datAlerg.isNotEmpty ? p.datAlerg : 'Ninguna';
|
||||
datTran = p.datTran.isNotEmpty ? p.datTran : 'ninguna';
|
||||
datCir = p.datCir.isNotEmpty ? p.datCir : 'ninguna';
|
||||
datAc = p.datAc.isNotEmpty ? p.datAc : 'ninguno';
|
||||
datOtrosp = p.datOtrosp.isNotEmpty ? p.datOtrosp : 'ninguno';
|
||||
|
||||
datMad = p.datMad.isNotEmpty ? p.datMad : 'Viva y sana';
|
||||
datEsp = p.datEsp.isNotEmpty ? p.datEsp : 'Viva y sana';
|
||||
datPadre = p.datPadre.isNotEmpty ? p.datPadre : 'Vivo y sano';
|
||||
datHijos = p.datHijos.isNotEmpty ? p.datHijos : 'Vivos y sanos';
|
||||
datHer = p.datHer.isNotEmpty ? p.datHer : 'Vivos y sanos';
|
||||
datOtrosnp = p.datOtrosnp.isNotEmpty ? p.datOtrosnp : 'Ningunos';
|
||||
|
||||
metDiagnRad = p.metDiagnRad;
|
||||
diagnDesc = p.diagnDesc;
|
||||
diagnDef = p.diagnDef;
|
||||
datLab = p.datLab;
|
||||
tratamiento = p.tratamiento;
|
||||
fecProxcita = p.fecProxcita;
|
||||
estadoEspera = p.estadoEspera.isNotEmpty ? p.estadoEspera : 'no_presente';
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> loadPatient(int patientId) async {
|
||||
isLoading = true;
|
||||
errorMessage = null;
|
||||
try {
|
||||
final p = await service.getPatientById(patientId);
|
||||
runInAction(() {
|
||||
populateFromModel(p);
|
||||
isLoading = false;
|
||||
});
|
||||
} catch (e) {
|
||||
runInAction(() {
|
||||
errorMessage = e.toString().replaceAll('Exception: ', '');
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void appendCurrentDateToConsultas() {
|
||||
final dateStr = DateFormat('dd/MM/yyyy').format(DateTime.now());
|
||||
if (motCons.isEmpty) {
|
||||
motCons = '$dateStr: ';
|
||||
} else {
|
||||
motCons = '$motCons\n$dateStr: ';
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
void appendCurrentDateToHistorial() {
|
||||
final dateStr = DateFormat('dd/MM/yyyy').format(DateTime.now());
|
||||
if (hisEnfAct.isEmpty) {
|
||||
hisEnfAct = '$dateStr: ';
|
||||
} else {
|
||||
hisEnfAct = '$hisEnfAct\n$dateStr: ';
|
||||
}
|
||||
}
|
||||
|
||||
PatientModel toModel() {
|
||||
return PatientModel(
|
||||
id: id,
|
||||
nombre: nombre.trim(),
|
||||
apellido: apellido.trim(),
|
||||
telefono: telefono.trim(),
|
||||
direccion: direccion.trim(),
|
||||
cedula: cedula.trim(),
|
||||
ocupacion: ocupacion.trim(),
|
||||
edad: edad.trim(),
|
||||
fecUltcon: fecUltcon.trim(),
|
||||
fecReg: fecReg.trim(),
|
||||
sexo: sexo.trim(),
|
||||
seguro: seguro.trim(),
|
||||
estCivil: estCivil.trim(),
|
||||
motCons: motCons,
|
||||
hisEnfAct: hisEnfAct,
|
||||
datPadre: datPadre,
|
||||
datEsp: datEsp,
|
||||
datHijos: datHijos,
|
||||
datOtrosnp: datOtrosnp,
|
||||
datAlerg: datAlerg,
|
||||
datTran: datTran,
|
||||
datOtrosp: datOtrosp,
|
||||
metDiagnRad: metDiagnRad,
|
||||
datLab: datLab,
|
||||
diagnDesc: diagnDesc,
|
||||
diagnDef: diagnDef,
|
||||
fecProxcita: fecProxcita.trim(),
|
||||
tratamiento: tratamiento,
|
||||
datHer: datHer,
|
||||
datMad: datMad,
|
||||
datNi: datNi,
|
||||
datAdo: datAdo,
|
||||
datAdu: datAdu,
|
||||
datAc: datAc,
|
||||
datCir: datCir,
|
||||
estadoEspera: estadoEspera,
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<bool> savePatient() async {
|
||||
if (!isValid) {
|
||||
errorMessage = 'Faltan campos obligatorios: Nombre, Apellido, Sexo y Seguro.';
|
||||
return false;
|
||||
}
|
||||
|
||||
// Si el paciente estaba en consulta o en espera y el médico guarda los cambios de la consulta médica,
|
||||
// la atención concluye formalmente y su estado de sala de espera pasa a 'atendido'.
|
||||
if (estadoEspera == 'en_consulta' || estadoEspera == 'en_espera') {
|
||||
estadoEspera = 'atendido';
|
||||
}
|
||||
|
||||
isSaving = true;
|
||||
errorMessage = null;
|
||||
successMessage = null;
|
||||
|
||||
try {
|
||||
final model = toModel();
|
||||
if (isEditing) {
|
||||
await service.updatePatient(id!, model.toJson(includeId: false));
|
||||
runInAction(() {
|
||||
successMessage = 'Paciente ${model.fullName} actualizado correctamente';
|
||||
isSaving = false;
|
||||
});
|
||||
} else {
|
||||
final created = await service.createPatient(model);
|
||||
runInAction(() {
|
||||
id = created.id;
|
||||
successMessage = 'Paciente ${model.fullName} guardado correctamente';
|
||||
isSaving = false;
|
||||
});
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
runInAction(() {
|
||||
errorMessage = e.toString().replaceAll('Exception: ', '');
|
||||
isSaving = false;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,873 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'patient_store.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// StoreGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// ignore_for_file: non_constant_identifier_names, unnecessary_brace_in_string_interps, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic, no_leading_underscores_for_local_identifiers
|
||||
|
||||
mixin _$PatientStore on _PatientStoreBase, Store {
|
||||
Computed<bool>? _$isEditingComputed;
|
||||
|
||||
@override
|
||||
bool get isEditing => (_$isEditingComputed ??= Computed<bool>(
|
||||
() => super.isEditing,
|
||||
name: '_PatientStoreBase.isEditing',
|
||||
)).value;
|
||||
Computed<bool>? _$isValidComputed;
|
||||
|
||||
@override
|
||||
bool get isValid => (_$isValidComputed ??= Computed<bool>(
|
||||
() => super.isValid,
|
||||
name: '_PatientStoreBase.isValid',
|
||||
)).value;
|
||||
Computed<int?>? _$diasParaProximaCitaComputed;
|
||||
|
||||
@override
|
||||
int? get diasParaProximaCita =>
|
||||
(_$diasParaProximaCitaComputed ??= Computed<int?>(
|
||||
() => super.diasParaProximaCita,
|
||||
name: '_PatientStoreBase.diasParaProximaCita',
|
||||
)).value;
|
||||
|
||||
late final _$idAtom = Atom(name: '_PatientStoreBase.id', context: context);
|
||||
|
||||
@override
|
||||
int? get id {
|
||||
_$idAtom.reportRead();
|
||||
return super.id;
|
||||
}
|
||||
|
||||
@override
|
||||
set id(int? value) {
|
||||
_$idAtom.reportWrite(value, super.id, () {
|
||||
super.id = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$isLoadingAtom = Atom(
|
||||
name: '_PatientStoreBase.isLoading',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
bool get isLoading {
|
||||
_$isLoadingAtom.reportRead();
|
||||
return super.isLoading;
|
||||
}
|
||||
|
||||
@override
|
||||
set isLoading(bool value) {
|
||||
_$isLoadingAtom.reportWrite(value, super.isLoading, () {
|
||||
super.isLoading = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$isSavingAtom = Atom(
|
||||
name: '_PatientStoreBase.isSaving',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
bool get isSaving {
|
||||
_$isSavingAtom.reportRead();
|
||||
return super.isSaving;
|
||||
}
|
||||
|
||||
@override
|
||||
set isSaving(bool value) {
|
||||
_$isSavingAtom.reportWrite(value, super.isSaving, () {
|
||||
super.isSaving = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$errorMessageAtom = Atom(
|
||||
name: '_PatientStoreBase.errorMessage',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String? get errorMessage {
|
||||
_$errorMessageAtom.reportRead();
|
||||
return super.errorMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
set errorMessage(String? value) {
|
||||
_$errorMessageAtom.reportWrite(value, super.errorMessage, () {
|
||||
super.errorMessage = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$successMessageAtom = Atom(
|
||||
name: '_PatientStoreBase.successMessage',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String? get successMessage {
|
||||
_$successMessageAtom.reportRead();
|
||||
return super.successMessage;
|
||||
}
|
||||
|
||||
@override
|
||||
set successMessage(String? value) {
|
||||
_$successMessageAtom.reportWrite(value, super.successMessage, () {
|
||||
super.successMessage = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$fecRegAtom = Atom(
|
||||
name: '_PatientStoreBase.fecReg',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get fecReg {
|
||||
_$fecRegAtom.reportRead();
|
||||
return super.fecReg;
|
||||
}
|
||||
|
||||
@override
|
||||
set fecReg(String value) {
|
||||
_$fecRegAtom.reportWrite(value, super.fecReg, () {
|
||||
super.fecReg = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$cedulaAtom = Atom(
|
||||
name: '_PatientStoreBase.cedula',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get cedula {
|
||||
_$cedulaAtom.reportRead();
|
||||
return super.cedula;
|
||||
}
|
||||
|
||||
@override
|
||||
set cedula(String value) {
|
||||
_$cedulaAtom.reportWrite(value, super.cedula, () {
|
||||
super.cedula = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$nombreAtom = Atom(
|
||||
name: '_PatientStoreBase.nombre',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get nombre {
|
||||
_$nombreAtom.reportRead();
|
||||
return super.nombre;
|
||||
}
|
||||
|
||||
@override
|
||||
set nombre(String value) {
|
||||
_$nombreAtom.reportWrite(value, super.nombre, () {
|
||||
super.nombre = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$apellidoAtom = Atom(
|
||||
name: '_PatientStoreBase.apellido',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get apellido {
|
||||
_$apellidoAtom.reportRead();
|
||||
return super.apellido;
|
||||
}
|
||||
|
||||
@override
|
||||
set apellido(String value) {
|
||||
_$apellidoAtom.reportWrite(value, super.apellido, () {
|
||||
super.apellido = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$direccionAtom = Atom(
|
||||
name: '_PatientStoreBase.direccion',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get direccion {
|
||||
_$direccionAtom.reportRead();
|
||||
return super.direccion;
|
||||
}
|
||||
|
||||
@override
|
||||
set direccion(String value) {
|
||||
_$direccionAtom.reportWrite(value, super.direccion, () {
|
||||
super.direccion = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$telefonoAtom = Atom(
|
||||
name: '_PatientStoreBase.telefono',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get telefono {
|
||||
_$telefonoAtom.reportRead();
|
||||
return super.telefono;
|
||||
}
|
||||
|
||||
@override
|
||||
set telefono(String value) {
|
||||
_$telefonoAtom.reportWrite(value, super.telefono, () {
|
||||
super.telefono = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$ocupacionAtom = Atom(
|
||||
name: '_PatientStoreBase.ocupacion',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get ocupacion {
|
||||
_$ocupacionAtom.reportRead();
|
||||
return super.ocupacion;
|
||||
}
|
||||
|
||||
@override
|
||||
set ocupacion(String value) {
|
||||
_$ocupacionAtom.reportWrite(value, super.ocupacion, () {
|
||||
super.ocupacion = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$edadAtom = Atom(
|
||||
name: '_PatientStoreBase.edad',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get edad {
|
||||
_$edadAtom.reportRead();
|
||||
return super.edad;
|
||||
}
|
||||
|
||||
@override
|
||||
set edad(String value) {
|
||||
_$edadAtom.reportWrite(value, super.edad, () {
|
||||
super.edad = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$sexoAtom = Atom(
|
||||
name: '_PatientStoreBase.sexo',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get sexo {
|
||||
_$sexoAtom.reportRead();
|
||||
return super.sexo;
|
||||
}
|
||||
|
||||
@override
|
||||
set sexo(String value) {
|
||||
_$sexoAtom.reportWrite(value, super.sexo, () {
|
||||
super.sexo = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$seguroAtom = Atom(
|
||||
name: '_PatientStoreBase.seguro',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get seguro {
|
||||
_$seguroAtom.reportRead();
|
||||
return super.seguro;
|
||||
}
|
||||
|
||||
@override
|
||||
set seguro(String value) {
|
||||
_$seguroAtom.reportWrite(value, super.seguro, () {
|
||||
super.seguro = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$estCivilAtom = Atom(
|
||||
name: '_PatientStoreBase.estCivil',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get estCivil {
|
||||
_$estCivilAtom.reportRead();
|
||||
return super.estCivil;
|
||||
}
|
||||
|
||||
@override
|
||||
set estCivil(String value) {
|
||||
_$estCivilAtom.reportWrite(value, super.estCivil, () {
|
||||
super.estCivil = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$motConsAtom = Atom(
|
||||
name: '_PatientStoreBase.motCons',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get motCons {
|
||||
_$motConsAtom.reportRead();
|
||||
return super.motCons;
|
||||
}
|
||||
|
||||
@override
|
||||
set motCons(String value) {
|
||||
_$motConsAtom.reportWrite(value, super.motCons, () {
|
||||
super.motCons = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$fecUltconAtom = Atom(
|
||||
name: '_PatientStoreBase.fecUltcon',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get fecUltcon {
|
||||
_$fecUltconAtom.reportRead();
|
||||
return super.fecUltcon;
|
||||
}
|
||||
|
||||
@override
|
||||
set fecUltcon(String value) {
|
||||
_$fecUltconAtom.reportWrite(value, super.fecUltcon, () {
|
||||
super.fecUltcon = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$hisEnfActAtom = Atom(
|
||||
name: '_PatientStoreBase.hisEnfAct',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get hisEnfAct {
|
||||
_$hisEnfActAtom.reportRead();
|
||||
return super.hisEnfAct;
|
||||
}
|
||||
|
||||
@override
|
||||
set hisEnfAct(String value) {
|
||||
_$hisEnfActAtom.reportWrite(value, super.hisEnfAct, () {
|
||||
super.hisEnfAct = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datNiAtom = Atom(
|
||||
name: '_PatientStoreBase.datNi',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datNi {
|
||||
_$datNiAtom.reportRead();
|
||||
return super.datNi;
|
||||
}
|
||||
|
||||
@override
|
||||
set datNi(String value) {
|
||||
_$datNiAtom.reportWrite(value, super.datNi, () {
|
||||
super.datNi = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datAdoAtom = Atom(
|
||||
name: '_PatientStoreBase.datAdo',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datAdo {
|
||||
_$datAdoAtom.reportRead();
|
||||
return super.datAdo;
|
||||
}
|
||||
|
||||
@override
|
||||
set datAdo(String value) {
|
||||
_$datAdoAtom.reportWrite(value, super.datAdo, () {
|
||||
super.datAdo = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datAduAtom = Atom(
|
||||
name: '_PatientStoreBase.datAdu',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datAdu {
|
||||
_$datAduAtom.reportRead();
|
||||
return super.datAdu;
|
||||
}
|
||||
|
||||
@override
|
||||
set datAdu(String value) {
|
||||
_$datAduAtom.reportWrite(value, super.datAdu, () {
|
||||
super.datAdu = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datAlergAtom = Atom(
|
||||
name: '_PatientStoreBase.datAlerg',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datAlerg {
|
||||
_$datAlergAtom.reportRead();
|
||||
return super.datAlerg;
|
||||
}
|
||||
|
||||
@override
|
||||
set datAlerg(String value) {
|
||||
_$datAlergAtom.reportWrite(value, super.datAlerg, () {
|
||||
super.datAlerg = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datTranAtom = Atom(
|
||||
name: '_PatientStoreBase.datTran',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datTran {
|
||||
_$datTranAtom.reportRead();
|
||||
return super.datTran;
|
||||
}
|
||||
|
||||
@override
|
||||
set datTran(String value) {
|
||||
_$datTranAtom.reportWrite(value, super.datTran, () {
|
||||
super.datTran = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datCirAtom = Atom(
|
||||
name: '_PatientStoreBase.datCir',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datCir {
|
||||
_$datCirAtom.reportRead();
|
||||
return super.datCir;
|
||||
}
|
||||
|
||||
@override
|
||||
set datCir(String value) {
|
||||
_$datCirAtom.reportWrite(value, super.datCir, () {
|
||||
super.datCir = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datAcAtom = Atom(
|
||||
name: '_PatientStoreBase.datAc',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datAc {
|
||||
_$datAcAtom.reportRead();
|
||||
return super.datAc;
|
||||
}
|
||||
|
||||
@override
|
||||
set datAc(String value) {
|
||||
_$datAcAtom.reportWrite(value, super.datAc, () {
|
||||
super.datAc = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datOtrospAtom = Atom(
|
||||
name: '_PatientStoreBase.datOtrosp',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datOtrosp {
|
||||
_$datOtrospAtom.reportRead();
|
||||
return super.datOtrosp;
|
||||
}
|
||||
|
||||
@override
|
||||
set datOtrosp(String value) {
|
||||
_$datOtrospAtom.reportWrite(value, super.datOtrosp, () {
|
||||
super.datOtrosp = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datMadAtom = Atom(
|
||||
name: '_PatientStoreBase.datMad',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datMad {
|
||||
_$datMadAtom.reportRead();
|
||||
return super.datMad;
|
||||
}
|
||||
|
||||
@override
|
||||
set datMad(String value) {
|
||||
_$datMadAtom.reportWrite(value, super.datMad, () {
|
||||
super.datMad = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datEspAtom = Atom(
|
||||
name: '_PatientStoreBase.datEsp',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datEsp {
|
||||
_$datEspAtom.reportRead();
|
||||
return super.datEsp;
|
||||
}
|
||||
|
||||
@override
|
||||
set datEsp(String value) {
|
||||
_$datEspAtom.reportWrite(value, super.datEsp, () {
|
||||
super.datEsp = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datPadreAtom = Atom(
|
||||
name: '_PatientStoreBase.datPadre',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datPadre {
|
||||
_$datPadreAtom.reportRead();
|
||||
return super.datPadre;
|
||||
}
|
||||
|
||||
@override
|
||||
set datPadre(String value) {
|
||||
_$datPadreAtom.reportWrite(value, super.datPadre, () {
|
||||
super.datPadre = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datHijosAtom = Atom(
|
||||
name: '_PatientStoreBase.datHijos',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datHijos {
|
||||
_$datHijosAtom.reportRead();
|
||||
return super.datHijos;
|
||||
}
|
||||
|
||||
@override
|
||||
set datHijos(String value) {
|
||||
_$datHijosAtom.reportWrite(value, super.datHijos, () {
|
||||
super.datHijos = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datHerAtom = Atom(
|
||||
name: '_PatientStoreBase.datHer',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datHer {
|
||||
_$datHerAtom.reportRead();
|
||||
return super.datHer;
|
||||
}
|
||||
|
||||
@override
|
||||
set datHer(String value) {
|
||||
_$datHerAtom.reportWrite(value, super.datHer, () {
|
||||
super.datHer = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datOtrosnpAtom = Atom(
|
||||
name: '_PatientStoreBase.datOtrosnp',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datOtrosnp {
|
||||
_$datOtrosnpAtom.reportRead();
|
||||
return super.datOtrosnp;
|
||||
}
|
||||
|
||||
@override
|
||||
set datOtrosnp(String value) {
|
||||
_$datOtrosnpAtom.reportWrite(value, super.datOtrosnp, () {
|
||||
super.datOtrosnp = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$metDiagnRadAtom = Atom(
|
||||
name: '_PatientStoreBase.metDiagnRad',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get metDiagnRad {
|
||||
_$metDiagnRadAtom.reportRead();
|
||||
return super.metDiagnRad;
|
||||
}
|
||||
|
||||
@override
|
||||
set metDiagnRad(String value) {
|
||||
_$metDiagnRadAtom.reportWrite(value, super.metDiagnRad, () {
|
||||
super.metDiagnRad = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$diagnDescAtom = Atom(
|
||||
name: '_PatientStoreBase.diagnDesc',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get diagnDesc {
|
||||
_$diagnDescAtom.reportRead();
|
||||
return super.diagnDesc;
|
||||
}
|
||||
|
||||
@override
|
||||
set diagnDesc(String value) {
|
||||
_$diagnDescAtom.reportWrite(value, super.diagnDesc, () {
|
||||
super.diagnDesc = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$diagnDefAtom = Atom(
|
||||
name: '_PatientStoreBase.diagnDef',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get diagnDef {
|
||||
_$diagnDefAtom.reportRead();
|
||||
return super.diagnDef;
|
||||
}
|
||||
|
||||
@override
|
||||
set diagnDef(String value) {
|
||||
_$diagnDefAtom.reportWrite(value, super.diagnDef, () {
|
||||
super.diagnDef = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$datLabAtom = Atom(
|
||||
name: '_PatientStoreBase.datLab',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get datLab {
|
||||
_$datLabAtom.reportRead();
|
||||
return super.datLab;
|
||||
}
|
||||
|
||||
@override
|
||||
set datLab(String value) {
|
||||
_$datLabAtom.reportWrite(value, super.datLab, () {
|
||||
super.datLab = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$tratamientoAtom = Atom(
|
||||
name: '_PatientStoreBase.tratamiento',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get tratamiento {
|
||||
_$tratamientoAtom.reportRead();
|
||||
return super.tratamiento;
|
||||
}
|
||||
|
||||
@override
|
||||
set tratamiento(String value) {
|
||||
_$tratamientoAtom.reportWrite(value, super.tratamiento, () {
|
||||
super.tratamiento = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$fecProxcitaAtom = Atom(
|
||||
name: '_PatientStoreBase.fecProxcita',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get fecProxcita {
|
||||
_$fecProxcitaAtom.reportRead();
|
||||
return super.fecProxcita;
|
||||
}
|
||||
|
||||
@override
|
||||
set fecProxcita(String value) {
|
||||
_$fecProxcitaAtom.reportWrite(value, super.fecProxcita, () {
|
||||
super.fecProxcita = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$estadoEsperaAtom = Atom(
|
||||
name: '_PatientStoreBase.estadoEspera',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
String get estadoEspera {
|
||||
_$estadoEsperaAtom.reportRead();
|
||||
return super.estadoEspera;
|
||||
}
|
||||
|
||||
@override
|
||||
set estadoEspera(String value) {
|
||||
_$estadoEsperaAtom.reportWrite(value, super.estadoEspera, () {
|
||||
super.estadoEspera = value;
|
||||
});
|
||||
}
|
||||
|
||||
late final _$loadPatientAsyncAction = AsyncAction(
|
||||
'_PatientStoreBase.loadPatient',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<void> loadPatient(int patientId) {
|
||||
return _$loadPatientAsyncAction.run(() => super.loadPatient(patientId));
|
||||
}
|
||||
|
||||
late final _$savePatientAsyncAction = AsyncAction(
|
||||
'_PatientStoreBase.savePatient',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
Future<bool> savePatient() {
|
||||
return _$savePatientAsyncAction.run(() => super.savePatient());
|
||||
}
|
||||
|
||||
late final _$_PatientStoreBaseActionController = ActionController(
|
||||
name: '_PatientStoreBase',
|
||||
context: context,
|
||||
);
|
||||
|
||||
@override
|
||||
void initForCreate() {
|
||||
final _$actionInfo = _$_PatientStoreBaseActionController.startAction(
|
||||
name: '_PatientStoreBase.initForCreate',
|
||||
);
|
||||
try {
|
||||
return super.initForCreate();
|
||||
} finally {
|
||||
_$_PatientStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void populateFromModel(PatientModel p) {
|
||||
final _$actionInfo = _$_PatientStoreBaseActionController.startAction(
|
||||
name: '_PatientStoreBase.populateFromModel',
|
||||
);
|
||||
try {
|
||||
return super.populateFromModel(p);
|
||||
} finally {
|
||||
_$_PatientStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void appendCurrentDateToConsultas() {
|
||||
final _$actionInfo = _$_PatientStoreBaseActionController.startAction(
|
||||
name: '_PatientStoreBase.appendCurrentDateToConsultas',
|
||||
);
|
||||
try {
|
||||
return super.appendCurrentDateToConsultas();
|
||||
} finally {
|
||||
_$_PatientStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void appendCurrentDateToHistorial() {
|
||||
final _$actionInfo = _$_PatientStoreBaseActionController.startAction(
|
||||
name: '_PatientStoreBase.appendCurrentDateToHistorial',
|
||||
);
|
||||
try {
|
||||
return super.appendCurrentDateToHistorial();
|
||||
} finally {
|
||||
_$_PatientStoreBaseActionController.endAction(_$actionInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '''
|
||||
id: ${id},
|
||||
isLoading: ${isLoading},
|
||||
isSaving: ${isSaving},
|
||||
errorMessage: ${errorMessage},
|
||||
successMessage: ${successMessage},
|
||||
fecReg: ${fecReg},
|
||||
cedula: ${cedula},
|
||||
nombre: ${nombre},
|
||||
apellido: ${apellido},
|
||||
direccion: ${direccion},
|
||||
telefono: ${telefono},
|
||||
ocupacion: ${ocupacion},
|
||||
edad: ${edad},
|
||||
sexo: ${sexo},
|
||||
seguro: ${seguro},
|
||||
estCivil: ${estCivil},
|
||||
motCons: ${motCons},
|
||||
fecUltcon: ${fecUltcon},
|
||||
hisEnfAct: ${hisEnfAct},
|
||||
datNi: ${datNi},
|
||||
datAdo: ${datAdo},
|
||||
datAdu: ${datAdu},
|
||||
datAlerg: ${datAlerg},
|
||||
datTran: ${datTran},
|
||||
datCir: ${datCir},
|
||||
datAc: ${datAc},
|
||||
datOtrosp: ${datOtrosp},
|
||||
datMad: ${datMad},
|
||||
datEsp: ${datEsp},
|
||||
datPadre: ${datPadre},
|
||||
datHijos: ${datHijos},
|
||||
datHer: ${datHer},
|
||||
datOtrosnp: ${datOtrosnp},
|
||||
metDiagnRad: ${metDiagnRad},
|
||||
diagnDesc: ${diagnDesc},
|
||||
diagnDef: ${diagnDef},
|
||||
datLab: ${datLab},
|
||||
tratamiento: ${tratamiento},
|
||||
fecProxcita: ${fecProxcita},
|
||||
estadoEspera: ${estadoEspera},
|
||||
isEditing: ${isEditing},
|
||||
isValid: ${isValid},
|
||||
diasParaProximaCita: ${diasParaProximaCita}
|
||||
''';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../../../../core/widgets/medical_modal.dart';
|
||||
import '../../../insurance_auth/presentation/views/insurance_auth_modal_view.dart';
|
||||
import '../../../prescription/presentation/views/prescription_modal_view.dart';
|
||||
import '../../../referrals/presentation/views/department_referral_modal_view.dart';
|
||||
import '../../../referrals/presentation/views/doctor_referral_modal_view.dart';
|
||||
import '../../data/models/patient_model.dart';
|
||||
import '../store/patient_store.dart';
|
||||
import '../widgets/clinical_documents_dropdown.dart';
|
||||
import '../widgets/patient_clinical_tabs.dart';
|
||||
import '../widgets/patient_demographics_form.dart';
|
||||
import '../widgets/secretary_appointment_section.dart';
|
||||
|
||||
class PatientModalView extends StatefulWidget {
|
||||
final PatientModel? initialPatient;
|
||||
final int? patientId;
|
||||
final ValueChanged<PatientModel>? onSaved;
|
||||
final bool isSecretary;
|
||||
|
||||
const PatientModalView({
|
||||
super.key,
|
||||
this.initialPatient,
|
||||
this.patientId,
|
||||
this.onSaved,
|
||||
this.isSecretary = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PatientModalView> createState() => _PatientModalViewState();
|
||||
}
|
||||
|
||||
class _PatientModalViewState extends State<PatientModalView> {
|
||||
late final PatientStore _store;
|
||||
final _formKey = GlobalKey<FormBuilderState>();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_store = PatientStore();
|
||||
|
||||
if (widget.initialPatient != null) {
|
||||
_store.populateFromModel(widget.initialPatient!);
|
||||
} else if (widget.patientId != null) {
|
||||
_store.loadPatient(widget.patientId!);
|
||||
} else {
|
||||
_store.initForCreate();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleSave() async {
|
||||
if (_formKey.currentState?.saveAndValidate() ?? false) {
|
||||
final values = _formKey.currentState!.value;
|
||||
if (values['nombre'] != null) _store.nombre = values['nombre'].toString();
|
||||
if (values['apellido'] != null) _store.apellido = values['apellido'].toString();
|
||||
if (values['cedula'] != null) _store.cedula = values['cedula'].toString();
|
||||
if (values['telefono'] != null) _store.telefono = values['telefono'].toString();
|
||||
if (values['direccion'] != null) _store.direccion = values['direccion'].toString();
|
||||
if (values['ocupacion'] != null) _store.ocupacion = values['ocupacion'].toString();
|
||||
if (values['edad'] != null) _store.edad = values['edad'].toString();
|
||||
if (values['sexo'] != null) _store.sexo = values['sexo'].toString();
|
||||
if (values['seguro'] != null) _store.seguro = values['seguro'].toString();
|
||||
if (values['est_civil'] != null) _store.estCivil = values['est_civil'].toString();
|
||||
|
||||
final success = await _store.savePatient();
|
||||
if (success && mounted) {
|
||||
if (widget.onSaved != null) {
|
||||
widget.onSaved!(_store.toModel());
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(_store.successMessage ?? 'Operación completada exitosamente'),
|
||||
backgroundColor: const Color(0xFF00A65A),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
Navigator.of(context).pop(_store.toModel());
|
||||
}
|
||||
} else {
|
||||
_store.errorMessage = 'Por favor complete los campos obligatorios resaltados en rojo.';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
final title = widget.isSecretary
|
||||
? (_store.isEditing ? 'Ficha de Recepción' : 'Nuevo Registro de Paciente')
|
||||
: (_store.isEditing ? 'Editar Paciente' : 'Registro De Paciente');
|
||||
final subtitle = widget.isSecretary
|
||||
? 'Recepción, datos personales y programación de citas'
|
||||
: (_store.isEditing
|
||||
? 'Actualizando ficha clínica del paciente ID: ${_store.id ?? ''}'
|
||||
: 'Ingrese los datos generales y antecedentes del paciente');
|
||||
final mediaQuery = MediaQuery.sizeOf(context);
|
||||
final modalHeight = (mediaQuery.height * 0.92).clamp(740.0, 900.0);
|
||||
final modalWidth = (mediaQuery.width * 0.92).clamp(980.0, 1160.0);
|
||||
|
||||
return MedicalModalDialog(
|
||||
maxWidth: modalWidth,
|
||||
maxHeight: modalHeight,
|
||||
title: title,
|
||||
titleIcon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
_store.isEditing ? Icons.edit_note_rounded : Icons.person_add_alt_1_rounded,
|
||||
color: AppColors.primary,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12, color: AppColors.textMuted),
|
||||
),
|
||||
actions: [
|
||||
if (_store.estadoEspera == 'en_consulta') ...[
|
||||
Container(
|
||||
height: 34,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE0F2FE),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFF7DD3FC)),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.medical_services_rounded, size: 13, color: Color(0xFF0369A1)),
|
||||
SizedBox(width: 5),
|
||||
Text(
|
||||
'En Consulta',
|
||||
style: TextStyle(fontSize: 11.5, fontWeight: FontWeight.w700, color: Color(0xFF0369A1)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
] else if (_store.estadoEspera == 'en_espera') ...[
|
||||
Container(
|
||||
height: 34,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFDCFCE7),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFF86EFAC)),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.hourglass_top_rounded, size: 13, color: Color(0xFF15803D)),
|
||||
SizedBox(width: 5),
|
||||
Text(
|
||||
'En Espera',
|
||||
style: TextStyle(fontSize: 11.5, fontWeight: FontWeight.w700, color: Color(0xFF15803D)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
] else if (_store.estadoEspera == 'atendido') ...[
|
||||
Container(
|
||||
height: 34,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFCBD5E1)),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.check_circle_rounded, size: 13, color: Color(0xFF64748B)),
|
||||
SizedBox(width: 5),
|
||||
Text(
|
||||
'Atendido',
|
||||
style: TextStyle(fontSize: 11.5, fontWeight: FontWeight.w600, color: Color(0xFF64748B)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
if (!widget.isSecretary)
|
||||
ClinicalDocumentsDropdown(
|
||||
onSelect: (docType) {
|
||||
if (docType == ClinicalDocumentType.recetaMedica) {
|
||||
final patientName = '${_store.nombre} ${_store.apellido}'.trim();
|
||||
showMedicalModal(
|
||||
context: context,
|
||||
builder: (modalCtx) => PrescriptionModalView(
|
||||
patientName: patientName.isNotEmpty ? patientName : 'Sin Nombre',
|
||||
),
|
||||
);
|
||||
} else if (docType == ClinicalDocumentType.autorizacionSeguro) {
|
||||
final patientName = '${_store.nombre} ${_store.apellido}'.trim();
|
||||
final diagnosis = _store.diagnDesc.trim().isNotEmpty
|
||||
? _store.diagnDesc.trim()
|
||||
: (_store.diagnDef.trim().isNotEmpty ? _store.diagnDef.trim() : null);
|
||||
showMedicalModal(
|
||||
context: context,
|
||||
builder: (modalCtx) => InsuranceAuthModalView(
|
||||
patientName: patientName.isNotEmpty ? patientName : 'Sin Nombre',
|
||||
initialDiagnosis: diagnosis,
|
||||
),
|
||||
);
|
||||
} else if (docType == ClinicalDocumentType.referirDoctor) {
|
||||
final patientName = '${_store.nombre} ${_store.apellido}'.trim();
|
||||
showMedicalModal(
|
||||
context: context,
|
||||
builder: (modalCtx) => DoctorReferralModalView(
|
||||
patientName: patientName.isNotEmpty ? patientName : 'Sin Nombre',
|
||||
),
|
||||
);
|
||||
} else if (docType == ClinicalDocumentType.referirDepartamento) {
|
||||
final patientName = '${_store.nombre} ${_store.apellido}'.trim();
|
||||
showMedicalModal(
|
||||
context: context,
|
||||
builder: (modalCtx) => DepartmentReferralModalView(
|
||||
patientName: patientName.isNotEmpty ? patientName : 'Sin Nombre',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
bottomBar: 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.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: _store.errorMessage != null
|
||||
? AppColors.error.withValues(alpha: 0.12)
|
||||
: (_store.successMessage != null
|
||||
? const Color(0xFF00A65A).withValues(alpha: 0.12)
|
||||
: AppColors.primary.withValues(alpha: 0.1)),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
_store.errorMessage != null
|
||||
? Icons.error_outline_rounded
|
||||
: (_store.successMessage != null
|
||||
? Icons.check_circle_outline_rounded
|
||||
: Icons.info_outline_rounded),
|
||||
size: 15,
|
||||
color: _store.errorMessage != null
|
||||
? AppColors.error
|
||||
: (_store.successMessage != null
|
||||
? const Color(0xFF00A65A)
|
||||
: AppColors.primary),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_store.errorMessage ??
|
||||
(_store.successMessage ??
|
||||
(_store.isSaving
|
||||
? 'Guardando datos en el servidor...'
|
||||
: (_store.estadoEspera == 'en_consulta'
|
||||
? 'Consulta médica activa · Al guardar pasará automáticamente a Atendido'
|
||||
: (_store.isEditing
|
||||
? 'Realizando cambios en paciente'
|
||||
: 'Listo para registrar')))),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _store.errorMessage != null
|
||||
? AppColors.error
|
||||
: (_store.successMessage != null
|
||||
? const Color(0xFF00A65A)
|
||||
: AppColors.textSecondary),
|
||||
),
|
||||
),
|
||||
),
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: _store.isLoading
|
||||
? const Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(strokeWidth: 3),
|
||||
SizedBox(height: 16),
|
||||
Text('Cargando ficha del paciente...', style: TextStyle(fontSize: 13)),
|
||||
],
|
||||
),
|
||||
)
|
||||
: FormBuilder(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
children: [
|
||||
PatientDemographicsForm(
|
||||
store: _store,
|
||||
formKey: _formKey,
|
||||
onSave: _handleSave,
|
||||
),
|
||||
if (widget.isSecretary)
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: SecretaryAppointmentSection(store: _store),
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: PatientClinicalTabs(store: _store),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
|
||||
enum ClinicalDocumentType {
|
||||
recetaMedica,
|
||||
autorizacionSeguro,
|
||||
referirDoctor,
|
||||
referirDepartamento,
|
||||
}
|
||||
|
||||
class ClinicalDocumentsDropdown extends StatelessWidget {
|
||||
final ValueChanged<ClinicalDocumentType> onSelect;
|
||||
|
||||
const ClinicalDocumentsDropdown({
|
||||
super.key,
|
||||
required this.onSelect,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<ClinicalDocumentType>(
|
||||
tooltip: 'Documentos Clínicos e Impresión',
|
||||
offset: const Offset(0, 44),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
side: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
),
|
||||
elevation: 6,
|
||||
color: Colors.white,
|
||||
onSelected: onSelect,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: ClinicalDocumentType.recetaMedica,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00A65A).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.medication_rounded,
|
||||
color: Color(0xFF00A65A),
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Receta Médica',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Formato media carta institucional',
|
||||
style: TextStyle(fontSize: 11, color: AppColors.textMuted),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(height: 1),
|
||||
PopupMenuItem(
|
||||
value: ClinicalDocumentType.autorizacionSeguro,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00A65A).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.verified_user_outlined,
|
||||
color: Color(0xFF00A65A),
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Autorización De Seguro',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Solicitud oficial de cobertura ARS',
|
||||
style: TextStyle(fontSize: 11, color: AppColors.textMuted),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(height: 1),
|
||||
PopupMenuItem(
|
||||
value: ClinicalDocumentType.referirDoctor,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00A65A).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.person_pin_circle_outlined,
|
||||
color: Color(0xFF00A65A),
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Referir a Doctor(a)',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Interconsulta médica a especialista',
|
||||
style: TextStyle(fontSize: 11, color: AppColors.textMuted),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuDivider(height: 1),
|
||||
PopupMenuItem(
|
||||
value: ClinicalDocumentType.referirDepartamento,
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00A65A).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.apartment_rounded,
|
||||
color: Color(0xFF00A65A),
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Referir a Departamento',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 13,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Referimiento a servicio institucional',
|
||||
style: TextStyle(fontSize: 11, color: AppColors.textMuted),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFCBD5E1)),
|
||||
),
|
||||
child: const Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.description_outlined, color: AppColors.primary, size: 16),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'Documentos Clínicos',
|
||||
style: TextStyle(
|
||||
color: AppColors.textPrimary,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12.5,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 4),
|
||||
Icon(Icons.keyboard_arrow_down_rounded, color: AppColors.textSecondary, size: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,939 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_form_builder/flutter_form_builder.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../store/patient_store.dart';
|
||||
|
||||
class PatientClinicalTabs extends StatefulWidget {
|
||||
final PatientStore store;
|
||||
|
||||
const PatientClinicalTabs({super.key, required this.store});
|
||||
|
||||
@override
|
||||
State<PatientClinicalTabs> createState() => _PatientClinicalTabsState();
|
||||
}
|
||||
|
||||
class _PatientClinicalTabsState extends State<PatientClinicalTabs>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final TabController _tabController;
|
||||
late final TextEditingController _motConsController;
|
||||
late final FocusNode _motConsFocusNode;
|
||||
late final TextEditingController _hisEnfActController;
|
||||
late final FocusNode _hisEnfActFocusNode;
|
||||
ReactionDisposer? _disposerMotCons;
|
||||
ReactionDisposer? _disposerHisEnfAct;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 7, vsync: this);
|
||||
_motConsController = TextEditingController(text: widget.store.motCons);
|
||||
_motConsFocusNode = FocusNode();
|
||||
_hisEnfActController = TextEditingController(text: widget.store.hisEnfAct);
|
||||
_hisEnfActFocusNode = FocusNode();
|
||||
|
||||
_disposerMotCons = reaction((_) => widget.store.motCons, (String val) {
|
||||
if (_motConsController.text != val) {
|
||||
_motConsController.text = val;
|
||||
}
|
||||
});
|
||||
|
||||
_disposerHisEnfAct = reaction((_) => widget.store.hisEnfAct, (String val) {
|
||||
if (_hisEnfActController.text != val) {
|
||||
_hisEnfActController.text = val;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_disposerMotCons?.call();
|
||||
_disposerHisEnfAct?.call();
|
||||
_tabController.dispose();
|
||||
_motConsController.dispose();
|
||||
_motConsFocusNode.dispose();
|
||||
_hisEnfActController.dispose();
|
||||
_hisEnfActFocusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _insertDateAtCursor({
|
||||
required TextEditingController controller,
|
||||
required FocusNode focusNode,
|
||||
required ValueChanged<String> onUpdateStore,
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
final dateStr = DateFormat('dd/MM/yyyy').format(now);
|
||||
final insertionText = '$dateStr: ';
|
||||
|
||||
final currentText = controller.text;
|
||||
final selection = controller.selection;
|
||||
|
||||
int start = selection.start;
|
||||
int end = selection.end;
|
||||
|
||||
// Si el cursor no es válido o no está enfocado, insertar al final
|
||||
if (start < 0 || end < 0) {
|
||||
start = currentText.length;
|
||||
end = currentText.length;
|
||||
}
|
||||
|
||||
if (start > end) {
|
||||
final temp = start;
|
||||
start = end;
|
||||
end = temp;
|
||||
}
|
||||
|
||||
final before = currentText.substring(0, start);
|
||||
final after = currentText.substring(end);
|
||||
final newText = '$before$insertionText$after';
|
||||
final newOffset = start + insertionText.length;
|
||||
|
||||
controller.value = TextEditingValue(
|
||||
text: newText,
|
||||
selection: TextSelection.collapsed(offset: newOffset),
|
||||
);
|
||||
|
||||
onUpdateStore(newText);
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
|
||||
InputDecoration _cleanTextAreaDecoration(String hintText) {
|
||||
return InputDecoration(
|
||||
hintText: hintText,
|
||||
hintStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 13),
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFF8FAFC),
|
||||
contentPadding: const EdgeInsets.all(16),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
InputDecoration _cleanDropdownDecoration(String label) {
|
||||
return InputDecoration(
|
||||
labelText: label,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
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),
|
||||
),
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
floatingLabelStyle: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
hintStyle: const TextStyle(
|
||||
fontSize: 13.5,
|
||||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildClinicalTextArea({
|
||||
required String name,
|
||||
String? initialValue,
|
||||
TextEditingController? controller,
|
||||
FocusNode? focusNode,
|
||||
required ValueChanged<String> onChanged,
|
||||
required String hintText,
|
||||
VoidCallback? onInsertDate,
|
||||
String? dateLabel,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FormBuilderTextField(
|
||||
name: name,
|
||||
controller: controller,
|
||||
initialValue: controller == null ? initialValue : null,
|
||||
focusNode: focusNode,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
style: const TextStyle(fontSize: 13.5, height: 1.5, color: AppColors.textPrimary),
|
||||
decoration: _cleanTextAreaDecoration(hintText),
|
||||
onChanged: (v) => onChanged(v ?? ''),
|
||||
),
|
||||
),
|
||||
if (onInsertDate != null) ...[
|
||||
const SizedBox(width: 16),
|
||||
SizedBox(
|
||||
width: 200,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (dateLabel != null && dateLabel.isNotEmpty) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryContainer.withValues(alpha: 0.6),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: AppColors.primary.withValues(alpha: 0.2)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.history_rounded, size: 16, color: AppColors.primary),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
dateLabel,
|
||||
style: const TextStyle(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const Text(
|
||||
'Entrada de Consulta',
|
||||
style: TextStyle(fontSize: 11, fontWeight: FontWeight.w700, color: AppColors.textSecondary),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Inserte el sello de tiempo para documentar una nueva nota clínica.',
|
||||
style: TextStyle(fontSize: 11, color: AppColors.textMuted, height: 1.3),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
ElevatedButton.icon(
|
||||
onPressed: onInsertDate,
|
||||
icon: const Icon(Icons.add_alarm_rounded, size: 16),
|
||||
label: const Text('Fecha actual', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w600)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDropdownItem({
|
||||
required String name,
|
||||
required String label,
|
||||
required String initialValue,
|
||||
required List<String> items,
|
||||
required ValueChanged<String> onChanged,
|
||||
}) {
|
||||
final list = items.contains(initialValue)
|
||||
? items
|
||||
: (initialValue.isNotEmpty ? [initialValue, ...items] : items);
|
||||
return FormBuilderDropdown<String>(
|
||||
name: name,
|
||||
initialValue: initialValue.isNotEmpty ? initialValue : null,
|
||||
decoration: _cleanDropdownDecoration(label),
|
||||
hint: const Text(
|
||||
'Seleccionar...',
|
||||
style: TextStyle(fontSize: 13.5, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
style: const TextStyle(fontSize: 13.5, color: AppColors.textPrimary, fontWeight: FontWeight.w500),
|
||||
onChanged: (v) {
|
||||
if (v != null) onChanged(v);
|
||||
},
|
||||
items: list.map((item) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: item,
|
||||
child: Text(
|
||||
item,
|
||||
style: const TextStyle(fontSize: 13.5),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAntecedentesTab(PatientStore s) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Patológicos
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.healing_rounded, size: 18, color: AppColors.primary),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Antecedentes Personales Patológicos',
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: AppColors.textPrimary),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_ni',
|
||||
label: 'Infancia',
|
||||
initialValue: s.datNi,
|
||||
items: const ['sarampión', 'varicela', 'Tos ferina', 'Hepatitis', 'Ninguna'],
|
||||
onChanged: (v) => s.datNi = v,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_ado',
|
||||
label: 'Adolescencia',
|
||||
initialValue: s.datAdo,
|
||||
items: const ['Asma', 'Trastornos alimenticios', 'TBC', 'VIH', 'Ninguna'],
|
||||
onChanged: (v) => s.datAdo = v,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_adu',
|
||||
label: 'Adultez',
|
||||
initialValue: s.datAdu,
|
||||
items: const [
|
||||
'TBC',
|
||||
'VIH',
|
||||
'Hepatopatías',
|
||||
'Cardiopatías',
|
||||
'Diabetes',
|
||||
'HTA',
|
||||
'Dislipidemias',
|
||||
'Ninguna',
|
||||
],
|
||||
onChanged: (v) => s.datAdu = v,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_alerg',
|
||||
label: 'Alergias',
|
||||
initialValue: s.datAlerg,
|
||||
items: const ['Ninguna', 'Penicilina', 'Sulfas', 'AINEs'],
|
||||
onChanged: (v) => s.datAlerg = v,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_tran',
|
||||
label: 'Transfusión sanguínea',
|
||||
initialValue: s.datTran,
|
||||
items: const ['1 ves', '2 veces', '3 veces', 'ninguna'],
|
||||
onChanged: (v) => s.datTran = v,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_cir',
|
||||
label: 'Intervenciones quirúrgicas',
|
||||
initialValue: s.datCir,
|
||||
items: const ['ninguna', 'Apendicectomía', 'Cesárea', 'Colecistectomía'],
|
||||
onChanged: (v) => s.datCir = v,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_ac',
|
||||
label: 'Accidentes',
|
||||
initialValue: s.datAc,
|
||||
items: const ['Si', 'ninguno'],
|
||||
onChanged: (v) => s.datAc = v,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_otrosp',
|
||||
label: 'Otros patológicos',
|
||||
initialValue: s.datOtrosp,
|
||||
items: const ['ninguno'],
|
||||
onChanged: (v) => s.datOtrosp = v,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// No patológicos
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.family_restroom_rounded, size: 18, color: AppColors.primary),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'Antecedentes Personales No Patológicos (Familiares)',
|
||||
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w700, color: AppColors.textPrimary),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_mad',
|
||||
label: 'Madre',
|
||||
initialValue: s.datMad,
|
||||
items: const ['Viva y sana', 'Fallecida', 'Enferma'],
|
||||
onChanged: (v) => s.datMad = v,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_esp',
|
||||
label: 'Pareja',
|
||||
initialValue: s.datEsp,
|
||||
items: const ['Viva y sana', 'Fallecida', 'Enferma'],
|
||||
onChanged: (v) => s.datEsp = v,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_padre',
|
||||
label: 'Padre',
|
||||
initialValue: s.datPadre,
|
||||
items: const ['Vivo y sano', 'Fallecido', 'Enfermo'],
|
||||
onChanged: (v) => s.datPadre = v,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_hijos',
|
||||
label: 'Hijos',
|
||||
initialValue: s.datHijos,
|
||||
items: const ['Vivos y sanos', 'Enfermos', 'Ninguno'],
|
||||
onChanged: (v) => s.datHijos = v,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_her',
|
||||
label: 'Hermanos',
|
||||
initialValue: s.datHer,
|
||||
items: const ['Vivos y sanos', 'Fallecidos', 'Enfermos', 'Ninguno'],
|
||||
onChanged: (v) => s.datHer = v,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildDropdownItem(
|
||||
name: 'dat_otrosnp',
|
||||
label: 'Otros no patológicos',
|
||||
initialValue: s.datOtrosnp,
|
||||
items: const ['Ningunos'],
|
||||
onChanged: (v) => s.datOtrosnp = v,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDiagnosticosTab(PatientStore s) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.biotech_rounded, size: 16, color: AppColors.primary),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'Métodos diagnósticos, radiológicos e invasivos',
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.textPrimary),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: FormBuilderTextField(
|
||||
name: 'met_diagn_rad',
|
||||
initialValue: s.metDiagnRad,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
decoration: _cleanTextAreaDecoration('Describa radiografías, ecografías, resonancias o procedimientos invasivos...'),
|
||||
onChanged: (v) => s.metDiagnRad = v ?? '',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.star_outline_rounded, size: 16, color: AppColors.primary),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'Diagnósticos a destacar',
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.textPrimary),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: FormBuilderTextField(
|
||||
name: 'diagn_desc',
|
||||
initialValue: s.diagnDesc,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
decoration: _cleanTextAreaDecoration('Patologías principales, hallazgos o diagnósticos relevantes...'),
|
||||
onChanged: (v) => s.diagnDesc = v ?? '',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(Icons.verified_outlined, size: 16, color: AppColors.primary),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'Diagnóstico definitivo',
|
||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w700, color: AppColors.textPrimary),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: FormBuilderTextField(
|
||||
name: 'diagn_def',
|
||||
initialValue: s.diagnDef,
|
||||
maxLines: null,
|
||||
expands: true,
|
||||
textAlignVertical: TextAlignVertical.top,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
decoration: _cleanTextAreaDecoration('Conclusión diagnóstica definitiva y codificación clínica...'),
|
||||
onChanged: (v) => s.diagnDef = v ?? '',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProximaCitaTab(PatientStore s) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final now = DateTime.now();
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: now.add(const Duration(days: 7)),
|
||||
firstDate: now.subtract(const Duration(days: 30)),
|
||||
lastDate: now.add(const Duration(days: 365 * 3)),
|
||||
builder: (context, child) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: AppColors.primary,
|
||||
onPrimary: Colors.white,
|
||||
surface: Colors.white,
|
||||
),
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (picked != null) {
|
||||
s.fecProxcita = DateFormat('dd/MM/yyyy').format(picked);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.calendar_month_rounded, size: 18),
|
||||
label: const Text('Seleccionar Fecha de Cita', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
if (s.fecProxcita.isNotEmpty)
|
||||
TextButton.icon(
|
||||
onPressed: () => s.fecProxcita = '',
|
||||
icon: const Icon(Icons.clear_rounded, size: 16, color: AppColors.error),
|
||||
label: const Text('Quitar próxima cita', style: TextStyle(color: AppColors.error, fontWeight: FontWeight.w600)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.03),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(Icons.event_available_rounded, size: 22, color: AppColors.primary),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Control de Citas del Paciente',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.textMuted, fontWeight: FontWeight.w500),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
s.fecProxcita.isNotEmpty
|
||||
? 'Próxima cita programada: ${s.fecProxcita}'
|
||||
: 'Sin próxima cita programada',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (s.diasParaProximaCita != null) ...[
|
||||
const SizedBox(height: 14),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: s.diasParaProximaCita! < 0
|
||||
? AppColors.error.withValues(alpha: 0.1)
|
||||
: (s.diasParaProximaCita! == 0
|
||||
? AppColors.warning.withValues(alpha: 0.1)
|
||||
: AppColors.success.withValues(alpha: 0.1)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Tiempo restante para la cita: ${s.diasParaProximaCita} días',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: s.diasParaProximaCita! < 0
|
||||
? AppColors.error
|
||||
: (s.diasParaProximaCita! == 0 ? AppColors.warning : AppColors.success),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = widget.store;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Barra de Pestañas Moderna con Estilo Limpio
|
||||
Container(
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xFFF8FAFC),
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFE2E8F0)),
|
||||
),
|
||||
),
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
isScrollable: true,
|
||||
labelColor: AppColors.primary,
|
||||
unselectedLabelColor: AppColors.textSecondary,
|
||||
labelStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 12.5),
|
||||
unselectedLabelStyle: const TextStyle(fontWeight: FontWeight.w500, fontSize: 12.5),
|
||||
indicatorColor: AppColors.primary,
|
||||
indicatorWeight: 2.5,
|
||||
tabAlignment: TabAlignment.start,
|
||||
tabs: const [
|
||||
Tab(
|
||||
height: 38,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.notes_rounded, size: 15),
|
||||
SizedBox(width: 6),
|
||||
Text('Consultas'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
height: 38,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.history_edu_rounded, size: 15),
|
||||
SizedBox(width: 6),
|
||||
Text('Hist. enfermedad actual'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
height: 38,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.medical_information_outlined, size: 15),
|
||||
SizedBox(width: 6),
|
||||
Text('Antec. patológicos'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
height: 38,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.biotech_outlined, size: 15),
|
||||
SizedBox(width: 6),
|
||||
Text('Diagnósticos'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
height: 38,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.science_outlined, size: 15),
|
||||
SizedBox(width: 6),
|
||||
Text('Laboratorios'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
height: 38,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.medication_outlined, size: 15),
|
||||
SizedBox(width: 6),
|
||||
Text('Tratamiento'),
|
||||
],
|
||||
),
|
||||
),
|
||||
Tab(
|
||||
height: 38,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.event_outlined, size: 15),
|
||||
SizedBox(width: 6),
|
||||
Text('Próxima cita'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Observer(
|
||||
builder: (_) => TabBarView(
|
||||
controller: _tabController,
|
||||
children: [
|
||||
// 1. Consultas
|
||||
_buildClinicalTextArea(
|
||||
name: 'mot_cons',
|
||||
controller: _motConsController,
|
||||
focusNode: _motConsFocusNode,
|
||||
hintText: 'Registro de consultas clínicas, motivo y evolución...',
|
||||
dateLabel: s.fecUltcon.isNotEmpty ? 'Última consulta: ${s.fecUltcon}' : null,
|
||||
onInsertDate: () {
|
||||
_insertDateAtCursor(
|
||||
controller: _motConsController,
|
||||
focusNode: _motConsFocusNode,
|
||||
onUpdateStore: (v) => s.motCons = v,
|
||||
);
|
||||
},
|
||||
onChanged: (v) => s.motCons = v,
|
||||
),
|
||||
|
||||
// 2. Hist. enfermedad actual
|
||||
_buildClinicalTextArea(
|
||||
name: 'his_enf_act',
|
||||
controller: _hisEnfActController,
|
||||
focusNode: _hisEnfActFocusNode,
|
||||
hintText: 'Historia y cronología de la enfermedad actual...',
|
||||
onInsertDate: () {
|
||||
_insertDateAtCursor(
|
||||
controller: _hisEnfActController,
|
||||
focusNode: _hisEnfActFocusNode,
|
||||
onUpdateStore: (v) => s.hisEnfAct = v,
|
||||
);
|
||||
},
|
||||
onChanged: (v) => s.hisEnfAct = v,
|
||||
),
|
||||
|
||||
// 3. Antec. patológicos
|
||||
_buildAntecedentesTab(s),
|
||||
|
||||
// 4. Diagnósticos
|
||||
_buildDiagnosticosTab(s),
|
||||
|
||||
// 5. Laboratorios
|
||||
_buildClinicalTextArea(
|
||||
name: 'dat_lab',
|
||||
initialValue: s.datLab,
|
||||
hintText: 'Resultados de laboratorio, hemogramas, analíticas...',
|
||||
onChanged: (v) => s.datLab = v,
|
||||
),
|
||||
|
||||
// 6. Tratamiento
|
||||
_buildClinicalTextArea(
|
||||
name: 'tratamiento',
|
||||
initialValue: s.tratamiento,
|
||||
hintText: 'Plan terapéutico, recetas, dosis y recomendaciones...',
|
||||
onChanged: (v) => s.tratamiento = v,
|
||||
),
|
||||
|
||||
// 7. Próxima cita
|
||||
_buildProximaCitaTab(s),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
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 'package:mask_text_input_formatter/mask_text_input_formatter.dart';
|
||||
import 'package:toggle_switch/toggle_switch.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../store/patient_store.dart';
|
||||
|
||||
class PatientDemographicsForm extends StatelessWidget {
|
||||
/// Altura estricta y compartida para todos los elementos de la ficha demográfica:
|
||||
/// los 8 campos de texto, las 3 secciones de toggle (sexo, seguro, estado civil)
|
||||
/// y el botón Guardar Paciente siempre tendrán exactamente la misma altura.
|
||||
static const double unifiedRowHeight = 46.0;
|
||||
|
||||
final PatientStore store;
|
||||
final GlobalKey<FormBuilderState> formKey;
|
||||
final VoidCallback onSave;
|
||||
|
||||
const PatientDemographicsForm({
|
||||
super.key,
|
||||
required this.store,
|
||||
required this.formKey,
|
||||
required this.onSave,
|
||||
});
|
||||
|
||||
InputDecoration _cleanDecoration({
|
||||
required String label,
|
||||
IconData? icon,
|
||||
bool isRequired = false,
|
||||
String? hintText,
|
||||
bool readOnly = false,
|
||||
}) {
|
||||
return InputDecoration(
|
||||
labelText: isRequired ? '$label *' : label,
|
||||
hintText: hintText,
|
||||
isDense: true,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.auto,
|
||||
filled: true,
|
||||
fillColor: readOnly ? const Color(0xFFF1F5F9) : Colors.white,
|
||||
prefixIcon: icon != null
|
||||
? Icon(icon, size: 16, color: isRequired ? AppColors.primary : AppColors.textMuted)
|
||||
: null,
|
||||
prefixIconConstraints: const BoxConstraints(minWidth: 34, minHeight: 34),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 11, vertical: 14),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.primary, width: 1.5),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.error),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.error, width: 1.5),
|
||||
),
|
||||
labelStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isRequired ? AppColors.primary : AppColors.textSecondary,
|
||||
),
|
||||
hintStyle: const TextStyle(fontSize: 11.5, color: Color(0xFF94A3B8)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactSelector({
|
||||
required BuildContext context,
|
||||
required String title,
|
||||
required IconData titleIcon,
|
||||
required Widget child,
|
||||
bool isRequired = false,
|
||||
}) {
|
||||
return Container(
|
||||
height: unifiedRowHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(titleIcon, size: 14, color: isRequired ? AppColors.primary : AppColors.textMuted),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
isRequired ? '$title *' : title,
|
||||
style: TextStyle(
|
||||
fontSize: 11.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: isRequired ? AppColors.primary : AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Flexible(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = store;
|
||||
|
||||
final cedulaFormatter = MaskTextInputFormatter(
|
||||
mask: '###-#######-#',
|
||||
filter: {'#': RegExp(r'[0-9]')},
|
||||
);
|
||||
|
||||
final telefonoFormatter = MaskTextInputFormatter(
|
||||
mask: '###-###-####',
|
||||
filter: {'#': RegExp(r'[0-9]')},
|
||||
);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
border: Border(
|
||||
bottom: BorderSide(color: Color(0xFFE2E8F0), width: 1.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Sub-bloque 1 (Columna 1): Fecha de reg, Nombre, Dirección, Ocupación
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: unifiedRowHeight,
|
||||
child: FormBuilderTextField(
|
||||
name: 'fec_reg',
|
||||
initialValue: s.fecReg,
|
||||
readOnly: true,
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||
decoration: _cleanDecoration(
|
||||
label: 'Fecha de Registro',
|
||||
icon: Icons.calendar_today_rounded,
|
||||
readOnly: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: unifiedRowHeight,
|
||||
child: FormBuilderTextField(
|
||||
name: 'nombre',
|
||||
initialValue: s.nombre,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
||||
decoration: _cleanDecoration(
|
||||
label: 'Nombre',
|
||||
icon: Icons.person_outline_rounded,
|
||||
isRequired: true,
|
||||
hintText: 'Ej. Juan Alberto',
|
||||
),
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(errorText: 'Requerido'),
|
||||
FormBuilderValidators.minLength(2, errorText: 'Mínimo 2 letras'),
|
||||
]),
|
||||
onChanged: (v) => s.nombre = v ?? '',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: unifiedRowHeight,
|
||||
child: FormBuilderTextField(
|
||||
name: 'direccion',
|
||||
initialValue: s.direccion,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: _cleanDecoration(
|
||||
label: 'Dirección',
|
||||
icon: Icons.location_on_outlined,
|
||||
hintText: 'Calle, número o sector',
|
||||
),
|
||||
onChanged: (v) => s.direccion = v ?? '',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: unifiedRowHeight,
|
||||
child: FormBuilderTextField(
|
||||
name: 'ocupacion',
|
||||
initialValue: s.ocupacion,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: _cleanDecoration(
|
||||
label: 'Ocupación',
|
||||
icon: Icons.work_outline_rounded,
|
||||
hintText: 'Profesión u oficio',
|
||||
),
|
||||
onChanged: (v) => s.ocupacion = v ?? '',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Sub-bloque 2 (Columna 2): Cédula o Pasaporte, Apellido, Teléfono, Edad
|
||||
Expanded(
|
||||
flex: 7,
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: unifiedRowHeight,
|
||||
child: FormBuilderTextField(
|
||||
name: 'cedula',
|
||||
initialValue: s.cedula,
|
||||
inputFormatters: [cedulaFormatter],
|
||||
keyboardType: TextInputType.number,
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w500),
|
||||
decoration: _cleanDecoration(
|
||||
label: 'Cédula o Pasaporte',
|
||||
icon: Icons.badge_outlined,
|
||||
hintText: '001-0000000-0',
|
||||
),
|
||||
onChanged: (v) => s.cedula = v ?? '',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: unifiedRowHeight,
|
||||
child: FormBuilderTextField(
|
||||
name: 'apellido',
|
||||
initialValue: s.apellido,
|
||||
textCapitalization: TextCapitalization.words,
|
||||
style: const TextStyle(fontSize: 12, fontWeight: FontWeight.w600),
|
||||
decoration: _cleanDecoration(
|
||||
label: 'Apellido',
|
||||
icon: Icons.badge_rounded,
|
||||
isRequired: true,
|
||||
hintText: 'Ej. Pérez Gómez',
|
||||
),
|
||||
validator: FormBuilderValidators.compose([
|
||||
FormBuilderValidators.required(errorText: 'Requerido'),
|
||||
FormBuilderValidators.minLength(2, errorText: 'Mínimo 2 letras'),
|
||||
]),
|
||||
onChanged: (v) => s.apellido = v ?? '',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: unifiedRowHeight,
|
||||
child: FormBuilderTextField(
|
||||
name: 'telefono',
|
||||
initialValue: s.telefono,
|
||||
inputFormatters: [telefonoFormatter],
|
||||
keyboardType: TextInputType.phone,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: _cleanDecoration(
|
||||
label: 'Teléfono',
|
||||
icon: Icons.phone_outlined,
|
||||
hintText: '809-555-0101',
|
||||
),
|
||||
onChanged: (v) => s.telefono = v ?? '',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: unifiedRowHeight,
|
||||
child: FormBuilderTextField(
|
||||
name: 'edad',
|
||||
initialValue: s.edad,
|
||||
keyboardType: TextInputType.number,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
decoration: _cleanDecoration(
|
||||
label: 'Edad',
|
||||
icon: Icons.cake_outlined,
|
||||
hintText: 'Años',
|
||||
),
|
||||
onChanged: (v) => s.edad = v ?? '',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// Sub-bloque 3 (Columna 3): Sexo, Asegurado, Estado Civil, Botón Guardar
|
||||
Expanded(
|
||||
flex: 10,
|
||||
child: Column(
|
||||
children: [
|
||||
_buildCompactSelector(
|
||||
context: context,
|
||||
title: 'Sexo',
|
||||
titleIcon: Icons.wc_rounded,
|
||||
isRequired: true,
|
||||
child: FormBuilderField<String>(
|
||||
name: 'sexo',
|
||||
initialValue: s.sexo,
|
||||
validator: FormBuilderValidators.required(errorText: 'Requerido'),
|
||||
builder: (FormFieldState<String> field) {
|
||||
final currentVal = field.value ?? s.sexo;
|
||||
final initialIndex = currentVal.toLowerCase().contains('fem') ? 1 : 0;
|
||||
return ToggleSwitch(
|
||||
key: ValueKey('sexo_$currentVal'),
|
||||
customWidths: const [108.0, 108.0],
|
||||
minHeight: 28.0,
|
||||
cornerRadius: 6.0,
|
||||
radiusStyle: true,
|
||||
fontSize: 10.5,
|
||||
iconSize: 13.0,
|
||||
centerText: true,
|
||||
initialLabelIndex: initialIndex,
|
||||
totalSwitches: 2,
|
||||
labels: const ['Masculino', 'Femenino'],
|
||||
icons: const [Icons.male_rounded, Icons.female_rounded],
|
||||
activeBgColor: const [AppColors.primary],
|
||||
activeFgColor: Colors.white,
|
||||
inactiveBgColor: const Color(0xFFE2E8F0),
|
||||
inactiveFgColor: AppColors.textSecondary,
|
||||
animate: true,
|
||||
curve: Curves.easeInOut,
|
||||
onToggle: (index) {
|
||||
final val = (index == 1) ? 'Femenino' : 'Masculino';
|
||||
field.didChange(val);
|
||||
s.sexo = val;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildCompactSelector(
|
||||
context: context,
|
||||
title: 'Seguro',
|
||||
titleIcon: Icons.health_and_safety_outlined,
|
||||
isRequired: true,
|
||||
child: FormBuilderField<String>(
|
||||
name: 'seguro',
|
||||
initialValue: s.seguro,
|
||||
validator: FormBuilderValidators.required(errorText: 'Requerido'),
|
||||
builder: (FormFieldState<String> field) {
|
||||
final currentVal = field.value ?? s.seguro;
|
||||
final cleanVal = currentVal.trim().toLowerCase();
|
||||
final isNo = cleanVal == 'no' || cleanVal.contains('sin seguro') || cleanVal.contains('no asegurado');
|
||||
final initialIndex = isNo ? 1 : 0;
|
||||
return ToggleSwitch(
|
||||
key: ValueKey('seguro_$currentVal'),
|
||||
customWidths: const [116.0, 116.0],
|
||||
minHeight: 28.0,
|
||||
cornerRadius: 6.0,
|
||||
radiusStyle: true,
|
||||
fontSize: 10.5,
|
||||
iconSize: 12.0,
|
||||
centerText: true,
|
||||
initialLabelIndex: initialIndex,
|
||||
totalSwitches: 2,
|
||||
labels: const ['Con Seguro', 'Sin Seguro'],
|
||||
icons: const [Icons.check_rounded, Icons.close_rounded],
|
||||
activeBgColor: const [AppColors.primary],
|
||||
activeFgColor: Colors.white,
|
||||
inactiveBgColor: const Color(0xFFE2E8F0),
|
||||
inactiveFgColor: AppColors.textSecondary,
|
||||
animate: true,
|
||||
curve: Curves.easeInOut,
|
||||
onToggle: (index) {
|
||||
final val = (index == 1) ? 'Sin Seguro' : 'Con Seguro';
|
||||
field.didChange(val);
|
||||
s.seguro = val;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildCompactSelector(
|
||||
context: context,
|
||||
title: 'Estado Civil',
|
||||
titleIcon: Icons.favorite_border_rounded,
|
||||
child: FormBuilderField<String>(
|
||||
name: 'est_civil',
|
||||
initialValue: s.estCivil,
|
||||
builder: (FormFieldState<String> field) {
|
||||
const options = ['Casado', 'Soltero', 'Otros'];
|
||||
final currentVal = field.value ?? s.estCivil;
|
||||
int initialIndex = 1;
|
||||
if (currentVal.toLowerCase().contains('casad')) {
|
||||
initialIndex = 0;
|
||||
} else if (currentVal.toLowerCase().contains('otro')) {
|
||||
initialIndex = 2;
|
||||
}
|
||||
return ToggleSwitch(
|
||||
key: ValueKey('est_civil_$currentVal'),
|
||||
customWidths: const [86.0, 90.0, 76.0],
|
||||
minHeight: 28.0,
|
||||
cornerRadius: 6.0,
|
||||
radiusStyle: true,
|
||||
fontSize: 10.5,
|
||||
iconSize: 11.5,
|
||||
centerText: true,
|
||||
initialLabelIndex: initialIndex,
|
||||
totalSwitches: 3,
|
||||
labels: options,
|
||||
icons: const [Icons.people_alt_outlined, Icons.person_outline_rounded, Icons.more_horiz_rounded],
|
||||
activeBgColor: const [AppColors.primary],
|
||||
activeFgColor: Colors.white,
|
||||
inactiveBgColor: const Color(0xFFE2E8F0),
|
||||
inactiveFgColor: AppColors.textSecondary,
|
||||
animate: true,
|
||||
curve: Curves.easeInOut,
|
||||
onToggle: (index) {
|
||||
final selected = options[index ?? 1];
|
||||
field.didChange(selected);
|
||||
s.estCivil = selected;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(
|
||||
height: unifiedRowHeight,
|
||||
width: double.infinity,
|
||||
child: Observer(
|
||||
builder: (_) => ElevatedButton(
|
||||
onPressed: s.isSaving ? null : onSave,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF00A65A),
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 1,
|
||||
shadowColor: const Color(0xFF00A65A).withValues(alpha: 0.35),
|
||||
padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: s.isSaving
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.check_circle_outline_rounded, size: 16),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
s.estadoEspera == 'en_consulta'
|
||||
? 'Finalizar Consulta'
|
||||
: (s.isEditing ? 'Guardar Cambios' : 'Guardar Paciente'),
|
||||
style: const TextStyle(
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../core/theme/app_colors.dart';
|
||||
import '../store/patient_store.dart';
|
||||
|
||||
class SecretaryAppointmentSection extends StatelessWidget {
|
||||
final PatientStore store;
|
||||
|
||||
const SecretaryAppointmentSection({
|
||||
super.key,
|
||||
required this.store,
|
||||
});
|
||||
|
||||
void _setDate(DateTime date) {
|
||||
store.fecProxcita = DateFormat('dd/MM/yyyy').format(date);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final now = DateTime.now();
|
||||
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 14),
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.02),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 1. Título de la sección
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF00A65A).withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.event_available_rounded,
|
||||
color: Color(0xFF00A65A),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Gestión de Citas y Turno del Paciente',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.textPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
'Coordine la próxima visita o turno del paciente en el consultorio',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.textMuted),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// 2. Estado actual de la cita
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
store.fecProxcita.isNotEmpty
|
||||
? Icons.check_circle_rounded
|
||||
: Icons.info_outline_rounded,
|
||||
color: store.fecProxcita.isNotEmpty
|
||||
? const Color(0xFF00A65A)
|
||||
: AppColors.textMuted,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
store.fecProxcita.isNotEmpty
|
||||
? 'Próxima Cita Programada: ${store.fecProxcita}'
|
||||
: 'Sin fecha de cita programada',
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: store.fecProxcita.isNotEmpty
|
||||
? AppColors.textPrimary
|
||||
: AppColors.textSecondary,
|
||||
),
|
||||
),
|
||||
if (store.diasParaProximaCita != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
store.diasParaProximaCita! < 0
|
||||
? 'La cita programada venció hace ${store.diasParaProximaCita!.abs()} días.'
|
||||
: (store.diasParaProximaCita! == 0
|
||||
? '¡Cita pautada para el día de HOY!'
|
||||
: 'Faltan ${store.diasParaProximaCita} días para la cita.'),
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: store.diasParaProximaCita! < 0
|
||||
? AppColors.error
|
||||
: (store.diasParaProximaCita! == 0
|
||||
? const Color(0xFFE65100)
|
||||
: const Color(0xFF00A65A)),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
if (store.fecProxcita.isNotEmpty)
|
||||
TextButton.icon(
|
||||
onPressed: () => store.fecProxcita = '',
|
||||
icon: const Icon(Icons.clear_rounded, size: 16, color: AppColors.error),
|
||||
label: const Text(
|
||||
'Quitar Cita',
|
||||
style: TextStyle(color: AppColors.error, fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 3. Botones para selección de fecha
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
ElevatedButton.icon(
|
||||
onPressed: () async {
|
||||
final picked = await showDatePicker(
|
||||
context: context,
|
||||
initialDate: now.add(const Duration(days: 7)),
|
||||
firstDate: now.subtract(const Duration(days: 30)),
|
||||
lastDate: now.add(const Duration(days: 365 * 3)),
|
||||
builder: (context, child) {
|
||||
return Theme(
|
||||
data: Theme.of(context).copyWith(
|
||||
colorScheme: const ColorScheme.light(
|
||||
primary: Color(0xFF00A65A),
|
||||
onPrimary: Colors.white,
|
||||
surface: Colors.white,
|
||||
),
|
||||
),
|
||||
child: child!,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (picked != null) {
|
||||
_setDate(picked);
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.calendar_month_rounded, size: 16),
|
||||
label: const Text('Elegir en Calendario', style: TextStyle(fontWeight: FontWeight.w600, fontSize: 12.5)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF00A65A),
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () => _setDate(now),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
side: const BorderSide(color: Color(0xFFCBD5E1)),
|
||||
),
|
||||
child: const Text('Para Hoy', style: TextStyle(fontSize: 12, color: AppColors.textPrimary)),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () => _setDate(now.add(const Duration(days: 1))),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
side: const BorderSide(color: Color(0xFFCBD5E1)),
|
||||
),
|
||||
child: const Text('Mañana', style: TextStyle(fontSize: 12, color: AppColors.textPrimary)),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () => _setDate(now.add(const Duration(days: 7))),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
side: const BorderSide(color: Color(0xFFCBD5E1)),
|
||||
),
|
||||
child: const Text('En 7 días', style: TextStyle(fontSize: 12, color: AppColors.textPrimary)),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () => _setDate(now.add(const Duration(days: 15))),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
side: const BorderSide(color: Color(0xFFCBD5E1)),
|
||||
),
|
||||
child: const Text('En 15 días', style: TextStyle(fontSize: 12, color: AppColors.textPrimary)),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () => _setDate(now.add(const Duration(days: 30))),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
side: const BorderSide(color: Color(0xFFCBD5E1)),
|
||||
),
|
||||
child: const Text('En 1 mes', style: TextStyle(fontSize: 12, color: AppColors.textPrimary)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// 4. Nota de Confidencialidad
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEFF6FF),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFFBFDBFE)),
|
||||
),
|
||||
child: const Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.shield_outlined, color: Color(0xFF2563EB), size: 18),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Privacidad de Datos: Los diagnósticos, notas de consulta y prescripciones médicas son de gestión exclusiva del médico tratante y se mantienen protegidos.',
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
color: Color(0xFF1E40AF),
|
||||
height: 1.35,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user