mi-proyecto/app/templates/admin_recipes.html

658 lines
44 KiB
HTML

{% extends 'base.html' %}
{% block content %}
<style>
.modal-recipe .modal-dialog { max-width: min(1180px, calc(100vw - 2rem)); margin: 1rem auto; }
.modal-recipe .modal-content { display:flex; flex-direction:column; max-height:calc(100vh - 2rem); border:0; border-radius:24px; overflow:hidden; box-shadow:0 24px 60px rgba(15,23,42,.24); position:relative; }
.modal-recipe .modal-header { flex:0 0 auto; padding:1rem 1.25rem; border-bottom:0; background:linear-gradient(135deg,#0b1220 0%,#13213a 100%); color:#fff; }
.modal-recipe .modal-body { flex:1 1 auto; overflow-y:auto; overflow-x:hidden; padding:1.25rem; background:#f8fafc; }
.modal-recipe .modal-footer { flex:0 0 auto; padding:1rem 1.25rem; border-top:1px solid #e5e7eb; background:#fff; }
.modal-recipe .section-card { background:#fff; border:1px solid #e6ebf2; border-radius:18px; padding:1rem; box-shadow:0 8px 22px rgba(15,23,42,.05); }
.modal-recipe .section-title { margin-bottom:.85rem; font-size:.85rem; font-weight:700; letter-spacing:.03em; text-transform:uppercase; color:#334155; }
.modal-recipe .form-label { font-weight:600; color:#334155; margin-bottom:.4rem; }
.modal-recipe .form-control, .modal-recipe .form-select { min-height:46px; border-radius:14px; }
.modal-recipe .top-logo-row { display:flex; align-items:center; justify-content:space-between; gap:1rem; }
.modal-recipe .recipe-logo { max-height:54px; width:auto; object-fit:contain; }
.recipe-security-overlay { position:absolute; inset:0; z-index:30; background:rgba(15,23,42,.58); display:flex; align-items:center; justify-content:center; padding:1.25rem; backdrop-filter:blur(2px); }
.recipe-security-overlay.d-none { display:none !important; }
.recipe-security-card { width:100%; max-width:460px; background:#fff; border-radius:22px; border:1px solid #dbe4f0; box-shadow:0 30px 70px rgba(15,23,42,.28); padding:1.25rem; }
.recipe-security-title { font-size:1.25rem; font-weight:700; color:#0f172a; margin-bottom:.15rem; }
.recipe-security-subtitle { font-size:.95rem; color:#64748b; margin-bottom:1rem; }
.order-kind-tabs .nav-link { border:0; border-radius:14px; padding:.8rem 1rem; color:#334155; background:#fff; box-shadow:inset 0 0 0 1px #dbe4f0; font-weight:600; }
.order-kind-tabs .nav-link.active { background:linear-gradient(135deg,#0b1220 0%,#1b6ca8 100%); color:#fff; box-shadow:none; }
.order-badge { display:inline-flex; align-items:center; gap:.45rem; border-radius:999px; padding:.35rem .75rem; background:#eef6fb; color:#1b6ca8; font-weight:700; font-size:.84rem; }
.order-card-title { font-weight:700; color:#0f172a; }
.order-subtext { color:#64748b; font-size:.92rem; }
.subtype-grid { display:grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap:.75rem; }
.subtype-card { position:relative; border:1px solid #dce5ef; border-radius:16px; padding:.85rem .95rem; background:#f8fbfe; cursor:pointer; transition:all .18s ease; }
.subtype-card:hover { border-color:#89b7dd; box-shadow:0 10px 22px rgba(15,23,42,.06); }
.subtype-card.active { background:linear-gradient(135deg,#0d4d7a 0%,#1b6ca8 100%); color:#fff; border-color:transparent; box-shadow:0 14px 30px rgba(27,108,168,.28); }
.subtype-card input { position:absolute; inset:0; opacity:0; cursor:pointer; }
.subtype-card-title { font-weight:700; font-size:.95rem; }
.subtype-card-desc { font-size:.82rem; opacity:.88; margin-top:.2rem; }
.smart-form-section { display:none; }
.smart-form-section.active { display:block; }
.smart-summary-box { border-radius:16px; background:#f8fbfe; border:1px dashed #c9d7e6; padding:1rem; }
.smart-summary-box h6 { margin-bottom:.35rem; color:#0f172a; font-weight:700; }
.smart-summary-box p { margin-bottom:0; color:#64748b; font-size:.92rem; }
</style>
<div class="page-toolbar">
<div>
<h2 class="h4 mb-1">Órdenes</h2>
<p class="text-muted mb-0">Emisión documental clínica centralizada para recetas, prácticas, informes y resultados.</p>
</div>
<button class="btn btn-primary" id="newRecipeBtn" data-bs-toggle="modal" data-bs-target="#recipeModal" {% if not selected_patient_id %}disabled{% endif %}>
<i class="bi {{ active_kind_meta.icon }}"></i> Nueva {{ active_kind_meta.label|lower }}
</button>
</div>
<div class="card table-panel mb-3">
<div class="card-body d-flex flex-column gap-3">
<div class="d-flex flex-wrap gap-2 order-kind-tabs">
{% for kind in order_kind_options %}
<a class="nav-link {% if active_kind == kind.code %}active{% endif %}" href="{{ url_for('admin_recipes', kind=kind.code, patient_id=selected_patient_id, q=request.args.get('q',''), status=request.args.get('status','')) }}">
<i class="bi {{ order_kind_meta.get(kind.code, {}).get('icon', 'bi-file-earmark-medical') }} me-1"></i>{{ kind.name }}
</a>
{% endfor %}
</div>
<form method="get" class="row g-3 align-items-end filter-toolbar mb-0">
<input type="hidden" name="kind" value="{{ active_kind }}">
<div class="col-lg-5">
<label class="form-label">Paciente</label>
<select class="form-select searchable-select patient-select" id="recipesFilterPatientSelect" data-placeholder="Buscar por apellido, nombre o DNI">
<option value="">Seleccionar</option>
{% for p in patients %}
<option value="{{ p.id }}" data-email="{{ p.email or '' }}" data-phone="{{ p.telefono or '' }}" {% if selected_patient_id == p.id %}selected{% endif %}>{{ p.apellido }}, {{ p.nombre }} · DNI {{ p.documento }}</option>
{% endfor %}
</select>
<input type="hidden" name="patient_id" id="recipesFilterPatientId" class="patient-id" value="{{ selected_patient_id or '' }}">
</div>
<div class="col-lg-3">
<label class="form-label">Filtro rápido</label>
<input class="form-control" name="q" value="{{ request.args.get('q','') }}" placeholder="N° legal, CUIR, paciente o profesional">
</div>
<div class="col-lg-2">
<label class="form-label">Estado</label>
<select class="form-select searchable-select" name="status" data-placeholder="Todos">
<option value="">Todos</option>
{% for st in statuses %}
<option value="{{ st }}" {% if request.args.get('status') == st %}selected{% endif %}>{{ st }}</option>
{% endfor %}
</select>
</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_recipes', kind=active_kind) }}">Limpiar</a>
</div>
</form>
</div>
</div>
<div class="card table-panel">
<div class="card-header d-flex justify-content-between align-items-center flex-wrap gap-2">
<div>
<div class="order-badge"><i class="bi {{ active_kind_meta.icon }}"></i> {{ active_kind_meta.plural }}</div>
<div class="small text-muted mt-1">{{ selected_patient.nombre_completo if selected_patient else 'Primero buscá un paciente para trabajar con sus documentos.' }}</div>
</div>
{% if selected_patient %}
<div class="small text-muted">Paciente actual: <strong>{{ selected_patient.nombre_completo }}</strong></div>
{% endif %}
</div>
<div class="card-body p-0">
{% if not selected_patient_id and not request.args.get('q') %}
<div class="empty-state">
<i class="bi bi-search"></i>
<h3>Buscá un paciente para ver sus {{ active_kind_meta.plural|lower }}</h3>
<p>Esto mejora la seguridad operativa y evita emitir documentos sobre un paciente equivocado.</p>
</div>
{% else %}
<div class="table-responsive wide-table">
<table class="table align-middle mb-0">
<thead>
<tr>
<th>Fecha</th>
<th>Paciente</th>
<th>Profesional</th>
<th>Documento</th>
<th>Detalle</th>
<th>Estado</th>
<th class="text-end">Acciones</th>
</tr>
</thead>
<tbody>
{% for item in recipes %}
<tr>
<td>{{ item.prescription_date.strftime('%d/%m/%Y') }}<br><span class="small text-muted">{{ item.issued_at.strftime('%H:%M') if item.issued_at else '' }}</span></td>
<td>{{ item.patient_full_name }}<br><span class="small text-muted">DNI {{ item.patient_document }}</span></td>
<td>{{ item.professional_display_name }}<br><span class="small text-muted">{{ item.professional_specialty or item.professional_profession_name or '—' }}</span></td>
<td>
<div class="order-card-title">{{ item.document_title or item.medication_generic_name or active_kind_meta.label }}</div>
<div class="small text-muted">{{ item.legal_number }}</div>
{% if item.cuir %}<div class="small text-muted">{{ item.cuir }}</div>{% endif %}
</td>
<td>
{% if active_kind == 'recipe' %}
{{ item.medication_presentation or '—' }}{% if item.pharmaceutical_form %} · {{ item.pharmaceutical_form }}{% endif %}
{% if item.quantity_units %}<div class="small text-muted">Cantidad: {{ item.quantity_units }}</div>{% endif %}
{% else %}
{{ (item.document_body or item.dosage_instructions or 'Sin detalle')[:120] }}{% if (item.document_body or item.dosage_instructions or '')|length > 120 %}...{% endif %}
{% endif %}
</td>
<td>
<span class="badge {% if item.computed_status == 'Activa' %}text-bg-success{% elif item.computed_status == 'Vencida' %}text-bg-warning{% else %}text-bg-secondary{% endif %}">{{ item.computed_status }}</span>
</td>
<td class="text-end">
<div class="d-flex justify-content-end gap-2 flex-wrap">
<a class="btn btn-sm btn-outline-primary" target="_blank" href="{{ url_for('admin_recipe_pdf', recipe_id=item.id) }}">PDF</a>
{% if active_kind == 'recipe' %}
<a class="btn btn-sm btn-outline-secondary" target="_blank" href="{{ url_for('public_verify_recipe', q=item.cuir) }}">Verificar</a>
{% endif %}
<form method="post" action="{{ url_for('admin_recipe_send_email', recipe_id=item.id) }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button class="btn btn-sm btn-outline-success">Email</button>
</form>
<form method="post" action="{{ url_for('admin_recipe_status', recipe_id=item.id) }}" class="d-flex gap-2 flex-wrap justify-content-end">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="status" value="Suspendida">
<button class="btn btn-sm btn-outline-danger">Suspender</button>
</form>
</div>
</td>
</tr>
{% else %}
<tr><td colspan="7" class="text-center text-muted py-4">Todavía no hay {{ active_kind_meta.plural|lower }} registradas para este criterio.</td></tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
</div>
{% if pagination and pagination.pages > 1 %}
<nav class="mt-3">
<ul class="pagination justify-content-center">
{% if pagination.has_prev %}
<li class="page-item"><a class="page-link" href="{{ url_for('admin_recipes', page=pagination.prev_num, kind=active_kind, patient_id=selected_patient_id, q=request.args.get('q',''), status=request.args.get('status','')) }}">Anterior</a></li>
{% endif %}
{% for page_num in pagination.iter_pages(left_edge=1, left_current=1, right_current=2, right_edge=1) %}
{% if page_num %}
<li class="page-item {% if page_num == pagination.page %}active{% endif %}"><a class="page-link" href="{{ url_for('admin_recipes', page=page_num, kind=active_kind, patient_id=selected_patient_id, q=request.args.get('q',''), status=request.args.get('status','')) }}">{{ page_num }}</a></li>
{% else %}
<li class="page-item disabled"><span class="page-link"></span></li>
{% endif %}
{% endfor %}
{% if pagination.has_next %}
<li class="page-item"><a class="page-link" href="{{ url_for('admin_recipes', page=pagination.next_num, kind=active_kind, patient_id=selected_patient_id, q=request.args.get('q',''), status=request.args.get('status','')) }}">Siguiente</a></li>
{% endif %}
</ul>
</nav>
{% endif %}
<div class="modal fade modal-recipe" id="recipeModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<div>
<h5 class="modal-title">Nueva {{ active_kind_meta.label|lower }}</h5>
<div class="small text-light opacity-75">Documento auditado con revalidación obligatoria de credenciales.</div>
</div>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Cerrar"></button>
</div>
<form method="post" id="recipeCreateForm" class="recipe-form-shell">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<input type="hidden" name="action" value="create">
<input type="hidden" name="document_kind" value="{{ active_kind }}">
<input type="hidden" name="auth_email" id="recipe_auth_email_hidden">
<input type="hidden" name="auth_password" id="recipe_auth_password_hidden">
<input type="hidden" name="document_title" id="smartDocumentTitle">
<input type="hidden" name="document_body" id="smartDocumentBody">
<input type="hidden" name="medication_generic_name" id="smartMedicationGenericName">
<div class="recipe-modal-shell">
<div class="modal-body">
<div class="row g-4">
<div class="col-12 top-logo-row">
{% if site_settings.logo_path %}<img src="{{ url_for('static', filename=site_settings.logo_path) }}" alt="logo" class="recipe-logo">{% endif %}
<div class="text-end small text-muted">La emisión queda registrada con paciente, profesional, hora y credenciales validadas.</div>
</div>
<div class="col-12">
<div class="section-card">
<div class="section-title">Paciente y profesional</div>
<div class="row g-3">
<div class="col-lg-8">
<label class="form-label">Paciente</label>
<select class="form-select searchable-select patient-select" id="recipeModalPatientSelect" data-placeholder="Buscar por apellido, nombre o DNI">
<option value="">Seleccionar</option>
{% for p in patients %}
<option value="{{ p.id }}" data-email="{{ p.email or '' }}" data-phone="{{ p.telefono or '' }}" {% if selected_patient_id == p.id %}selected{% endif %}>{{ p.apellido }}, {{ p.nombre }} · DNI {{ p.documento }}</option>
{% endfor %}
</select>
<input type="hidden" name="patient_id" id="recipeModalPatientId" class="patient-id" value="{{ selected_patient_id or '' }}">
</div>
{% if current_user.role == 'admin' %}
<div class="col-lg-4">
<label class="form-label">Profesional emisor</label>
<select class="form-select searchable-select" name="professional_id" required data-placeholder="Seleccionar profesional">
{% for p in professionals %}<option value="{{ p.id }}">{{ p.display_name }} · {{ p.specialty }}</option>{% endfor %}
</select>
</div>
{% endif %}
<div class="col-md-3">
<label class="form-label">Fecha</label>
<input class="form-control" type="date" name="prescription_date" value="{{ today_iso() }}">
</div>
<div class="col-md-3 {% if active_kind != 'recipe' %}d-none{% endif %}" data-order-role="recipe-only">
<label class="form-label">Tipo</label>
<select class="form-select searchable-select" name="prescription_type" id="prescription_type_select">
{% for item in type_options %}<option value="{{ item.code }}">{{ item.code }} · {{ item.name }}</option>{% endfor %}
</select>
</div>
<div class="col-md-3 {% if active_kind != 'recipe' %}d-none{% endif %}" data-order-role="recipe-only">
<label class="form-label">Subtipo</label>
<select class="form-select searchable-select" name="prescription_subtype" id="prescription_subtype_select">
{% for item in subtype_options %}<option value="{{ item.code }}" {% if item.code=='02' %}selected{% endif %}>{{ item.code }} · {{ item.name }}</option>{% endfor %}
</select>
</div>
<div class="col-md-3">
<label class="form-label">Número de ítem</label>
<input class="form-control field-placeholder" name="item_number" value="01" placeholder="01">
</div>
<div class="col-12">
<label class="form-label">Grupo documental</label>
<input class="form-control field-placeholder" name="prescription_group" placeholder="Opcional. Si queda vacío, el sistema lo genera automáticamente.">
</div>
</div>
</div>
</div>
<div class="col-lg-6 {% if active_kind != 'recipe' %}d-none{% endif %}" data-order-role="recipe-only">
<div class="section-card h-100">
<div class="section-title">Contenido de receta</div>
<div class="row g-3">
<div class="col-md-8"><label class="form-label">Medicamento / DCI</label><input class="form-control field-placeholder" id="recipeMedicationGeneric" name="medication_generic_name_visible" placeholder="Ej.: Amoxicilina 500 mg" {% if active_kind == 'recipe' %}required{% endif %}></div>
<div class="col-md-4"><label class="form-label">Cantidad</label><input class="form-control field-placeholder" name="quantity_units" placeholder="Ej.: 1 caja"></div>
<div class="col-md-6"><label class="form-label">Presentación</label><input class="form-control field-placeholder" name="medication_presentation" placeholder="Caja x 30 g · Tabletas · Ampollas"></div>
<div class="col-md-6"><label class="form-label">Forma farmacéutica</label><input class="form-control field-placeholder" name="pharmaceutical_form" placeholder="Crema, comprimidos, solución..."></div>
<div class="col-12"><label class="form-label">Posología / indicaciones</label><textarea class="form-control field-placeholder" name="dosage_instructions" rows="4" placeholder="1 comprimido cada 8 horas por 7 días"></textarea></div>
</div>
</div>
</div>
<div class="col-lg-6 {% if active_kind == 'recipe' %}d-none{% endif %}" data-order-role="generic-only">
<div class="section-card h-100">
<div class="section-title">Plantilla profesional</div>
<div class="smart-summary-box mb-3">
<h6>Formulario inteligente</h6>
<p>Seleccioná el subtipo del documento. El sistema te mostrará un formulario específico y generará automáticamente el título y el cuerpo institucional del PDF.</p>
</div>
<div class="mb-3">
<label class="form-label">Subtipo documental</label>
<div class="subtype-grid" id="subtypeCardGrid"></div>
<input type="hidden" id="smartSubtypeCode" name="smart_subtype_code" value="">
</div>
<div id="smartFormSections">
<div class="smart-form-section" data-kind="practice" data-subtype="lab">
<div class="row g-3">
<div class="col-12"><label class="form-label">Perfil / estudio solicitado</label><input class="form-control" data-smart-field="practice_lab_study" placeholder="Ej.: Laboratorio general · Hemograma + glucemia + hepatograma"></div>
<div class="col-md-6"><label class="form-label">Prioridad</label><select class="form-select" data-smart-field="practice_lab_priority"><option value="Rutina">Rutina</option><option value="Preferente">Preferente</option><option value="Urgente">Urgente</option></select></div>
<div class="col-md-6"><label class="form-label">Ayuno / preparación</label><input class="form-control" data-smart-field="practice_lab_prep" placeholder="Ej.: Ayuno de 8 horas"></div>
<div class="col-12"><label class="form-label">Observaciones clínicas</label><textarea class="form-control" rows="4" data-smart-field="practice_lab_notes" placeholder="Motivo de pedido, datos clínicos, antecedentes relevantes"></textarea></div>
</div>
</div>
<div class="smart-form-section" data-kind="practice" data-subtype="imaging">
<div class="row g-3">
<div class="col-md-8"><label class="form-label">Estudio por imágenes</label><input class="form-control" data-smart-field="practice_imaging_study" placeholder="Ej.: Rx tórax frente y perfil · RMN columna lumbar"></div>
<div class="col-md-4"><label class="form-label">Con / sin contraste</label><select class="form-select" data-smart-field="practice_imaging_contrast"><option value="Sin contraste">Sin contraste</option><option value="Con contraste">Con contraste</option><option value="A criterio">A criterio profesional</option></select></div>
<div class="col-md-6"><label class="form-label">Región anatómica</label><input class="form-control" data-smart-field="practice_imaging_region" placeholder="Ej.: Rodilla derecha"></div>
<div class="col-md-6"><label class="form-label">Prioridad</label><select class="form-select" data-smart-field="practice_imaging_priority"><option value="Rutina">Rutina</option><option value="Preferente">Preferente</option><option value="Urgente">Urgente</option></select></div>
<div class="col-12"><label class="form-label">Datos clínicos</label><textarea class="form-control" rows="4" data-smart-field="practice_imaging_notes" placeholder="Sospecha diagnóstica, lateralidad, antecedentes, objetivo del estudio"></textarea></div>
</div>
</div>
<div class="smart-form-section" data-kind="practice" data-subtype="interconsult">
<div class="row g-3">
<div class="col-md-6"><label class="form-label">Especialidad / servicio</label><input class="form-control" data-smart-field="practice_interconsult_service" placeholder="Ej.: Cardiología · Kinesiología"></div>
<div class="col-md-6"><label class="form-label">Motivo de derivación</label><input class="form-control" data-smart-field="practice_interconsult_reason" placeholder="Ej.: Evaluación especializada"></div>
<div class="col-12"><label class="form-label">Resumen clínico</label><textarea class="form-control" rows="4" data-smart-field="practice_interconsult_summary" placeholder="Antecedentes, cuadro actual, objetivo de la interconsulta"></textarea></div>
<div class="col-12"><label class="form-label">Solicitud concreta</label><textarea class="form-control" rows="3" data-smart-field="practice_interconsult_request" placeholder="Qué se requiere del especialista o del servicio"></textarea></div>
</div>
</div>
<div class="smart-form-section" data-kind="report" data-subtype="constancy">
<div class="row g-3">
<div class="col-md-6"><label class="form-label">Tipo de constancia</label><input class="form-control" data-smart-field="report_constancy_type" placeholder="Ej.: Constancia de atención"></div>
<div class="col-md-6"><label class="form-label">Fecha / rango</label><input class="form-control" data-smart-field="report_constancy_period" placeholder="Ej.: 22/04/2026 · del 22/04 al 24/04"></div>
<div class="col-12"><label class="form-label">Texto base</label><textarea class="form-control" rows="4" data-smart-field="report_constancy_body" placeholder="Detalle profesional de la constancia a emitir"></textarea></div>
</div>
</div>
<div class="smart-form-section" data-kind="report" data-subtype="fitness">
<div class="row g-3">
<div class="col-md-6"><label class="form-label">Finalidad</label><input class="form-control" data-smart-field="report_fitness_purpose" placeholder="Ej.: Apto físico escolar · Actividad deportiva"></div>
<div class="col-md-6"><label class="form-label">Resultado del apto</label><select class="form-select" data-smart-field="report_fitness_result"><option value="APTO">APTO</option><option value="APTO CON OBSERVACIONES">APTO CON OBSERVACIONES</option><option value="NO APTO">NO APTO</option></select></div>
<div class="col-12"><label class="form-label">Fundamentos / observaciones</label><textarea class="form-control" rows="5" data-smart-field="report_fitness_notes" placeholder="Evaluación realizada, recomendaciones, restricciones si corresponden"></textarea></div>
</div>
</div>
<div class="smart-form-section" data-kind="report" data-subtype="medical-report">
<div class="row g-3">
<div class="col-md-6"><label class="form-label">Destino / receptor</label><input class="form-control" data-smart-field="report_target" placeholder="Ej.: Obra social · Empresa · Institución"></div>
<div class="col-md-6"><label class="form-label">Tema del informe</label><input class="form-control" data-smart-field="report_subject" placeholder="Ej.: Evolución clínica breve"></div>
<div class="col-12"><label class="form-label">Contenido del informe</label><textarea class="form-control" rows="5" data-smart-field="report_body" placeholder="Resumen clínico, evolución, hallazgos y recomendaciones"></textarea></div>
</div>
</div>
<div class="smart-form-section" data-kind="result" data-subtype="lab-result">
<div class="row g-3">
<div class="col-md-6"><label class="form-label">Panel / estudio</label><input class="form-control" data-smart-field="result_lab_panel" placeholder="Ej.: Hemograma completo"></div>
<div class="col-md-6"><label class="form-label">Conclusión rápida</label><input class="form-control" data-smart-field="result_lab_conclusion" placeholder="Ej.: Sin alteraciones significativas"></div>
<div class="col-12"><label class="form-label">Resultados principales</label><textarea class="form-control" rows="5" data-smart-field="result_lab_values" placeholder="Valores, interpretación y correlación clínica"></textarea></div>
</div>
</div>
<div class="smart-form-section" data-kind="result" data-subtype="imaging-result">
<div class="row g-3">
<div class="col-md-6"><label class="form-label">Estudio</label><input class="form-control" data-smart-field="result_imaging_study" placeholder="Ej.: Ecografía abdominal"></div>
<div class="col-md-6"><label class="form-label">Conclusión</label><input class="form-control" data-smart-field="result_imaging_conclusion" placeholder="Ej.: Hallazgos compatibles con..."></div>
<div class="col-12"><label class="form-label">Descripción</label><textarea class="form-control" rows="5" data-smart-field="result_imaging_body" placeholder="Informe descriptivo, interpretación y sugerencias"></textarea></div>
</div>
</div>
<div class="smart-form-section" data-kind="result" data-subtype="pathology">
<div class="row g-3">
<div class="col-md-6"><label class="form-label">Muestra / material</label><input class="form-control" data-smart-field="result_path_material" placeholder="Ej.: Biopsia de piel"></div>
<div class="col-md-6"><label class="form-label">Diagnóstico anatómico</label><input class="form-control" data-smart-field="result_path_diagnosis" placeholder="Ej.: Nevus melanocítico benigno"></div>
<div class="col-12"><label class="form-label">Detalle del resultado</label><textarea class="form-control" rows="5" data-smart-field="result_path_body" placeholder="Microscopía, diagnóstico e interpretación"></textarea></div>
</div>
</div>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="section-card h-100">
<div class="section-title">Control clínico y legal</div>
<div class="row g-3">
<div class="col-12"><label class="form-label">Diagnóstico</label><input class="form-control field-placeholder" name="diagnosis" placeholder="Diagnóstico o motivo de indicación"></div>
<div class="col-12"><label class="form-label">Notas internas</label><textarea class="form-control field-placeholder" name="internal_notes" rows="4" placeholder="Comentarios internos del profesional o del sistema"></textarea></div>
<div class="col-12">
<div class="alert alert-light border mb-0">El documento incluirá PDF institucional, código documental y firma institucional de emisión.</div>
</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 type="button" class="btn btn-primary" id="openRecipeAuthOverlayBtn">Generar {{ active_kind_meta.label|lower }}</button>
</div>
<div class="recipe-security-overlay d-none" id="recipeAuthOverlay">
<div class="recipe-security-card">
<div class="recipe-security-title">Validación de credenciales</div>
<div class="recipe-security-subtitle">Segundo factor operativo</div>
<p class="recipe-security-text">Para emitir la {{ active_kind_meta.label|lower }}, reingresá credenciales del profesional o administrador. Este evento queda auditado.</p>
<div class="mb-3"><label class="form-label">Usuario</label><input type="email" class="form-control field-placeholder" id="recipeAuthEmail" value="{{ current_user.email }}" placeholder="usuario@dominio.com"></div>
<div class="mb-3"><label class="form-label">Contraseña</label><input type="password" class="form-control field-placeholder" id="recipeAuthPassword" placeholder="Ingresá tu contraseña"></div>
<div class="alert alert-danger d-none mb-3" id="recipeAuthError"></div>
<div class="d-flex justify-content-end gap-2"><button type="button" class="btn btn-outline-secondary" id="cancelRecipeAuthOverlayBtn">Cancelar</button><button type="button" class="btn btn-primary" id="confirmRecipeCreate">Validar y emitir</button></div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
{{ super() }}
<script>
document.addEventListener('DOMContentLoaded', function () {
const ACTIVE_KIND = {{ active_kind|tojson }};
const SMART_CONFIG = {
practice: [
{ code: 'lab', title: 'Laboratorio', desc: 'Análisis, perfiles bioquímicos y controles seriados.' },
{ code: 'imaging', title: 'Imágenes', desc: 'Radiografías, tomografías, ecografías o resonancias.' },
{ code: 'interconsult', title: 'Interconsulta', desc: 'Derivación a especialidad o a otro servicio.' }
],
report: [
{ code: 'constancy', title: 'Constancia', desc: 'Atención, reposo o certificación simple.' },
{ code: 'fitness', title: 'Apto físico', desc: 'Escolar, deportivo o laboral según evaluación.' },
{ code: 'medical-report', title: 'Informe clínico', desc: 'Resumen profesional para obra social o institución.' }
],
result: [
{ code: 'lab-result', title: 'Resultado laboratorio', desc: 'Carga e interpretación de estudios bioquímicos.' },
{ code: 'imaging-result', title: 'Resultado imágenes', desc: 'Informe e interpretación de diagnóstico por imágenes.' },
{ code: 'pathology', title: 'Anatomía patológica', desc: 'Resultado histopatológico o citológico.' }
]
};
const filterPatientSelect = document.getElementById('recipesFilterPatientSelect');
const filterPatientId = document.getElementById('recipesFilterPatientId');
const recipeForm = document.getElementById('recipeCreateForm');
const recipeModalPatientSelect = document.getElementById('recipeModalPatientSelect');
const recipeModalPatientId = document.getElementById('recipeModalPatientId');
const overlay = document.getElementById('recipeAuthOverlay');
const openOverlayBtn = document.getElementById('openRecipeAuthOverlayBtn');
const cancelOverlayBtn = document.getElementById('cancelRecipeAuthOverlayBtn');
const confirmCreateBtn = document.getElementById('confirmRecipeCreate');
const authEmail = document.getElementById('recipeAuthEmail');
const authPassword = document.getElementById('recipeAuthPassword');
const authError = document.getElementById('recipeAuthError');
const authEmailHidden = document.getElementById('recipe_auth_email_hidden');
const authPasswordHidden = document.getElementById('recipe_auth_password_hidden');
const subtypeGrid = document.getElementById('subtypeCardGrid');
const subtypeCodeInput = document.getElementById('smartSubtypeCode');
const smartSectionsWrap = document.getElementById('smartFormSections');
const titleHidden = document.getElementById('smartDocumentTitle');
const bodyHidden = document.getElementById('smartDocumentBody');
const medHidden = document.getElementById('smartMedicationGenericName');
const recipeMedicationVisible = document.getElementById('recipeMedicationGeneric');
function syncHiddenFromSelect(selectEl, hiddenEl) {
if (!selectEl || !hiddenEl) return;
hiddenEl.value = selectEl.value || '';
}
function setActiveSubtype(code) {
if (!subtypeCodeInput) return;
subtypeCodeInput.value = code || '';
document.querySelectorAll('.subtype-card').forEach(function(card){
card.classList.toggle('active', card.dataset.code === code);
});
document.querySelectorAll('.smart-form-section').forEach(function(section){
const visible = section.dataset.kind === ACTIVE_KIND && section.dataset.subtype === code;
section.classList.toggle('active', visible);
section.querySelectorAll('input, textarea, select').forEach(function(field){
field.required = visible;
});
});
}
function buildSubtypeCards() {
if (!subtypeGrid || !SMART_CONFIG[ACTIVE_KIND]) return;
subtypeGrid.innerHTML = SMART_CONFIG[ACTIVE_KIND].map(function(item, index){
return `\n <label class="subtype-card ${index === 0 ? 'active' : ''}" data-code="${item.code}">\n <input type="radio" name="smart_subtype_radio" value="${item.code}" ${index === 0 ? 'checked' : ''}>\n <div class="subtype-card-title">${item.title}</div>\n <div class="subtype-card-desc">${item.desc}</div>\n </label>`;
}).join('');
const first = SMART_CONFIG[ACTIVE_KIND][0];
setActiveSubtype(first ? first.code : '');
subtypeGrid.querySelectorAll('input[type="radio"]').forEach(function(radio){
radio.addEventListener('change', function(){ setActiveSubtype(radio.value); });
});
}
function getFieldValue(name) {
const el = document.querySelector(`[data-smart-field="${name}"]`);
return el ? String(el.value || '').trim() : '';
}
function composeSmartDocument() {
if (ACTIVE_KIND === 'recipe') {
if (medHidden && recipeMedicationVisible) medHidden.value = recipeMedicationVisible.value.trim();
if (titleHidden && recipeMedicationVisible) titleHidden.value = recipeMedicationVisible.value.trim();
return true;
}
const subtype = subtypeCodeInput ? subtypeCodeInput.value : '';
let title = '';
let body = '';
let primary = '';
if (ACTIVE_KIND === 'practice') {
if (subtype === 'lab') {
primary = getFieldValue('practice_lab_study');
title = `Orden de laboratorio · ${primary || 'Perfil solicitado'}`;
body = [
`Perfil / estudio: ${primary || 'No consignado'}`,
`Prioridad: ${getFieldValue('practice_lab_priority') || 'Rutina'}`,
`Preparación: ${getFieldValue('practice_lab_prep') || 'Sin preparación especial consignada'}`,
'',
'Observaciones clínicas:',
getFieldValue('practice_lab_notes') || 'Sin observaciones clínicas adicionales.'
].join('\n');
} else if (subtype === 'imaging') {
primary = getFieldValue('practice_imaging_study');
title = `Orden de diagnóstico por imágenes · ${primary || 'Estudio solicitado'}`;
body = [
`Estudio: ${primary || 'No consignado'}`,
`Región anatómica: ${getFieldValue('practice_imaging_region') || 'No consignada'}`,
`Contraste: ${getFieldValue('practice_imaging_contrast') || 'Sin contraste'}`,
`Prioridad: ${getFieldValue('practice_imaging_priority') || 'Rutina'}`,
'',
'Datos clínicos:',
getFieldValue('practice_imaging_notes') || 'Sin datos clínicos adicionales.'
].join('\n');
} else if (subtype === 'interconsult') {
primary = getFieldValue('practice_interconsult_service');
title = `Interconsulta / derivación · ${primary || 'Especialidad solicitada'}`;
body = [
`Especialidad / servicio: ${primary || 'No consignado'}`,
`Motivo de derivación: ${getFieldValue('practice_interconsult_reason') || 'No consignado'}`,
'',
'Resumen clínico:',
getFieldValue('practice_interconsult_summary') || 'Sin resumen clínico.',
'',
'Solicitud concreta:',
getFieldValue('practice_interconsult_request') || 'Sin solicitud adicional.'
].join('\n');
}
} else if (ACTIVE_KIND === 'report') {
if (subtype === 'constancy') {
primary = getFieldValue('report_constancy_type');
title = `${primary || 'Constancia médica'}`;
body = [
`Tipo de constancia: ${primary || 'No consignado'}`,
`Fecha / período: ${getFieldValue('report_constancy_period') || 'No consignado'}`,
'',
getFieldValue('report_constancy_body') || 'Sin detalle adicional.'
].join('\n');
} else if (subtype === 'fitness') {
primary = getFieldValue('report_fitness_purpose');
title = `Informe de aptitud · ${primary || 'Apto físico'}`;
body = [
`Finalidad: ${primary || 'No consignada'}`,
`Resultado del apto: ${getFieldValue('report_fitness_result') || 'APTO'}`,
'',
'Fundamentos / observaciones:',
getFieldValue('report_fitness_notes') || 'Sin observaciones adicionales.'
].join('\n');
} else if (subtype === 'medical-report') {
primary = getFieldValue('report_subject');
title = `${primary || 'Informe clínico médico'}`;
body = [
`Destino / receptor: ${getFieldValue('report_target') || 'No consignado'}`,
`Asunto: ${primary || 'No consignado'}`,
'',
getFieldValue('report_body') || 'Sin contenido adicional.'
].join('\n');
}
} else if (ACTIVE_KIND === 'result') {
if (subtype === 'lab-result') {
primary = getFieldValue('result_lab_panel');
title = `Resultado de laboratorio · ${primary || 'Panel informado'}`;
body = [
`Panel / estudio: ${primary || 'No consignado'}`,
`Conclusión rápida: ${getFieldValue('result_lab_conclusion') || 'Sin conclusión resumida'}`,
'',
'Resultados principales:',
getFieldValue('result_lab_values') || 'Sin detalle cargado.'
].join('\n');
} else if (subtype === 'imaging-result') {
primary = getFieldValue('result_imaging_study');
title = `Resultado de imágenes · ${primary || 'Estudio informado'}`;
body = [
`Estudio: ${primary || 'No consignado'}`,
`Conclusión: ${getFieldValue('result_imaging_conclusion') || 'Sin conclusión resumida'}`,
'',
'Descripción e interpretación:',
getFieldValue('result_imaging_body') || 'Sin detalle cargado.'
].join('\n');
} else if (subtype === 'pathology') {
primary = getFieldValue('result_path_material');
title = `Resultado anatomía patológica · ${primary || 'Muestra informada'}`;
body = [
`Muestra / material: ${primary || 'No consignado'}`,
`Diagnóstico anatómico: ${getFieldValue('result_path_diagnosis') || 'No consignado'}`,
'',
'Detalle del resultado:',
getFieldValue('result_path_body') || 'Sin detalle cargado.'
].join('\n');
}
}
if (titleHidden) titleHidden.value = title.trim();
if (bodyHidden) bodyHidden.value = body.trim();
if (medHidden) medHidden.value = (primary || title).trim();
return !!title.trim();
}
if (filterPatientSelect && filterPatientId) {
filterPatientSelect.addEventListener('change', function () { syncHiddenFromSelect(filterPatientSelect, filterPatientId); });
}
if (recipeModalPatientSelect && recipeModalPatientId) {
recipeModalPatientSelect.addEventListener('change', function () { syncHiddenFromSelect(recipeModalPatientSelect, recipeModalPatientId); });
}
buildSubtypeCards();
if (openOverlayBtn) {
openOverlayBtn.addEventListener('click', function () {
if (!recipeModalPatientId || !recipeModalPatientId.value) {
window.alert('Seleccioná un paciente para emitir el documento.');
return;
}
if (!composeSmartDocument()) {
window.alert('Completá el formulario profesional antes de emitir el documento.');
return;
}
if (!recipeForm.checkValidity()) {
recipeForm.reportValidity();
return;
}
authError.classList.add('d-none');
overlay.classList.remove('d-none');
authPassword.focus();
});
}
if (cancelOverlayBtn) {
cancelOverlayBtn.addEventListener('click', function () { overlay.classList.add('d-none'); });
}
if (confirmCreateBtn) {
confirmCreateBtn.addEventListener('click', function () {
if (!authEmail.value.trim() || !authPassword.value.trim()) {
authError.textContent = 'Ingresá usuario y contraseña para continuar.';
authError.classList.remove('d-none');
return;
}
authEmailHidden.value = authEmail.value.trim();
authPasswordHidden.value = authPassword.value;
recipeForm.submit();
});
}
});
</script>
{% endblock %}