fase 2.5: formularios digitales y consentimientos (modulo skeen_formularios: plantillas con 12 tipos de campo, versionado, respuestas con firma PNG y fotos antes/despues, portal publico con token HMAC y limite de subida, historial por paciente, permisos y notificacion de envio)

This commit is contained in:
2026-10-11 09:48:28 +00:00
parent 91f909a6d0
commit 9286ab8ee2
30 changed files with 3545 additions and 12 deletions

View File

@@ -3,13 +3,15 @@ import { useEffect, useState, useCallback, useRef } from 'react';
import {
Search, Plus, Activity, ShoppingBag, User, Edit2, Phone, Mail, Calendar, Droplet, Heart,
MapPin, Briefcase, Users, Baby, Stethoscope, FileText, AlertCircle, CheckCircle2, XCircle,
Download, ChevronRight, AlertTriangle, ClipboardList, Camera, Wallet, X,
Download, ChevronRight, AlertTriangle, ClipboardList, Camera, Wallet, X, FileSignature,
} from 'lucide-react';
import Layout from '../components/Layout';
import PatientAdjuntos from '../components/PatientAdjuntos';
import EstadoCuentaPrint from '../components/EstadoCuentaPrint';
import RecargarSaldoModal from '../components/RecargarSaldoModal';
import ReciboPrint from '../components/ReciboPrint';
import FormulariosPaciente from '../components/FormulariosPaciente';
import EnviarFormularioModal from '../components/formulario/EnviarFormularioModal';
import { usePatientDrawer } from '../components/PatientDrawer';
import {
Card,
@@ -144,10 +146,12 @@ const Pacientes: FC = () => {
const [recargaOpen, setRecargaOpen] = useState(false);
const [reciboDeposito, setReciboDeposito] = useState<ReceiptData | null>(null);
const [historyLoading, setHistoryLoading] = useState(false);
const [tab, setTab] = useState<'info' | 'clinical' | 'timeline' | 'docs'>('info');
const [tab, setTab] = useState<'info' | 'clinical' | 'timeline' | 'docs' | 'formularios'>('info');
const [entryDetail, setEntryDetail] = useState<
{ kind: 'cita'; item: Appointment } | { kind: 'venta'; item: Sale } | { kind: 'visita'; item: Visita } | null
>(null);
// Envío de formulario desde el detalle de la cita del historial
const [enviarFormularioCita, setEnviarFormularioCita] = useState<{ patientId: number; patientName: string; citaId: number } | null>(null);
const [exporting, setExporting] = useState(false);
const [onlyIncomplete, setOnlyIncomplete] = useState(false);
const [onlyVip, setOnlyVip] = useState(false);
@@ -1002,6 +1006,7 @@ const Pacientes: FC = () => {
{ key: 'clinical', label: 'Historia clínica' },
{ key: 'timeline', label: 'Historial' },
{ key: 'docs', label: 'Documentos' },
{ key: 'formularios', label: 'Formularios' },
] as const).map(({ key, label }) => (
<button
key={key}
@@ -1192,6 +1197,10 @@ const Pacientes: FC = () => {
</>
)}
</div>
) : tab === 'formularios' ? (
selected ? (
<FormulariosPaciente patientId={selected.id} patientName={selected.name} />
) : null
) : (
timelineItems.length > 0 ? (
<div className="space-y-2">
@@ -1253,6 +1262,20 @@ const Pacientes: FC = () => {
onClose={() => setEntryDetail(null)}
title={entryDetail?.kind === 'cita' ? 'Detalle de la cita' : entryDetail?.kind === 'visita' ? 'Detalle de la visita' : 'Detalle de la venta'}
maxWidth="lg"
footer={
entryDetail?.kind === 'cita' && can('formularios.enviar') ? (
<Button
variant="outline"
onClick={() => setEnviarFormularioCita({
patientId: entryDetail.item.patient_id,
patientName: entryDetail.item.patient,
citaId: entryDetail.item.id,
})}
>
<FileSignature size={16} className="mr-2" /> Enviar formulario
</Button>
) : undefined
}
>
{entryDetail?.kind === 'cita' && (
<div className="space-y-4">
@@ -1399,6 +1422,16 @@ const Pacientes: FC = () => {
}}
/>
<ReciboPrint receipt={reciboDeposito} onClose={() => setReciboDeposito(null)} />
<EnviarFormularioModal
isOpen={!!enviarFormularioCita}
onClose={() => setEnviarFormularioCita(null)}
patientId={enviarFormularioCita?.patientId ?? null}
patientName={enviarFormularioCita?.patientName}
citaId={enviarFormularioCita?.citaId ?? null}
onSent={() => {
if (selected?.id) setTab('formularios');
}}
/>
</Layout>
);
};