first commit

This commit is contained in:
=
2026-09-21 16:33:51 -04:00
commit a899479dc9
204 changed files with 21132 additions and 0 deletions
@@ -0,0 +1,175 @@
class PatientModel {
final int? id;
final String nombre;
final String apellido;
final String telefono;
final String direccion;
final String cedula;
final String ocupacion;
final String edad;
final String fecUltcon;
final String fecReg;
final String sexo;
final String seguro;
final String estCivil;
final String motCons;
final String hisEnfAct;
final String datPadre;
final String datEsp;
final String datHijos;
final String datOtrosnp;
final String datAlerg;
final String datTran;
final String datOtrosp;
final String metDiagnRad;
final String datLab;
final String diagnDesc;
final String diagnDef;
final String fecProxcita;
final String tratamiento;
final String datHer;
final String datMad;
final String datNi;
final String datAdo;
final String datAdu;
final String datAc;
final String datCir;
final String datJuv;
final String estadoEspera;
const PatientModel({
this.id,
this.nombre = '',
this.apellido = '',
this.telefono = '',
this.direccion = '',
this.cedula = '',
this.ocupacion = '',
this.edad = '',
this.fecUltcon = '',
this.fecReg = '',
this.sexo = 'Masculino',
this.seguro = 'Sí',
this.estCivil = 'Soltero',
this.motCons = '',
this.hisEnfAct = '',
this.datPadre = '',
this.datEsp = '',
this.datHijos = '',
this.datOtrosnp = '',
this.datAlerg = '',
this.datTran = '',
this.datOtrosp = '',
this.metDiagnRad = '',
this.datLab = '',
this.diagnDesc = '',
this.diagnDef = '',
this.fecProxcita = '',
this.tratamiento = '',
this.datHer = '',
this.datMad = '',
this.datNi = '',
this.datAdo = '',
this.datAdu = '',
this.datAc = '',
this.datCir = '',
this.datJuv = '',
this.estadoEspera = 'no_presente',
});
String get fullName => '$nombre $apellido'.trim();
factory PatientModel.fromJson(Map<String, dynamic> json) {
int? parsedId;
if (json['id'] is int) {
parsedId = json['id'] as int;
} else if (json['id'] != null) {
parsedId = int.tryParse(json['id'].toString());
}
return PatientModel(
id: parsedId,
nombre: json['nombre']?.toString().trim() ?? '',
apellido: json['apellido']?.toString().trim() ?? '',
telefono: json['telefono']?.toString().trim() ?? '',
direccion: json['direccion']?.toString().trim() ?? '',
cedula: json['cedula']?.toString().trim() ?? '',
ocupacion: json['ocupacion']?.toString().trim() ?? '',
edad: json['edad']?.toString().trim() ?? '',
fecUltcon: json['fec_ultcon']?.toString().trim() ?? '',
fecReg: json['fec_reg']?.toString().trim() ?? '',
sexo: json['sexo']?.toString().trim() ?? 'Masculino',
seguro: json['seguro']?.toString().trim() ?? 'Sí',
estCivil: json['est_civil']?.toString().trim() ?? 'Soltero',
motCons: json['mot_cons']?.toString().trim() ?? '',
hisEnfAct: json['his_enf_act']?.toString().trim() ?? '',
datPadre: json['dat_padre']?.toString().trim() ?? '',
datEsp: json['dat_esp']?.toString().trim() ?? '',
datHijos: json['dat_hijos']?.toString().trim() ?? '',
datOtrosnp: json['dat_otrosnp']?.toString().trim() ?? '',
datAlerg: json['dat_alerg']?.toString().trim() ?? '',
datTran: json['dat_tran']?.toString().trim() ?? '',
datOtrosp: json['dat_otrosp']?.toString().trim() ?? '',
metDiagnRad: json['met_diagn_rad']?.toString().trim() ?? '',
datLab: json['dat_lab']?.toString().trim() ?? '',
diagnDesc: json['diagn_desc']?.toString().trim() ?? '',
diagnDef: json['diagn_def']?.toString().trim() ?? '',
fecProxcita: json['fec_proxcita']?.toString().trim() ?? '',
tratamiento: json['tratamiento']?.toString().trim() ?? '',
datHer: json['dat_her']?.toString().trim() ?? '',
datMad: json['dat_mad']?.toString().trim() ?? '',
datNi: json['dat_ni']?.toString().trim() ?? '',
datAdo: json['dat_ado']?.toString().trim() ?? '',
datAdu: json['dat_adu']?.toString().trim() ?? '',
datAc: json['dat_ac']?.toString().trim() ?? '',
datCir: json['dat_cir']?.toString().trim() ?? '',
datJuv: json['dat_juv']?.toString().trim() ?? '',
estadoEspera: json['estado_espera']?.toString().trim() ?? 'no_presente',
);
}
Map<String, dynamic> toJson({bool includeId = false}) {
final data = <String, dynamic>{
'nombre': nombre,
'apellido': apellido,
'telefono': telefono,
'direccion': direccion,
'cedula': cedula,
'ocupacion': ocupacion,
'edad': edad,
'fec_ultcon': fecUltcon,
'fec_reg': fecReg,
'sexo': sexo,
'seguro': seguro,
'est_civil': estCivil,
'mot_cons': motCons,
'his_enf_act': hisEnfAct,
'dat_padre': datPadre,
'dat_esp': datEsp,
'dat_hijos': datHijos,
'dat_otrosnp': datOtrosnp,
'dat_alerg': datAlerg,
'dat_tran': datTran,
'dat_otrosp': datOtrosp,
'met_diagn_rad': metDiagnRad,
'dat_lab': datLab,
'diagn_desc': diagnDesc,
'diagn_def': diagnDef,
'fec_proxcita': fecProxcita,
'tratamiento': tratamiento,
'dat_her': datHer,
'dat_mad': datMad,
'dat_ni': datNi,
'dat_ado': datAdo,
'dat_adu': datAdu,
'dat_ac': datAc,
'dat_cir': datCir,
'dat_juv': datJuv,
'estado_espera': estadoEspera,
};
if (includeId && id != null) {
data['id'] = id;
}
return data;
}
}
@@ -0,0 +1,64 @@
import 'package:dio/dio.dart';
import '../../../../core/network/api_client.dart';
import '../models/patient_model.dart';
class PatientService {
final ApiClient apiClient;
PatientService({ApiClient? client}) : apiClient = client ?? ApiClient();
Future<PatientModel> getPatientById(int id) async {
try {
final response = await apiClient.dio.get('/items/pacientes/$id');
if (response.statusCode == 200 && response.data != null) {
final data = response.data['data'] as Map<String, dynamic>;
return PatientModel.fromJson(data);
}
throw Exception('No se encontró el paciente con ID $id');
} on DioException catch (e) {
final serverMessage = e.response?.data?['errors']?[0]?['message'] ?? e.message;
throw Exception('Error al obtener datos del paciente: $serverMessage');
} catch (e) {
throw Exception('Error al obtener datos del paciente: $e');
}
}
Future<PatientModel> createPatient(PatientModel model) async {
try {
final payload = model.toJson(includeId: false);
final response = await apiClient.dio.post(
'/items/pacientes',
data: payload,
);
if ((response.statusCode == 200 || response.statusCode == 201) && response.data != null) {
final data = response.data['data'] as Map<String, dynamic>;
return PatientModel.fromJson(data);
}
throw Exception('Error en el servidor al registrar el paciente');
} on DioException catch (e) {
final serverMessage = e.response?.data?['errors']?[0]?['message'] ?? e.message;
throw Exception('Error al registrar paciente: $serverMessage');
} catch (e) {
throw Exception('Error al registrar paciente: $e');
}
}
Future<PatientModel> updatePatient(int id, Map<String, dynamic> updates) async {
try {
final response = await apiClient.dio.patch(
'/items/pacientes/$id',
data: updates,
);
if (response.statusCode == 200 && response.data != null) {
final data = response.data['data'] as Map<String, dynamic>;
return PatientModel.fromJson(data);
}
throw Exception('Error en el servidor al actualizar el paciente');
} on DioException catch (e) {
final serverMessage = e.response?.data?['errors']?[0]?['message'] ?? e.message;
throw Exception('Error al actualizar paciente: $serverMessage');
} catch (e) {
throw Exception('Error al actualizar paciente: $e');
}
}
}
@@ -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,
),
),
),
],
),
),
],
),
);
},
);
}
}