mi-proyecto/app/templates/admin_clinical_records.html

382 lines
70 KiB
HTML

{% extends 'base.html' %}
{% block content %}
<style>
.clinical-shell{display:flex;flex-direction:column;gap:1rem}.clinical-hero{border:1px solid #e5ecf6;background:linear-gradient(135deg,#ffffff 0%,#f7fbff 100%);border-radius:24px;padding:1rem 1.1rem;box-shadow:0 16px 40px rgba(15,23,42,.06)}
.clinical-grid{display:grid;grid-template-columns:1.1fr 1fr;gap:1rem}.clinical-card{border:1px solid #e8eef6;border-radius:24px;background:#fff;box-shadow:0 14px 36px rgba(15,23,42,.05);overflow:hidden}.clinical-head{padding:1rem 1.15rem;border-bottom:1px solid #edf2f7;display:flex;justify-content:space-between;gap:1rem;align-items:flex-start}.clinical-body{padding:1rem 1.15rem}
.clinical-chip{display:inline-flex;align-items:center;gap:.45rem;border-radius:999px;padding:.38rem .72rem;background:#eef6ff;color:#1857a4;font-size:.82rem;font-weight:700}.clinical-chip.warn{background:#fff5df;color:#9a6700}.clinical-chip.success{background:#e8faf0;color:#166534}.clinical-chip.danger{background:#ffecec;color:#b42318}
.clinical-summary-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.85rem}.clinical-summary-grid textarea,.clinical-summary-grid input,.clinical-summary-grid select{border-radius:15px!important}
.clinical-patient-table th{width:180px;color:#64748b;font-weight:700}.clinical-kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.clinical-kpi{border:1px solid #e7edf6;background:#f8fbff;border-radius:18px;padding:.85rem .95rem}.clinical-kpi small{display:block;color:#64748b}.clinical-kpi strong{display:block;color:#0f172a;font-size:1rem}
.episode-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(310px,1fr));gap:.85rem}.episode-card{border:1px solid #e7edf5;background:linear-gradient(180deg,#fff 0%,#fbfdff 100%);border-radius:22px;padding:1rem;box-shadow:0 12px 28px rgba(15,23,42,.05)}.episode-members{display:flex;flex-wrap:wrap;gap:.4rem}.episode-member{display:inline-flex;align-items:center;gap:.35rem;padding:.35rem .6rem;border-radius:999px;background:#f4f7fb;color:#334155;font-size:.78rem;font-weight:700}
.timeline-toolbar{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:.75rem}.timeline-list{display:flex;flex-direction:column;gap:1rem}.entry-card{border:1px solid #e8eef6;border-radius:24px;background:#fff;box-shadow:0 14px 34px rgba(15,23,42,.05);overflow:hidden}.entry-head{padding:1rem 1.1rem;border-bottom:1px solid #edf2f7;display:flex;justify-content:space-between;gap:1rem;align-items:flex-start;flex-wrap:wrap}.entry-body{padding:1rem 1.1rem;display:grid;grid-template-columns:1.25fr 1fr;gap:1rem}.soft-block{border:1px solid #e7edf5;background:#f9fbff;border-radius:18px;padding:.9rem 1rem}.soft-block h6{font-weight:800;color:#0f172a;margin-bottom:.35rem}.soft-block div,.soft-block li{color:#475569}.vitals-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:.5rem}.vital-box{border:1px solid #e7edf5;background:#fff;border-radius:15px;padding:.55rem .7rem}.vital-box small{display:block;color:#64748b}.vital-box strong{color:#0f172a}
.attachment-list{display:flex;flex-wrap:wrap;gap:.55rem}.attachment-visibility-form{display:inline-flex;align-items:center;gap:.35rem;flex-wrap:wrap}.attachment-flag{display:inline-flex;align-items:center;gap:.35rem;padding:.25rem .55rem;border-radius:999px;background:#eef6ff;color:#1857a4;font-size:.74rem;font-weight:700}.attachment-pill{display:inline-flex;align-items:center;gap:.45rem;padding:.45rem .72rem;border-radius:999px;background:#f6f8fb;color:#334155;text-decoration:none;border:1px solid #e5e7eb}.action-row{display:flex;flex-wrap:wrap;gap:.5rem}.code-preview{border:1px dashed #cbd7e8;background:#f8fbff;border-radius:16px;padding:.75rem .9rem;display:flex;flex-direction:column;gap:.35rem}.policy-list{margin:0;padding-left:1rem}.policy-list li{margin-bottom:.35rem;color:#475569}
.template-picker-grid{display:grid;grid-template-columns:280px 1fr;gap:1rem}.template-list{max-height:420px;overflow:auto;display:flex;flex-direction:column;gap:.55rem}.template-item{border:1px solid #e7edf5;background:#fff;border-radius:18px;padding:.75rem .85rem;text-align:left;width:100%;transition:.16s}.template-item:hover,.template-item.active{border-color:#20a464;box-shadow:0 12px 28px rgba(32,164,100,.12);transform:translateY(-1px)}.template-item small{display:block;color:#64748b}.template-preview{border:1px dashed #cbd7e8;background:#f8fbff;border-radius:18px;padding:1rem;min-height:180px}.apex-clinical-tip{border:1px solid #dbeafe;background:#eff6ff;color:#1e3a8a;border-radius:16px;padding:.7rem .9rem}
@media (max-width:1199.98px){.clinical-grid,.entry-body{grid-template-columns:1fr}.timeline-toolbar{grid-template-columns:repeat(3,minmax(0,1fr))}}
@media (max-width:767.98px){.clinical-summary-grid,.vitals-grid,.timeline-toolbar{grid-template-columns:1fr}}
</style>
<div class="page-toolbar">
<div>
<h1 class="h3 mb-1">Historia clínica electrónica avanzada</h1>
<p class="text-muted mb-0">Legajo longitudinal, episodios de atención, equipo tratante, firma institucional avanzada y exportación FHIR local.</p>
</div>
<div class="d-flex gap-2 flex-wrap">
{% if record %}<a class="btn btn-outline-primary" href="{{ url_for('clinical_record_fhir', record_id=record.id) }}"><i class="bi bi-diagram-3 me-1"></i> Exportar FHIR</a>{% endif %}
{% if patient %}<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#episodeModal"><i class="bi bi-folder-plus me-1"></i> Nuevo episodio</button>{% endif %}
{% if patient %}<button class="btn btn-outline-primary" data-bs-toggle="modal" data-bs-target="#episodeTemplatePickerModal"><i class="bi bi-folder-symlink me-1"></i> Episodio desde template</button>{% endif %}
{% if patient %}<button class="btn btn-outline-success" data-bs-toggle="modal" data-bs-target="#clinicalTemplatePickerModal"><i class="bi bi-file-earmark-medical me-1"></i> Evolución desde template</button>{% endif %}
{% if patient %}<button class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#clinicalEntryModal"><i class="bi bi-plus-circle me-1"></i> {% if editing_entry %}Editar borrador{% else %}Nueva evolución{% endif %}</button>{% endif %}
</div>
</div>
<div class="clinical-shell">
<div class="clinical-card"><div class="clinical-body"><form method="get" class="row g-3 align-items-end"><div class="col-lg-4"><label class="form-label">Buscar por DNI</label><input class="form-control" name="dni" value="{{ query_dni }}" placeholder="Documento del paciente"></div><div class="col-lg-2 d-flex gap-2"><button class="btn btn-outline-primary flex-fill">Buscar</button><a class="btn btn-outline-secondary" href="{{ url_for('admin_clinical_records') }}">Limpiar</a></div></form></div></div>
{% if patient and record %}
<div class="clinical-hero">
<div class="d-flex justify-content-between align-items-start gap-3 flex-wrap">
<div>
<div class="fw-bold mb-1"><i class="bi bi-shield-lock me-2"></i>Gobierno del dato clínico</div>
<ul class="policy-list small">
<li>Acceso mínimo necesario con matriz por episodio y trazabilidad de visualización.</li>
<li>Firma institucional avanzada con hash, sello temporal y rastro de revisión. Integración certificada externa pendiente.</li>
<li>Exportación FHIR local para interoperabilidad estructurada y futura integración entre sistemas.</li>
</ul>
</div>
<div class="d-flex gap-2 flex-wrap">
<span class="clinical-chip"><i class="bi bi-folder2-open"></i> Legajo {{ record.legajo_number }}</span>
<span class="clinical-chip"><i class="bi bi-lock"></i> {{ record.confidentiality_level or 'Restringido' }}</span>
<span class="clinical-chip warn"><i class="bi bi-archive"></i> Retención hasta {{ record.retention_until.strftime('%d/%m/%Y') if record.retention_until else '—' }}</span>
</div>
</div>
</div>
<div class="clinical-grid">
<div class="clinical-card"><div class="clinical-head"><div><div class="h5 mb-1">Resumen clínico fijo</div><div class="text-muted small">Capa 1 · datos críticos, antecedentes, medicación y alertas.</div></div></div><div class="clinical-body">
<div class="clinical-kpis mb-3">
<div class="clinical-kpi"><small>Paciente</small><strong>{{ patient.nombre_completo }}</strong></div>
<div class="clinical-kpi"><small>DNI</small><strong>{{ patient.documento }}</strong></div>
<div class="clinical-kpi"><small>Obra social</small><strong>{{ patient.obra_social.denominacion if patient.obra_social else 'Particular' }}</strong></div>
<div class="clinical-kpi"><small>Entradas firmadas</small><strong>{{ entries|selectattr('entry_status','equalto','Firmado')|list|length }}</strong></div>
</div>
<table class="table table-sm align-middle clinical-patient-table mb-0"><tr><th>Fecha nac.</th><td>{{ patient.fecha_nacimiento or '—' }}</td></tr><tr><th>Género</th><td>{{ patient.genero or '—' }}</td></tr><tr><th>Teléfono</th><td>{{ patient.telefono or '—' }}</td></tr><tr><th>Email</th><td>{{ patient.email or '—' }}</td></tr><tr><th>Domicilio</th><td>{{ patient.domicilio_completo or '—' }}</td></tr><tr><th>Contacto</th><td>{{ patient.nombre_contacto or '—' }}{% if patient.telefono_contacto %}<br><span class="small text-muted">{{ patient.telefono_contacto }}</span>{% endif %}</td></tr></table>
</div></div>
<div class="clinical-card"><div class="clinical-head"><div><div class="h5 mb-1">Completar resumen clínico</div><div class="text-muted small">Alergias, problemas activos, medicación y datos sensibles del legajo.</div></div></div><div class="clinical-body">
<form method="post" class="clinical-summary-grid"><input type="hidden" name="action" value="update_summary"><input type="hidden" name="patient_id" value="{{ patient.id }}">
<div><label class="form-label">Confidencialidad</label><select class="form-select" name="confidentiality_level"><option value="Paciente" {% if record.confidentiality_level=='Paciente' %}selected{% endif %}>Paciente</option><option value="Institucional" {% if record.confidentiality_level=='Institucional' %}selected{% endif %}>Institucional</option><option value="Restringido" {% if record.confidentiality_level!='Paciente' and record.confidentiality_level!='Institucional' %}selected{% endif %}>Restringido</option></select></div>
<div><label class="form-label">Grupo sanguíneo</label><input class="form-control" name="blood_type" value="{{ summary_data.blood_type }}"></div>
<div><label class="form-label">Alergias</label><textarea class="form-control" name="allergies" rows="3">{{ summary_data.allergies }}</textarea></div>
<div><label class="form-label">Medicaciones habituales</label><textarea class="form-control" name="current_medications" rows="3">{{ summary_data.current_medications }}</textarea></div>
<div><label class="form-label">Problemas activos</label><textarea class="form-control" name="active_problems" rows="3">{{ summary_data.active_problems }}</textarea></div>
<div><label class="form-label">Alertas clínicas</label><textarea class="form-control" name="clinical_alerts" rows="3">{{ summary_data.clinical_alerts }}</textarea></div>
<div><label class="form-label">Antecedentes personales</label><textarea class="form-control" name="personal_history" rows="3">{{ summary_data.personal_history }}</textarea></div>
<div><label class="form-label">Antecedentes familiares</label><textarea class="form-control" name="family_history" rows="3">{{ summary_data.family_history }}</textarea></div>
<div><label class="form-label">Quirúrgicos / internaciones</label><textarea class="form-control" name="surgical_history" rows="3">{{ summary_data.surgical_history }}</textarea></div>
<div><label class="form-label">Hábitos</label><textarea class="form-control" name="habits" rows="3">{{ summary_data.habits }}</textarea></div>
<div><label class="form-label">Vacunación</label><textarea class="form-control" name="immunizations" rows="3">{{ summary_data.immunizations }}</textarea></div>
<div><label class="form-label">Discapacidad / apoyos</label><input class="form-control" name="disability_flags" value="{{ summary_data.disability_flags }}"></div>
<div><label class="form-label">Embarazo / condición especial</label><input class="form-control" name="pregnancy_status" value="{{ summary_data.pregnancy_status }}"></div>
<div><label class="form-label">Consentimientos y privacidad</label><textarea class="form-control" name="consent_overview" rows="3">{{ summary_data.consent_overview }}</textarea></div>
<div><label class="form-label">Datos críticos de emergencia</label><textarea class="form-control" name="emergency_notes" rows="3">{{ summary_data.emergency_notes }}</textarea></div>
<div><label class="form-label">Contactos autorizados</label><textarea class="form-control" name="privacy_contacts" rows="3">{{ summary_data.privacy_contacts }}</textarea></div>
<div style="grid-column:1/-1"><label class="form-label">Notas institucionales del legajo</label><textarea class="form-control" name="record_notes" rows="3">{{ record.notes or '' }}</textarea></div>
<div style="grid-column:1/-1" class="d-flex justify-content-end"><button class="btn btn-primary" data-confirm="¿Deseás guardar los cambios del resumen clínico fijo?" data-confirm-title="Guardar resumen clínico"><i class="bi bi-save me-1"></i> Guardar resumen</button></div>
</form>
</div></div>
</div>
<div class="clinical-card"><div class="clinical-head"><div><div class="h5 mb-1">Episodios clínicos y equipo tratante</div><div class="text-muted small">Matriz fina de accesos por episodio con roles y permisos.</div></div><button class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#episodeMemberModal"><i class="bi bi-people me-1"></i> Gestionar equipo</button></div><div class="clinical-body">
<div class="episode-grid">
{% for episode in episodes %}
<div class="episode-card">
<div class="d-flex justify-content-between gap-2 align-items-start mb-2"><div><div class="fw-bold">{{ episode.title }}</div><div class="small text-muted">{{ episode.specialty_name or 'Sin especialidad' }} · {{ episode.care_level }}</div></div><div class="d-flex flex-column gap-1 align-items-end"><span class="clinical-chip {% if episode.status != 'Abierto' %}success{% endif %}">{{ episode.status }}</span><span class="clinical-chip {% if episode.visibility_scope == 'Restringido' %}danger{% elif episode.visibility_scope == 'Paciente' %}success{% else %}warn{% endif %}">{{ episode.visibility_scope }}</span></div></div>
<div class="small text-muted mb-2">{{ episode.reason or 'Sin motivo consignado.' }}</div>
<div class="small mb-2"><strong>Resumen diagnóstico:</strong> {{ episode.diagnosis_summary or '—' }}</div>
<div class="episode-members mb-3">{% for member in episode.team_payload %}<div class="episode-member"><i class="bi bi-person-badge"></i>{{ member.full_name }} · {{ member.role }}<span class="small">{{ member.flags }}</span>{% if episode.permissions.can_write and member.can_revoke %}<form method="post" class="d-inline" data-confirm="¿Deseás revocar el acceso de {{ member.full_name }} en este episodio?" data-confirm-title="Revocar acceso de equipo tratante"><input type="hidden" name="action" value="revoke_episode_member"><input type="hidden" name="membership_id" value="{{ member.id }}"><input type="hidden" name="patient_id" value="{{ patient.id }}"><button class="btn btn-sm btn-link text-danger p-0 ms-1">Quitar</button></form>{% endif %}</div>{% else %}<span class="text-muted small">Sin integrantes cargados.</span>{% endfor %}</div>
<div class="small text-muted mb-2">Permisos del usuario actual: {% if episode.permissions.can_view %}ver{% endif %}{% if episode.permissions.can_write %} · escribir{% endif %}{% if episode.permissions.can_sign %} · firmar{% endif %}{% if episode.permissions.can_export %} · exportar{% endif %}</div>
<div class="action-row"><a class="btn btn-sm btn-outline-secondary" href="{{ url_for('clinical_episode_epicrisis', episode_id=episode.id) }}" target="_blank">Epicrisis</a><span class="btn btn-sm btn-light disabled">{{ episode.entries_count }} registros</span>{% if episode.status != 'Cerrado' and episode.permissions.can_sign %}<form method="post" class="d-inline" data-confirm="¿Deseás cerrar formalmente el episodio {{ episode.title }}? Esta acción deja asentado su cierre clínico." data-confirm-title="Cerrar episodio clínico"><input type="hidden" name="action" value="close_episode"><input type="hidden" name="patient_id" value="{{ patient.id }}"><input type="hidden" name="episode_id" value="{{ episode.id }}"><input type="hidden" name="closure_note" value="Cierre formal del episodio por {{ current_user.full_name }}"><button class="btn btn-sm btn-outline-danger">Cerrar episodio</button></form>{% endif %}</div>
</div>
{% else %}
<div class="text-muted">Todavía no hay episodios clínicos creados.</div>
{% endfor %}
</div>
</div></div>
<div class="clinical-card"><div class="clinical-head"><div><div class="h5 mb-1">Timeline clínico</div><div class="text-muted small">Capa 2 · tarjetas cronológicas, adjuntos, impresión y filtros.</div></div><span class="clinical-chip">{{ entries|length }} registros</span></div><div class="clinical-body">
<form method="get" class="timeline-toolbar mb-3"><input type="hidden" name="dni" value="{{ query_dni }}"><div><label class="form-label">Profesional</label><select class="form-select" name="professional_id"><option value="">Todos</option>{% for prof in professionals %}<option value="{{ prof.id }}" {% if selected_professional_id == prof.id %}selected{% endif %}>{{ prof.display_name }}</option>{% endfor %}</select></div><div><label class="form-label">Tipo</label><select class="form-select" name="encounter_type"><option value="">Todos</option>{% for item in encounter_type_options %}<option value="{{ item }}" {% if selected_type == item %}selected{% endif %}>{{ item }}</option>{% endfor %}</select></div><div><label class="form-label">Estado</label><select class="form-select" name="entry_status"><option value="">Todos</option>{% for item in entry_status_options %}<option value="{{ item }}" {% if selected_status == item %}selected{% endif %}>{{ item }}</option>{% endfor %}</select></div><div><label class="form-label">Fecha desde</label><input class="form-control" type="date" name="date_from" value="{{ selected_date_from }}"></div><div><label class="form-label">Fecha hasta</label><input class="form-control" type="date" name="date_to" value="{{ selected_date_to }}"></div><div><label class="form-label">Dx / código</label><input class="form-control" name="diagnosis" value="{{ selected_dx }}" placeholder="Texto, CIE o SNOMED"></div><div class="d-flex gap-2" style="grid-column:1/-1"><button class="btn btn-outline-primary">Aplicar filtros</button><a class="btn btn-outline-secondary" href="{{ url_for('admin_clinical_records', dni=query_dni) }}">Restablecer</a></div></form>
<div class="timeline-list">
{% for item in entries %}
<article class="entry-card">
<div class="entry-head"><div><div class="d-flex gap-2 flex-wrap mb-2"><span class="clinical-chip">Folio {{ item.folio_number }}</span><span class="clinical-chip">{{ item.entry_datetime.strftime('%d/%m/%Y %H:%M') if item.entry_datetime else '' }}</span><span class="clinical-chip">{{ item.encounter_type }}</span><span class="clinical-chip {% if item.entry_status == 'Firmado' %}success{% else %}warn{% endif %}">{{ item.entry_status }}</span><span class="clinical-chip {% if item.visibility_scope == 'Restringido' %}danger{% elif item.visibility_scope == 'Paciente' %}success{% else %}warn{% endif %}">{{ item.visibility_scope }}</span>{% if item.episode %}<span class="clinical-chip"><i class="bi bi-folder2-open"></i>{{ item.episode.title }}</span>{% endif %}</div><div class="h5 mb-1">{{ item.chief_complaint or item.diagnosis_text or 'Registro clínico sin encabezado' }}</div><div class="text-muted small">{{ item.professional.display_name if item.professional else item.signed_name }} · {{ item.specialty_name or 'Sin especialidad' }}</div></div><div class="text-end small text-muted"><div>CIE-10: <strong>{{ item.cie10_code or '—' }}</strong></div><div>SNOMED: <strong>{{ item.snomed_term or item.snomed_code or '—' }}</strong></div><div>Rev. {{ item.edit_revision or 1 }}</div></div></div>
<div class="entry-body">
<div class="d-flex flex-column gap-3"><div class="soft-block"><h6>Motivo / diagnóstico</h6><div><strong>Motivo:</strong> {{ item.chief_complaint or '—' }}</div><div><strong>Presuntivo:</strong> {{ item.provisional_diagnosis or '—' }}</div><div><strong>Clínico:</strong> {{ item.diagnosis_text or '—' }}</div></div><div class="soft-block"><h6>SOAP</h6><div><strong>Subjetivo:</strong> {{ item.subjective|safe if item.subjective else '—' }}</div><div class="mt-2"><strong>Objetivo:</strong> {{ item.objective|safe if item.objective else '—' }}</div><div class="mt-2"><strong>Valoración:</strong> {{ item.assessment|safe if item.assessment else '—' }}</div><div class="mt-2"><strong>Plan:</strong> {{ item.plan|safe if item.plan else '—' }}</div></div><div class="soft-block"><h6>Tratamiento y estudios</h6><div><strong>Indicaciones:</strong> {{ item.treatment|safe if item.treatment else '—' }}</div><div class="mt-2"><strong>Estudios / resultados:</strong> {{ item.study_results|safe if item.study_results else '—' }}</div><div class="mt-2"><strong>Consentimiento:</strong> {{ item.consent_reference or '—' }}</div></div></div>
<div class="d-flex flex-column gap-3"><div class="soft-block"><h6>Signos vitales</h6><div class="vitals-grid">{% for label, key in [('TA','bp'),('FC','hr'),('FR','rr'),('Temp','temp'),('SatO2','spo2'),('Glucemia','glucose'),('Peso','weight'),('Talla','height'),('IMC','bmi')] %}<div class="vital-box"><small>{{ label }}</small><strong>{{ item.vitals_data.get(key) or '—' }}</strong></div>{% endfor %}</div></div><div class="soft-block"><h6>Complemento por especialidad</h6>{% if item.structured_data %}<ul class="mb-0 ps-3">{% for key, value in item.structured_data.items() if value %}<li><strong>{{ key.replace('_',' ')|capitalize }}:</strong> {{ value }}</li>{% endfor %}</ul>{% else %}<div>Sin campos complementarios cargados.</div>{% endif %}</div><div class="soft-block"><h6>Firma y auditoría</h6><div><strong>Registró:</strong> {{ item.created_by_user.full_name if item.created_by_user else '—' }}</div><div><strong>Firma:</strong> {{ item.signature_data.mode or 'Sin firma' }}</div><div><strong>Estado certificado:</strong> {{ item.signature_data.certificate_status or '—' }}</div><div><strong>Serial:</strong> {{ item.signature_data.serial or '—' }}</div><div><strong>Bloqueado:</strong> {{ item.locked_at.strftime('%d/%m/%Y %H:%M') if item.locked_at else 'Pendiente de firma' }}</div></div><div class="soft-block"><h6>Adjuntos y acciones</h6><div class="attachment-list mb-3">{% for att in item.attachments %}<button type="button" class="attachment-pill border-0 attachment-view-btn" data-url="{{ att.preview_url }}" data-mime="{{ att.mime_type or '' }}" data-filename="{{ att.filename }}"><i class="bi bi-paperclip"></i>{{ att.filename }}</button>{% else %}<span class="text-muted">Sin adjuntos.</span>{% endfor %}</div><div class="action-row"><a class="btn btn-sm btn-outline-primary" href="{{ url_for('clinical_entry_print', entry_id=item.id) }}" target="_blank">Imprimir</a>{% if item.episode %}<a class="btn btn-sm btn-outline-secondary" href="{{ url_for('clinical_episode_epicrisis', episode_id=item.episode.id) }}" target="_blank">Epicrisis</a>{% endif %}{% if item.can_edit_draft %}<a class="btn btn-sm btn-outline-warning" href="{{ url_for('admin_clinical_records', dni=query_dni, edit_entry=item.id) }}">Editar borrador</a>{% endif %}</div></div></div>
</div>
</article>
{% else %}<div class="text-center text-muted py-5">Todavía no hay evoluciones registradas para este legajo.</div>{% endfor %}
</div>
</div></div>
<div class="modal fade" id="episodeModal" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-lg modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Nuevo episodio clínico</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><form method="post" id="episodeForm" data-confirm="¿Deseás crear este nuevo episodio clínico para el paciente?" data-confirm-title="Crear episodio clínico"><input type="hidden" name="action" value="create_episode"><input type="hidden" name="patient_id" value="{{ patient.id }}"><input type="hidden" name="applied_episode_template_id" id="appliedEpisodeTemplateId"><div class="modal-body"><div class="apex-clinical-tip small mb-3">La Historia Clínica sigue siendo el legajo único del paciente. Este formulario solo abre un episodio agrupador; luego podés cargar muchas evoluciones dentro del mismo episodio, incluso varias en el mismo día.</div><div class="row g-3"><div class="col-md-6"><label class="form-label">Título del episodio</label><input class="form-control" name="title" id="episodeTitleInput" required placeholder="Ej. Seguimiento HTA, dolor lumbar, internación por neumonía"></div><div class="col-md-6"><label class="form-label">Especialidad principal</label><input class="form-control" name="specialty_name" id="episodeSpecialtyInput" placeholder="Clínica médica, guardia, pediatría..."></div><div class="col-md-6"><label class="form-label">Motivo / apertura</label><input class="form-control" name="reason" id="episodeReasonInput" placeholder="Ej. ingreso por dolor abdominal, control pediátrico..."></div><div class="col-md-6"><label class="form-label">Resumen diagnóstico</label><input class="form-control" name="diagnosis_summary" id="episodeDiagnosisInput" placeholder="Resumen breve del cuadro o hipótesis diagnóstica"></div><div class="col-md-4"><label class="form-label">Nivel de cuidado</label><select class="form-select" name="care_level" id="episodeCareLevelSelect"><option>Ambulatorio</option><option>Observación</option><option>Internación</option><option>Terapia intensiva</option></select></div><div class="col-md-4"><label class="form-label">Estado</label><select class="form-select" name="status" id="episodeStatusSelect"><option>Abierto</option><option>Cerrado</option><option>Seguimiento</option></select></div><div class="col-md-4"><label class="form-label">Visibilidad</label><select class="form-select" name="visibility_scope" id="episodeVisibilitySelect">{% for item in visibility_scope_options %}<option value="{{ item.value }}">{{ item.label }}</option>{% endfor %}</select></div><div class="col-12"><label class="form-label">Notas del episodio</label><textarea class="form-control html-editor apex-clinical-editor" name="notes" id="episodeNotesInput" rows="4" placeholder="Contexto clínico, riesgos, interconsultas, objetivos del episodio..."></textarea></div><div class="col-12"><div class="soft-block"><div class="fw-bold mb-2"><i class="bi bi-bookmark-plus me-1"></i> Template de episodio</div><div class="text-muted small mb-3">Guardá esta apertura de episodio como modelo personal del profesional. No crea ni duplica historias clínicas.</div><div class="form-check form-switch mb-3"><input class="form-check-input" type="checkbox" name="save_episode_as_template" id="saveEpisodeTemplateCheck"><label class="form-check-label fw-semibold" for="saveEpisodeTemplateCheck">Guardar este episodio como template de mi perfil</label></div><div class="row g-3"><div class="col-md-6"><label class="form-label">Nombre del template</label><input class="form-control" name="episode_template_title" id="episodeTemplateTitleInput" placeholder="Ej.: Seguimiento HTA"></div><div class="col-md-6"><label class="form-label">Categoría / especialidad</label><input class="form-control" name="episode_template_category" id="episodeTemplateCategoryInput" placeholder="Ej.: Cardiología, clínica médica"></div></div></div></div></div></div><div class="modal-footer"><button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button><button class="btn btn-primary">Crear episodio</button></div></form></div></div></div>
<div class="modal fade" id="episodeTemplatePickerModal" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-xl modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><div><h5 class="modal-title">Crear episodio desde template</h5><div class="text-muted small">Templates personales de apertura de episodios, separados por especialidad/categoría.</div></div><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="template-picker-grid"><div><label class="form-label">Categoría / especialidad</label><select class="form-select mb-3" id="episodeTemplateCategoryFilter"><option value="">Todas</option></select><div class="template-list" id="episodeTemplateList"></div></div><div><div class="template-preview" id="episodeTemplatePreview"><div class="text-muted">Seleccioná un template de episodio.</div></div></div></div></div><div class="modal-footer"><button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button><button type="button" class="btn btn-primary" id="applyEpisodeTemplateBtn" disabled>Usar este template</button></div></div></div></div>
<div class="modal fade" id="clinicalTemplatePickerModal" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-xl modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><div><h5 class="modal-title">Crear evolución desde template</h5><div class="text-muted small">Templates personales de evoluciones clínicas. La evolución se puede ajustar antes de firmar.</div></div><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body"><div class="template-picker-grid"><div><label class="form-label">Categoría / especialidad</label><select class="form-select mb-3" id="templateCategoryFilter"><option value="">Todas</option></select><div class="template-list" id="clinicalTemplateList"></div></div><div><div class="template-preview" id="clinicalTemplatePreview"><div class="text-muted">Seleccioná un template de evolución.</div></div></div></div></div><div class="modal-footer"><button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button><button type="button" class="btn btn-primary" id="applyClinicalTemplateBtn" disabled>Usar este template</button></div></div></div></div>
<div class="modal fade" id="episodeMemberModal" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-lg"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">Equipo tratante y accesos</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><form method="post" data-confirm="¿Deseás guardar este acceso en el equipo tratante del episodio?" data-confirm-title="Guardar acceso clínico"><input type="hidden" name="action" value="add_episode_member"><div class="modal-body"><div class="row g-3"><div class="col-md-6"><label class="form-label">Episodio</label><select class="form-select" name="episode_id" required>{% for episode in episodes %}<option value="{{ episode.id }}">{{ episode.title }}</option>{% endfor %}</select></div><div class="col-md-6"><label class="form-label">Usuario</label><select class="form-select searchable-select" name="user_id" required>{% for user in care_team_users %}<option value="{{ user.id }}">{{ user.full_name }} ({{ user.role }})</option>{% endfor %}</select></div><div class="col-md-6"><label class="form-label">Rol en el episodio</label><input class="form-control" name="role_label" placeholder="Ej. médico tratante, interconsultor, referente" maxlength="120"></div><div class="col-md-6"><label class="form-label">Permisos</label><div class="d-flex flex-wrap gap-3 pt-2"><label><input type="checkbox" name="can_view" checked> Ver</label><label><input type="checkbox" name="can_write" checked> Escribir</label><label><input type="checkbox" name="can_sign"> Firmar</label><label><input type="checkbox" name="can_export"> Exportar</label></div></div></div></div><div class="modal-footer"><button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button><button class="btn btn-primary">Guardar acceso</button></div></form></div></div></div>
<div class="modal fade" id="clinicalEntryModal" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-xl modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title">{% if editing_entry %}Editar borrador clínico{% else %}Nueva evolución clínica inteligente{% endif %}</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><form method="post" enctype="multipart/form-data" id="clinicalEntryForm"><input type="hidden" name="action" value="{{ 'update_entry' if editing_entry else 'add_entry' }}"><input type="hidden" name="patient_id" value="{{ patient.id }}"><input type="hidden" name="entry_id" value="{{ editing_entry.id if editing_entry else '' }}"><input type="hidden" name="specialty_template" id="specialtyTemplateInput" value="{{ editing_entry.specialty_template if editing_entry else default_specialty_template }}"><input type="hidden" name="specialty_name_display" id="specialtyNameDisplay" value="{{ editing_entry.specialty_name if editing_entry else default_specialty_name }}"><input type="hidden" name="vitals_payload" id="vitalsPayloadInput"><input type="hidden" name="structured_payload" id="structuredPayloadInput"><input type="hidden" name="applied_template_id" id="appliedTemplateId"><input type="hidden" name="entry_status" id="entryStatusInput" value="{{ editing_entry.entry_status if editing_entry else 'Firmado' }}">
<div class="modal-body"><div class="row g-4"><div class="col-lg-4"><div class="soft-block h-100"><div class="fw-bold mb-3">Identificación clínica</div>{% if current_user.role == 'admin' %}<div class="mb-3"><label class="form-label">Profesional interviniente</label><select class="form-select searchable-select" name="professional_id" id="clinicalProfessionalSelect" required>{% for prof in professionals %}<option value="{{ prof.id }}" data-specialty="{{ prof.specialty }}">{{ prof.display_name }} · {{ prof.specialty }}</option>{% endfor %}</select></div>{% else %}<input type="hidden" id="clinicalProfessionalSelect"><div class="mb-3"><label class="form-label">Profesional</label><input class="form-control" value="{{ current_user.professional_profile.display_name if current_user.professional_profile else current_user.full_name }}" readonly></div>{% endif %}<div class="mb-3"><label class="form-label">Episodio</label><select class="form-select" name="episode_id"><option value="">Sin episodio asociado</option>{% for episode in episodes %}<option value="{{ episode.id }}">{{ episode.title }} · {{ episode.specialty_name or 'General' }}</option>{% endfor %}</select></div><div class="mb-3"><label class="form-label">Tipo de registro</label><select class="form-select" name="encounter_type">{% for item in encounter_type_options %}<option value="{{ item }}">{{ item }}</option>{% endfor %}</select></div><div class="mb-3"><label class="form-label">Especialidad asistencial</label><input class="form-control" id="specialtyDisplayText" value="{{ editing_entry.specialty_name if editing_entry else default_specialty_name }}" readonly></div><div class="mb-3"><label class="form-label">Plantilla clínica</label><select class="form-select" id="specialtyTemplateSelect">{% for tpl in specialty_templates %}<option value="{{ tpl.key }}">{{ tpl.label }}</option>{% endfor %}</select></div><div class="mb-3"><label class="form-label">Motivo de consulta</label><input class="form-control" id="chief_complaint" name="chief_complaint" maxlength="255" placeholder="Motivo principal de consulta o síntoma guía"></div><div class="mb-3"><label class="form-label">Diagnóstico presuntivo</label><input class="form-control" id="provisional_diagnosis" name="provisional_diagnosis" maxlength="255" placeholder="Hipótesis diagnóstica inicial"></div><div class="mb-3"><label class="form-label">Diagnóstico clínico</label><input class="form-control" id="diagnosis_text" name="diagnosis_text" maxlength="255" placeholder="Diagnóstico clínico principal o evolución diagnóstica"></div><div class="mb-3"><label class="form-label">CIE-10</label><select class="form-select searchable-select" id="clinicalCieSelect"><option value="">Seleccionar CIE-10</option>{% for item in cie10_catalog %}<option value="{{ item.code }}" data-term="{{ item.term }}">{{ item.code }} · {{ item.term }}</option>{% endfor %}</select><input type="hidden" id="cie10_code" name="cie10_code"><div class="code-preview mt-2"><div id="ciePreview">CIE-10: no seleccionado</div></div></div><div class="mb-3"><label class="form-label">SNOMED CT</label><select class="form-select searchable-select" id="clinicalSnomedSelect"><option value="">Seleccionar SNOMED CT</option>{% for item in snomed_catalog %}<option value="{{ item.code }}" data-term="{{ item.term }}">{{ item.term }} · {{ item.code }}</option>{% endfor %}</select><input type="hidden" id="snomed_term" name="snomed_term"><input type="hidden" id="snomed_code" name="snomed_code"><div class="code-preview mt-2"><div id="snomedPreview">SNOMED: no seleccionado</div></div></div><div><label class="form-label">Visibilidad</label><select class="form-select" name="visibility_scope">{% for item in visibility_scope_options %}<option value="{{ item.value }}">{{ item.label }}</option>{% endfor %}</select></div></div></div>
<div class="col-lg-8"><div class="d-flex flex-column gap-3"><div class="soft-block"><div class="fw-bold mb-3">SOAP y conducta</div><div class="row g-3"><div class="col-md-6"><label class="form-label">Subjetivo</label><textarea class="form-control html-editor apex-clinical-editor" name="subjective" id="subjective" rows="4" placeholder="Relato del paciente, síntomas y antecedentes referidos"></textarea></div><div class="col-md-6"><label class="form-label">Objetivo</label><textarea class="form-control html-editor apex-clinical-editor" name="objective" id="objective" rows="4" placeholder="Hallazgos del examen físico o mental"></textarea></div><div class="col-md-6"><label class="form-label">Valoración</label><textarea class="form-control html-editor apex-clinical-editor" name="assessment" id="assessment" rows="4" placeholder="Interpretación clínica del cuadro"></textarea></div><div class="col-md-6"><label class="form-label">Plan</label><textarea class="form-control html-editor apex-clinical-editor" name="plan" id="plan" rows="4" placeholder="Conducta, seguimiento, estudios o derivación"></textarea></div><div class="col-md-6"><label class="form-label">Indicaciones / tratamiento</label><textarea class="form-control html-editor apex-clinical-editor" name="treatment" id="treatment" rows="3" placeholder="Medicaciones, indicaciones y cuidados"></textarea></div><div class="col-md-6"><label class="form-label">Estudios / resultados</label><textarea class="form-control html-editor apex-clinical-editor" name="study_results" id="study_results" rows="3" placeholder="Laboratorios, imágenes, interconsultas o resultados"></textarea></div><div class="col-12"><label class="form-label">Consentimiento vinculado</label><input class="form-control" name="consent_reference" id="consent_reference" maxlength="120" placeholder="Referencia, número o enlace del consentimiento informado"></div></div></div><div class="soft-block"><div class="fw-bold mb-3">Signos vitales</div><div class="row g-3"><div class="col-md-4"><label class="form-label">TA</label><input class="form-control vital-input" data-key="bp" placeholder="120/80"></div><div class="col-md-4"><label class="form-label">FC</label><input class="form-control vital-input" data-key="hr" inputmode="numeric" maxlength="3" placeholder="72"></div><div class="col-md-4"><label class="form-label">FR</label><input class="form-control vital-input" data-key="rr" inputmode="numeric" maxlength="3" placeholder="18"></div><div class="col-md-4"><label class="form-label">Temperatura</label><input class="form-control vital-input" data-key="temp" inputmode="decimal" maxlength="5" placeholder="36.7"></div><div class="col-md-4"><label class="form-label">SatO2</label><input class="form-control vital-input" data-key="spo2" inputmode="numeric" maxlength="3" placeholder="98"></div><div class="col-md-4"><label class="form-label">Glucemia</label><input class="form-control vital-input" data-key="glucose" inputmode="decimal" maxlength="6" placeholder="105"></div><div class="col-md-4"><label class="form-label">Peso</label><input class="form-control vital-input" data-key="weight" inputmode="decimal" maxlength="6" placeholder="70"></div><div class="col-md-4"><label class="form-label">Talla</label><input class="form-control vital-input" data-key="height" inputmode="decimal" maxlength="6" placeholder="1.70"></div><div class="col-md-4"><label class="form-label">IMC</label><input class="form-control vital-input" data-key="bmi" inputmode="decimal" maxlength="6" placeholder="24.2"></div></div></div><div class="soft-block"><div class="fw-bold mb-3">Complemento por especialidad</div><div id="specialtyTemplateHint" class="text-muted small mb-3"></div><div id="specialtyDynamicFields" class="row g-3"></div></div><div class="soft-block"><div class="fw-bold mb-3"><i class="bi bi-bookmark-plus me-1"></i> Template de evolución del profesional</div><div class="apex-clinical-tip small mb-3">Al guardar esta evolución podés convertirla en un template personal para reutilizarla con otros pacientes de la misma especialidad. Después solo ajustás lo particular de cada caso.</div><div class="form-check form-switch mb-3"><input class="form-check-input" type="checkbox" name="save_as_template" id="saveAsTemplateCheck"><label class="form-check-label fw-semibold" for="saveAsTemplateCheck">Guardar esta evolución como template de mi perfil</label></div><div class="row g-3"><div class="col-md-6"><label class="form-label">Nombre del template</label><input class="form-control" name="template_title" id="templateTitleInput" placeholder="Ej.: Control HTA estable"></div><div class="col-md-6"><label class="form-label">Categoría / especialidad</label><input class="form-control" name="template_category" id="templateCategoryInput" placeholder="Ej.: Cardiología, clínica médica"></div></div></div><div class="soft-block"><div class="fw-bold mb-3">Adjuntos clínicos</div><input type="file" class="form-control" name="clinical_attachments" id="clinicalAttachmentInput" multiple accept=".pdf,.png,.jpg,.jpeg,.webp,.doc,.docx,.xls,.xlsx,.csv,.txt"><div id="clinicalAttachmentVisibility" class="mt-3 d-flex flex-column gap-2"></div><div class="form-hint mt-2">PDF, imágenes, interconsultas o estudios complementarios.</div></div></div></div></div></div><div class="modal-footer d-flex justify-content-between flex-wrap gap-2"><div class="small text-muted">Los borradores quedan con trazabilidad de revisión. La firma certificada externa queda lista para integración posterior.</div><div class="d-flex gap-2"><button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button><button type="submit" class="btn btn-outline-primary clinical-draft-btn" data-confirm="¿Deseás guardar este registro como borrador clínico?" data-confirm-title="Guardar borrador">Guardar borrador</button><button type="submit" class="btn btn-primary clinical-sign-btn" data-confirm="¿Deseás firmar y guardar esta evolución clínica?" data-confirm-title="Firmar evolución clínica" data-confirm-accept="Firmar y guardar">Firmar y guardar</button></div></div></form></div></div></div>
<div class="modal fade" id="attachmentViewerModal" tabindex="-1" aria-hidden="true"><div class="modal-dialog modal-xl modal-dialog-scrollable"><div class="modal-content"><div class="modal-header"><h5 class="modal-title" id="attachmentViewerTitle">Adjunto clínico</h5><button type="button" class="btn-close" data-bs-dismiss="modal"></button></div><div class="modal-body" id="attachmentViewerBody"></div></div></div></div>
{% elif query_dni %}<div class="clinical-card"><div class="clinical-body text-center py-5 text-muted"><i class="bi bi-search d-block mb-2" style="font-size:2rem"></i><h3>No se encontró el paciente</h3><p>Verificá el DNI o registrá primero al paciente para abrir el legajo electrónico.</p></div></div>
{% else %}<div class="clinical-card"><div class="clinical-body text-center py-5 text-muted"><i class="bi bi-journal-medical d-block mb-2" style="font-size:2rem"></i><h3>Buscá un paciente para abrir la historia clínica</h3><p>El legajo se crea automáticamente y luego puede organizarse por episodios, equipo tratante y registros estructurados.</p></div></div>{% endif %}
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function(){
const specialtyCatalog = {{ specialty_templates_json|safe }};
const clinicalTemplates = {{ clinical_templates_json|safe }};
const episodeTemplates = {{ episode_templates_json|safe }};
const specialtySelect = document.getElementById('specialtyTemplateSelect');
const specialtyDisplayText = document.getElementById('specialtyDisplayText');
const specialtyNameInput = document.getElementById('specialtyNameDisplay');
const specialtyDynamicFields = document.getElementById('specialtyDynamicFields');
const specialtyHint = document.getElementById('specialtyTemplateHint');
const professionalSelect = document.getElementById('clinicalProfessionalSelect');
const structuredPayloadInput = document.getElementById('structuredPayloadInput');
const vitalsPayloadInput = document.getElementById('vitalsPayloadInput');
const entryStatusInput = document.getElementById('entryStatusInput');
const form = document.getElementById('clinicalEntryForm');
const diagnosisInput = document.getElementById('diagnosis_text');
const cieSelect = document.getElementById('clinicalCieSelect');
const snomedSelect = document.getElementById('clinicalSnomedSelect');
const cieCodeInput = document.getElementById('cie10_code');
const snomedTermInput = document.getElementById('snomed_term');
const snomedCodeInput = document.getElementById('snomed_code');
const ciePreview = document.getElementById('ciePreview');
const snomedPreview = document.getElementById('snomedPreview');
const attachmentModalEl = document.getElementById('attachmentViewerModal');
const attachmentInput = document.getElementById('clinicalAttachmentInput');
const attachmentVisibility = document.getElementById('clinicalAttachmentVisibility');
const attachmentTitle = document.getElementById('attachmentViewerTitle');
const attachmentBody = document.getElementById('attachmentViewerBody');
const editingData = {{ editing_entry_form|tojson if editing_entry_form else '{}'|safe }};
const clinicalTemplateList = document.getElementById('clinicalTemplateList');
const templateCategoryFilter = document.getElementById('templateCategoryFilter');
const templatePreview = document.getElementById('clinicalTemplatePreview');
const applyClinicalTemplateBtn = document.getElementById('applyClinicalTemplateBtn');
const appliedTemplateId = document.getElementById('appliedTemplateId');
const saveAsTemplateCheck = document.getElementById('saveAsTemplateCheck');
const templateTitleInput = document.getElementById('templateTitleInput');
const templateCategoryInput = document.getElementById('templateCategoryInput');
let selectedClinicalTemplate = null;
const episodeTemplateList = document.getElementById('episodeTemplateList');
const episodeTemplateCategoryFilter = document.getElementById('episodeTemplateCategoryFilter');
const episodeTemplatePreview = document.getElementById('episodeTemplatePreview');
const applyEpisodeTemplateBtn = document.getElementById('applyEpisodeTemplateBtn');
const appliedEpisodeTemplateId = document.getElementById('appliedEpisodeTemplateId');
const saveEpisodeTemplateCheck = document.getElementById('saveEpisodeTemplateCheck');
const episodeTemplateTitleInput = document.getElementById('episodeTemplateTitleInput');
const episodeTemplateCategoryInput = document.getElementById('episodeTemplateCategoryInput');
let selectedEpisodeTemplate = null;
function setFieldValue(name, value){
const el = document.querySelector(`[name="${name}"]`) || document.getElementById(name);
if (!el) return;
if (window.ApexRichText && el.matches && el.matches('textarea.html-editor')) window.ApexRichText.setValue(el, value || '');
else el.value = value || '';
}
function currentProfessionalId(){
if (professionalSelect && professionalSelect.tagName === 'SELECT') return Number(professionalSelect.value || 0);
{% if current_user.role == 'professional' and current_user.professional_profile %}return {{ current_user.professional_profile.id }};{% else %}return 0;{% endif %}
}
function templateCategories(){
const profId = currentProfessionalId();
const cats = new Set();
clinicalTemplates.forEach(tpl => {
if (profId && tpl.professional_id && Number(tpl.professional_id) !== Number(profId)) return;
cats.add((tpl.category || tpl.specialty_name || 'General').trim() || 'General');
});
return Array.from(cats).sort((a,b) => a.localeCompare(b));
}
function templateMatches(tpl){
const profId = currentProfessionalId();
const cat = templateCategoryFilter ? templateCategoryFilter.value : '';
if (profId && tpl.professional_id && Number(tpl.professional_id) !== Number(profId)) return false;
const tplCat = (tpl.category || tpl.specialty_name || 'General').trim() || 'General';
return !cat || tplCat === cat;
}
function renderTemplateCategories(){
if (!templateCategoryFilter) return;
const current = templateCategoryFilter.value;
templateCategoryFilter.innerHTML = '<option value="">Todas</option>';
templateCategories().forEach(cat => {
const opt = document.createElement('option'); opt.value = cat; opt.textContent = cat; templateCategoryFilter.appendChild(opt);
});
if (current) templateCategoryFilter.value = current;
}
function templateSummaryHtml(tpl){
if (!tpl) return '<div class="text-muted">Seleccioná un template.</div>';
const pairs = [
['Tipo', tpl.encounter_type], ['Motivo', tpl.chief_complaint], ['Diagnóstico', tpl.diagnosis_text || tpl.provisional_diagnosis],
['Subjetivo', tpl.subjective], ['Objetivo', tpl.objective], ['Valoración', tpl.assessment], ['Plan', tpl.plan], ['Tratamiento', tpl.treatment]
].filter(pair => pair[1]);
return `<div class="d-flex justify-content-between gap-3 align-items-start mb-3"><div><h5 class="mb-1">${tpl.title}</h5><div class="small text-muted">${tpl.professional_name || 'Profesional'} · ${tpl.specialty_name || tpl.category || 'General'}</div></div><span class="badge text-bg-success">Usado ${tpl.usage_count || 0} vez/veces</span></div>` +
(pairs.length ? '<div class="row g-2">' + pairs.map(pair => `<div class="col-md-6"><div class="border rounded-4 p-2 h-100"><div class="small text-muted">${pair[0]}</div><div>${String(pair[1]).slice(0, 420)}</div></div></div>`).join('') + '</div>' : '<div class="text-muted">Template sin contenido principal.</div>');
}
function renderClinicalTemplateList(){
if (!clinicalTemplateList) return;
renderTemplateCategories();
clinicalTemplateList.innerHTML = '';
const items = clinicalTemplates.filter(templateMatches);
if (!items.length){
clinicalTemplateList.innerHTML = '<div class="text-muted small border rounded-4 p-3">No hay templates de evolución para esta especialidad/profesional. Guardá una evolución como template para reutilizarla.</div>';
selectedClinicalTemplate = null;
if (applyClinicalTemplateBtn) applyClinicalTemplateBtn.disabled = true;
if (templatePreview) templatePreview.innerHTML = '<div class="text-muted">No hay templates disponibles con el filtro actual.</div>';
return;
}
items.forEach(tpl => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'template-item';
btn.innerHTML = `<strong>${tpl.title}</strong><small>${tpl.category || tpl.specialty_name || 'General'} · ${tpl.encounter_type || 'Evolución médica'}</small>`;
btn.addEventListener('click', function(){
selectedClinicalTemplate = tpl;
document.querySelectorAll('.template-item').forEach(el => el.classList.remove('active'));
btn.classList.add('active');
if (templatePreview) templatePreview.innerHTML = templateSummaryHtml(tpl);
if (applyClinicalTemplateBtn) applyClinicalTemplateBtn.disabled = false;
});
clinicalTemplateList.appendChild(btn);
});
}
function applyClinicalTemplate(tpl){
if (!tpl) return;
if (appliedTemplateId) appliedTemplateId.value = tpl.id || '';
if (specialtyDisplayText) specialtyDisplayText.value = tpl.specialty_name || tpl.category || specialtyDisplayText.value;
if (specialtyNameInput) specialtyNameInput.value = specialtyDisplayText ? specialtyDisplayText.value : '';
if (specialtySelect && tpl.specialty_template) specialtySelect.value = tpl.specialty_template;
['encounter_type','chief_complaint','provisional_diagnosis','diagnosis_text','subjective','objective','assessment','plan','treatment','study_results','consent_reference','visibility_scope'].forEach(key => setFieldValue(key, tpl[key] || ''));
if (cieCodeInput) cieCodeInput.value = tpl.cie10_code || '';
if (snomedTermInput) snomedTermInput.value = tpl.snomed_term || '';
if (snomedCodeInput) snomedCodeInput.value = tpl.snomed_code || '';
if (cieSelect && tpl.cie10_code) cieSelect.value = tpl.cie10_code;
if (snomedSelect && tpl.snomed_code) snomedSelect.value = tpl.snomed_code;
renderSpecialtyFields((tpl.specialty_template || specialtySelect?.value || '{{ default_specialty_template }}'), tpl.structured || {});
Object.entries(tpl.vitals || {}).forEach(([key, value]) => { const input = document.querySelector(`.vital-input[data-key="${key}"]`); if (input) input.value = value; });
syncStructuredPayload(); syncVitalsPayload(); updateSelectedCodes();
const pickerEl = document.getElementById('clinicalTemplatePickerModal');
const entryEl = document.getElementById('clinicalEntryModal');
if (pickerEl && window.bootstrap) bootstrap.Modal.getOrCreateInstance(pickerEl).hide();
if (entryEl && window.bootstrap) bootstrap.Modal.getOrCreateInstance(entryEl).show();
}
function episodeTemplateCategories(){
const profId = currentProfessionalId();
const cats = new Set();
episodeTemplates.forEach(tpl => {
if (profId && tpl.professional_id && Number(tpl.professional_id) !== Number(profId)) return;
cats.add((tpl.category || tpl.specialty_name || 'General').trim() || 'General');
});
return Array.from(cats).sort((a,b) => a.localeCompare(b));
}
function episodeTemplateMatches(tpl){
const profId = currentProfessionalId();
const cat = episodeTemplateCategoryFilter ? episodeTemplateCategoryFilter.value : '';
if (profId && tpl.professional_id && Number(tpl.professional_id) !== Number(profId)) return false;
const tplCat = (tpl.category || tpl.specialty_name || 'General').trim() || 'General';
return !cat || tplCat === cat;
}
function renderEpisodeTemplateCategories(){
if (!episodeTemplateCategoryFilter) return;
const current = episodeTemplateCategoryFilter.value;
episodeTemplateCategoryFilter.innerHTML = '<option value="">Todas</option>';
episodeTemplateCategories().forEach(cat => {
const opt = document.createElement('option'); opt.value = cat; opt.textContent = cat; episodeTemplateCategoryFilter.appendChild(opt);
});
if (current) episodeTemplateCategoryFilter.value = current;
}
function episodeTemplateSummaryHtml(tpl){
if (!tpl) return '<div class="text-muted">Seleccioná un template de episodio.</div>';
const pairs = [
['Especialidad', tpl.specialty_name], ['Motivo', tpl.reason], ['Resumen diagnóstico', tpl.diagnosis_summary],
['Nivel', tpl.care_level], ['Visibilidad', tpl.visibility_scope], ['Notas', tpl.notes]
].filter(pair => pair[1]);
return `<div class="d-flex justify-content-between gap-3 align-items-start mb-3"><div><h5 class="mb-1">${tpl.title}</h5><div class="small text-muted">${tpl.professional_name || 'Profesional'} · ${tpl.category || tpl.specialty_name || 'General'}</div></div><span class="badge text-bg-primary">Usado ${tpl.usage_count || 0} vez/veces</span></div>` +
(pairs.length ? '<div class="row g-2">' + pairs.map(pair => `<div class="col-md-6"><div class="border rounded-4 p-2 h-100"><div class="small text-muted">${pair[0]}</div><div>${String(pair[1]).slice(0, 420)}</div></div></div>`).join('') + '</div>' : '<div class="text-muted">Template de episodio sin contenido principal.</div>');
}
function renderEpisodeTemplateList(){
if (!episodeTemplateList) return;
renderEpisodeTemplateCategories();
episodeTemplateList.innerHTML = '';
const items = episodeTemplates.filter(episodeTemplateMatches);
if (!items.length){
episodeTemplateList.innerHTML = '<div class="text-muted small border rounded-4 p-3">No hay templates de episodio para esta especialidad/profesional. Guardá una apertura de episodio como template para reutilizarla.</div>';
selectedEpisodeTemplate = null;
if (applyEpisodeTemplateBtn) applyEpisodeTemplateBtn.disabled = true;
if (episodeTemplatePreview) episodeTemplatePreview.innerHTML = '<div class="text-muted">No hay templates de episodio disponibles con el filtro actual.</div>';
return;
}
items.forEach(tpl => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'template-item episode-template-item';
btn.innerHTML = `<strong>${tpl.title}</strong><small>${tpl.category || tpl.specialty_name || 'General'} · ${tpl.care_level || 'Ambulatorio'}</small>`;
btn.addEventListener('click', function(){
selectedEpisodeTemplate = tpl;
document.querySelectorAll('.episode-template-item').forEach(el => el.classList.remove('active'));
btn.classList.add('active');
if (episodeTemplatePreview) episodeTemplatePreview.innerHTML = episodeTemplateSummaryHtml(tpl);
if (applyEpisodeTemplateBtn) applyEpisodeTemplateBtn.disabled = false;
});
episodeTemplateList.appendChild(btn);
});
}
function setEpisodeFieldValue(name, value){
const el = document.querySelector(`#episodeForm [name="${name}"]`) || document.getElementById(name);
if (!el) return;
if (window.ApexRichText && el.matches && el.matches('textarea.html-editor')) window.ApexRichText.setValue(el, value || '');
else el.value = value || '';
}
function applyEpisodeTemplate(tpl){
if (!tpl) return;
if (appliedEpisodeTemplateId) appliedEpisodeTemplateId.value = tpl.id || '';
setEpisodeFieldValue('title', tpl.title || '');
setEpisodeFieldValue('specialty_name', tpl.specialty_name || tpl.category || '');
setEpisodeFieldValue('reason', tpl.reason || '');
setEpisodeFieldValue('diagnosis_summary', tpl.diagnosis_summary || '');
setEpisodeFieldValue('care_level', tpl.care_level || 'Ambulatorio');
setEpisodeFieldValue('visibility_scope', tpl.visibility_scope || 'Institucional');
setEpisodeFieldValue('notes', tpl.notes || '');
const pickerEl = document.getElementById('episodeTemplatePickerModal');
const episodeEl = document.getElementById('episodeModal');
if (pickerEl && window.bootstrap) bootstrap.Modal.getOrCreateInstance(pickerEl).hide();
if (episodeEl && window.bootstrap) bootstrap.Modal.getOrCreateInstance(episodeEl).show();
}
function findTemplate(key){return specialtyCatalog.find(item => item.key === key) || specialtyCatalog[0];}
function inferTemplateFromSpecialty(source){const text = String(source || '').trim().toLowerCase(); if(!text) return specialtyCatalog[0]?.key || 'general_medicine'; const found = specialtyCatalog.find(item => (item.areas || []).some(area => text.includes(area.toLowerCase()) || area.toLowerCase().includes(text)) || item.label.toLowerCase() === text); return found ? found.key : (specialtyCatalog[0]?.key || 'general_medicine');}
function renderSpecialtyFields(key, preset){ if(!specialtyDynamicFields) return; const template = findTemplate(key); specialtyHint.textContent = template ? template.description : ''; specialtyDynamicFields.innerHTML = ''; (template?.fields || []).forEach(field => { const wrapper = document.createElement('div'); wrapper.className = 'col-md-6'; const label = document.createElement('label'); label.className='form-label'; label.textContent = field.label; wrapper.appendChild(label); let control; if(field.type === 'textarea'){ control = document.createElement('textarea'); control.rows = 3; } else { control = document.createElement('input'); control.type = field.type || 'text'; } control.className = 'form-control specialty-dynamic-field' + (field.type === 'textarea' ? ' html-editor apex-clinical-editor' : ''); control.dataset.key = field.name; control.placeholder = field.placeholder || ''; control.value = preset && preset[field.name] ? preset[field.name] : ''; wrapper.appendChild(control); specialtyDynamicFields.appendChild(wrapper); }); if (specialtyNameInput) specialtyNameInput.value = specialtyDisplayText ? specialtyDisplayText.value : ''; syncStructuredPayload(); if (window.ApexRichText) window.ApexRichText.refresh(); }
function syncStructuredPayload(){ if (window.ApexRichText) window.ApexRichText.syncAll(); const payload = {}; document.querySelectorAll('.specialty-dynamic-field').forEach(el => { if (el.value && String(el.value).trim()) payload[el.dataset.key] = el.value.trim(); }); if (structuredPayloadInput) structuredPayloadInput.value = JSON.stringify(payload); }
function syncVitalsPayload(){ const payload = {}; document.querySelectorAll('.vital-input').forEach(el => { if (el.value && String(el.value).trim()) payload[el.dataset.key] = el.value.trim(); }); if (vitalsPayloadInput) vitalsPayloadInput.value = JSON.stringify(payload); }
function updateSelectedCodes(){ const cieOpt = cieSelect?.selectedOptions?.[0]; if (cieOpt && cieOpt.value){ if(cieCodeInput) cieCodeInput.value = cieOpt.value; if(diagnosisInput && !diagnosisInput.value.trim()) diagnosisInput.value = cieOpt.dataset.term || cieOpt.textContent || ''; if(ciePreview) ciePreview.textContent = `CIE-10: ${cieOpt.value} · ${cieOpt.dataset.term || cieOpt.textContent || ''}`; } else if(ciePreview){ if(cieCodeInput) cieCodeInput.value=''; ciePreview.textContent='CIE-10: no seleccionado'; }
const snomedOpt = snomedSelect?.selectedOptions?.[0]; if (snomedOpt && snomedOpt.value){ if(snomedCodeInput) snomedCodeInput.value = snomedOpt.value; if(snomedTermInput) snomedTermInput.value = snomedOpt.dataset.term || snomedOpt.textContent || ''; if(snomedPreview) snomedPreview.textContent = `SNOMED: ${snomedOpt.dataset.term || ''} · ${snomedOpt.value}`; } else if(snomedPreview){ if(snomedCodeInput) snomedCodeInput.value=''; if(snomedTermInput) snomedTermInput.value=''; snomedPreview.textContent='SNOMED: no seleccionado'; }}
function renderAttachmentVisibilityChoices(){ if(!attachmentInput || !attachmentVisibility) return; attachmentVisibility.innerHTML=''; Array.from(attachmentInput.files || []).forEach(file => { const safeToken = String(file.name || '').replace(/[^A-Za-z0-9._-]/g, ''); const row = document.createElement('label'); row.className = 'd-flex align-items-center justify-content-between gap-3 border rounded-4 px-3 py-2'; row.innerHTML = `<span class="small text-truncate">${file.name}</span><span class="form-check form-switch mb-0"><input class="form-check-input" type="checkbox" name="patient_visible_attachment_tokens" value="${safeToken}"><span class="small ms-2">Visible en portal</span></span>`; attachmentVisibility.appendChild(row); }); }
function syncProfessionalSpecialty(){ if(!professionalSelect || !professionalSelect.selectedOptions) return; const opt = professionalSelect.selectedOptions[0]; if(!opt) return; const specialty = opt.dataset.specialty || ''; if(specialtyDisplayText) specialtyDisplayText.value = specialty; if(specialtyNameInput) specialtyNameInput.value = specialty; const inferred = inferTemplateFromSpecialty(specialty); if(specialtySelect) specialtySelect.value = inferred; renderSpecialtyFields(inferred); }
function applyEditingData(){ if (!editingData || !editingData.id) return; const map = ['episode_id','encounter_type','chief_complaint','provisional_diagnosis','diagnosis_text','subjective','objective','assessment','plan','treatment','study_results','consent_reference','visibility_scope']; map.forEach(key => { if (editingData[key] !== undefined) setFieldValue(key, editingData[key]); }); if (specialtyDisplayText) specialtyDisplayText.value = editingData.specialty_name || specialtyDisplayText.value; if (specialtyNameInput) specialtyNameInput.value = editingData.specialty_name || specialtyNameInput.value; if (specialtySelect && editingData.specialty_template) specialtySelect.value = editingData.specialty_template; renderSpecialtyFields((editingData.specialty_template || specialtySelect?.value || '{{ default_specialty_template }}'), editingData.structured || {}); Object.entries(editingData.vitals || {}).forEach(([key, value]) => { const input = document.querySelector(`.vital-input[data-key="${key}"]`); if (input) input.value = value; }); syncVitalsPayload(); if (cieCodeInput) cieCodeInput.value = editingData.cie10_code || ''; if (snomedTermInput) snomedTermInput.value = editingData.snomed_term || ''; if (snomedCodeInput) snomedCodeInput.value = editingData.snomed_code || ''; if (cieSelect && editingData.cie10_code) cieSelect.value = editingData.cie10_code; if (snomedSelect && editingData.snomed_code) snomedSelect.value = editingData.snomed_code; updateSelectedCodes(); const modalEl = document.getElementById('clinicalEntryModal'); if (modalEl && window.bootstrap) bootstrap.Modal.getOrCreateInstance(modalEl).show(); }
if (specialtySelect){ renderSpecialtyFields(specialtySelect.value || '{{ default_specialty_template }}'); specialtySelect.addEventListener('change', function(){ renderSpecialtyFields(specialtySelect.value); }); }
if (professionalSelect && professionalSelect.tagName === 'SELECT'){ professionalSelect.addEventListener('change', syncProfessionalSpecialty); syncProfessionalSpecialty(); }
if (cieSelect) cieSelect.addEventListener('change', updateSelectedCodes); if (snomedSelect) snomedSelect.addEventListener('change', updateSelectedCodes);
if (attachmentInput) attachmentInput.addEventListener('change', renderAttachmentVisibilityChoices);
document.addEventListener('input', function(ev){ if (ev.target.classList.contains('specialty-dynamic-field')) syncStructuredPayload(); if (ev.target.classList.contains('vital-input')) syncVitalsPayload(); });
document.querySelectorAll('.clinical-draft-btn').forEach(btn => btn.addEventListener('click', function(){ if(entryStatusInput) entryStatusInput.value='Borrador'; }));
document.querySelectorAll('.clinical-sign-btn').forEach(btn => btn.addEventListener('click', function(){ if(entryStatusInput) entryStatusInput.value='Firmado'; }));
if (form){ form.addEventListener('submit', function(){ syncStructuredPayload(); syncVitalsPayload(); updateSelectedCodes(); if (specialtySelect && document.getElementById('specialtyTemplateInput')) document.getElementById('specialtyTemplateInput').value = specialtySelect.value; if (specialtyDisplayText && specialtyNameInput) specialtyNameInput.value = specialtyDisplayText.value || ''; }); }
document.querySelectorAll('.attachment-view-btn').forEach(btn => btn.addEventListener('click', function(){ if (!attachmentBody) return; const url = btn.dataset.url || ''; const mime = (btn.dataset.mime || '').toLowerCase(); const name = btn.dataset.filename || 'Adjunto'; attachmentTitle.textContent = name; if (mime.includes('pdf') || url.toLowerCase().endsWith('.pdf')) { attachmentBody.innerHTML = `<iframe src="${url}" style="width:100%;height:78vh;border:0;border-radius:16px;"></iframe>`; } else if (mime.startsWith('image/')) { attachmentBody.innerHTML = `<img src="${url}" alt="${name}" style="max-width:100%;border-radius:16px;display:block;margin:auto;">`; } else { attachmentBody.innerHTML = `<div class="text-center py-5"><a class="btn btn-primary" href="${url}" target="_blank">Abrir adjunto</a></div>`; } if (window.bootstrap) bootstrap.Modal.getOrCreateInstance(attachmentModalEl).show(); }));
if (templateCategoryFilter) templateCategoryFilter.addEventListener('change', renderClinicalTemplateList);
if (episodeTemplateCategoryFilter) episodeTemplateCategoryFilter.addEventListener('change', renderEpisodeTemplateList);
if (applyClinicalTemplateBtn) applyClinicalTemplateBtn.addEventListener('click', function(){ applyClinicalTemplate(selectedClinicalTemplate); });
if (applyEpisodeTemplateBtn) applyEpisodeTemplateBtn.addEventListener('click', function(){ applyEpisodeTemplate(selectedEpisodeTemplate); });
if (professionalSelect && professionalSelect.tagName === 'SELECT') professionalSelect.addEventListener('change', function(){ renderClinicalTemplateList(); renderEpisodeTemplateList(); });
if (saveAsTemplateCheck) saveAsTemplateCheck.addEventListener('change', function(){ if (saveAsTemplateCheck.checked){ if (templateCategoryInput && specialtyDisplayText && !templateCategoryInput.value) templateCategoryInput.value = specialtyDisplayText.value || ''; if (templateTitleInput && diagnosisInput && !templateTitleInput.value) templateTitleInput.value = diagnosisInput.value || document.getElementById('chief_complaint')?.value || ''; }});
if (saveEpisodeTemplateCheck) saveEpisodeTemplateCheck.addEventListener('change', function(){ if (saveEpisodeTemplateCheck.checked){ const spec = document.getElementById('episodeSpecialtyInput'); const title = document.getElementById('episodeTitleInput'); if (episodeTemplateCategoryInput && spec && !episodeTemplateCategoryInput.value) episodeTemplateCategoryInput.value = spec.value || ''; if (episodeTemplateTitleInput && title && !episodeTemplateTitleInput.value) episodeTemplateTitleInput.value = title.value || ''; }});
renderClinicalTemplateList(); renderEpisodeTemplateList();
updateSelectedCodes(); renderAttachmentVisibilityChoices(); applyEditingData();
});
</script>
{% endblock %}