/* Lote «Páginas recuperadas del sitio actual».
*
* Nueve destinos: FIT, Helicobacter pylori, Check Up, Acerca de, Formatos de
* informe médico y cuatro fichas de sucursal.
*
* Los datos públicos viven en `src/recuperadas-data.js`; el registro de qué se
* rescató, qué se excluyó y por qué, en
* docs/BITACORA-PAGINAS-RECUPERADAS-v0.1.md. Las fichas de sucursal leen
* dirección, teléfonos, horario y mapa de la API, nunca de la capa local.
*/
const RD = () => window.RECUPERADAS_DATA;
// Sólo una aprobación explícita habilita contenido clínico pendiente.
const rcpPublicar = (clave) => {
const d = RD();
return !!(d && d.PUBLICACION_CLINICA && d.PUBLICACION_CLINICA[clave] === true);
};
function RecuperadasStyles() {
return ;
}
/* ───────────────────────── piezas compartidas ───────────────────────── */
function RcpCrumbs({ items }) {
return (
);
}
function RcpPasos({ pasos, azul }) {
if (azul) {
return (
{pasos.map((p, i) => (
{p.n}
{p.img ? : p.chat && }
{p.titulo}
{p.texto}
))}
);
}
return (
{pasos.map((p) => (
{p.img && (
)}
{p.n}{p.titulo}
{p.texto}
))}
);
}
/* Animación de aparición al hacer scroll. Un solo IntersectionObserver para
* toda la página y sin librerías. Solo oculta contenido si el navegador puede
* mostrarlo después y si el usuario no pidió reducir movimiento; los bloques
* que llegan tarde de la API se detectan con un MutationObserver. */
function useRevela(ref) {
React.useEffect(() => {
const raiz = ref.current;
if (!raiz || !("IntersectionObserver" in window)) return;
if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
raiz.classList.add("rcp-anim");
const io = new IntersectionObserver((entradas) => {
entradas.forEach((e) => {
if (e.isIntersecting) { e.target.classList.add("is-visible"); io.unobserve(e.target); }
});
}, { rootMargin: "0px 0px -8% 0px", threshold: 0.12 });
const vigilar = () => raiz.querySelectorAll(".rcp-revela:not(.is-visible)").forEach((el) => io.observe(el));
vigilar();
const mo = new MutationObserver(vigilar);
mo.observe(raiz, { childList: true, subtree: true });
return () => { io.disconnect(); mo.disconnect(); raiz.classList.remove("rcp-anim"); };
}, []);
}
/* Estudios en vivo desde InfectoApp. Precio, entrega, muestra y contenedor
* salen de la API (fuente de verdad); la landing solo decide cuáles mostrar.
* Un id que falle se omite: la sección nunca enseña un precio inventado. */
function useEstudiosApi(ids) {
const [lista, setLista] = React.useState(null);
const llave = (ids || []).join(",");
React.useEffect(() => {
if (!ids || !ids.length || !window.InfectoAPI) { setLista([]); return; }
let vivo = true;
const E = window.InfectoEnrich;
Promise.all(ids.map((id) =>
window.InfectoAPI.getEstudio(id)
.then((e) => (E ? E.enriquecer(e) : e))
.catch(() => null)
)).then((r) => { if (vivo) setLista(r); });
return () => { vivo = false; };
}, [llave]);
return lista;
}
function RcpPrecio({ s }) {
return s.precio.consultar
? Consultar precio
: <>{s.precio.texto} MXN>;
}
/* Ficha de la prueba principal con su precio público. `nombre` es el nombre
* comercial (editorial); los datos duros siguen siendo los de la API. */
function RcpFicha({ id, nombre, openStudy, lead }) {
const d = RD();
const lista = useEstudiosApi([id]);
const s = lista && lista[0] && (nombre ? Object.assign({}, lista[0], { titulo: nombre }) : lista[0]);
if (lista && !s) return null;
return (
);
}
function rcpIrA(id) {
const el = document.getElementById(id);
if (el) el.scrollIntoView({ behavior: "smooth", block: "start" });
}
/* Hero azul de las landings. `arte` es la ilustración o foto de la derecha. */
function RcpHeroAzul({ crumbs, eyebrow, icono, titulo, enfasis, entrada, cta, arte }) {
return (
{eyebrow}
{titulo} {enfasis}
{entrada}
{cta}
{arte}
);
}
function RcpArteChips({ chips }) {
return (chips || []).map((c, i) => (
{c.texto}
));
}
/* Variante con fotografía: la foto va recortada en círculo dentro de los anillos. */
function RcpArteFoto({ src, alt, chips }) {
return (
);
}
function RcpClaves({ items }) {
return (
{items.map((c) => (
{c.titulo}{c.texto}
))}
);
}
/* CTA corto entre secciones: el lector no tiene que llegar al final para
* encontrar cómo pedir la prueba. */
function RcpCtaLinea({ texto, lead }) {
const d = RD();
return (
);
}
/* Conversación dibujada para los pasos sin fotografía. Es decorativa. */
function RcpChat({ yo, lab }) {
return (
InfectoLab
{yo}{lab}
);
}
/* Franja para médicos y clínicas: la orden de laboratorio es una herramienta
* profesional, no se le ofrece al paciente. */
function RcpPro() {
const d = RD();
return (
¿Eres médico o clínica?
Genera la orden de laboratorio para tu paciente desde InfectoApp.
);
}
/* El iframe se marca sin permisos extra y con título: es contenido de un
* tercero y carga sin consentimiento previo (pendiente del lote de cookies). */
function RcpVideo({ id, titulo }) {
if (!id) return null;
return (
);
}
function RcpCta({ navigate, lead, children }) {
const d = RD();
return (
Cada formato es el documento oficial de la aseguradora. Se abre y se
descarga desde InfectoApp, que es donde el laboratorio mantiene la
versión vigente.
{/* Página informativa: InfectoLab no tiene un área que oriente sobre
* formatos de aseguradora (Daniel, 21-oct-2026). Sin aviso de
* «escríbenos» ni botón de ayuda. */}
);
}
/* ═════════════════════ 6. FICHAS DE SUCURSAL ═════════════════════ */
function SucursalDetallePage({ page, navigate, onOpenLead }) {
const d = RD();
const ficha = d.sucursal(page);
const [respuesta, setRespuesta] = React.useState(null);
// La respuesta pertenece a una ficha: al cambiar la ruta, ni el primer
// render (antes del efecto) puede reutilizar dirección, mapa o enlaces.
const api = respuesta && respuesta.page === page ? respuesta.datos : null;
const cargando = !respuesta || respuesta.page !== page;
React.useEffect(() => {
let vivo = true;
setRespuesta(null);
window.InfectoAPI.getSucursales()
.then((lista) => {
if (!vivo || !ficha) return;
/* Cruce por nombre exacto: la API todavía no expone un identificador
* estable. Si no coincide, la ficha muestra fotos y contacto, nunca
* una dirección inventada. */
setRespuesta({ page, datos: lista.filter((s) => s.nombre === ficha.apiNombre)[0] || null });
})
.catch(() => {
// Sin API, se conserva el contacto editorial de la ficha actual.
if (vivo) setRespuesta({ page, datos: null });
});
return () => { vivo = false; };
}, [page]);
if (!ficha) return null;
return (
navigate("home") },
{ label: "Sucursales", go: () => navigate("sucursales") },
{ label: ficha.h1.replace("InfectoLab ", "") },
]} />
{ficha.zona}
{ficha.h1}
{ficha.intro}
Dirección
{cargando ? "Cargando…" : (api && api.direccion) || "Consulta la dirección con el laboratorio."}