Files
fspm-web/src/app/(sitio)/aviso-de-privacidad/page.tsx
T
Uriel JarethandClaude Opus 5 31ee899987 fix: defectos encontrados por la suite de extremo a extremo
La primera corrida completa dio 57 fallas. Solo una parte eran del
producto; el resto era instrumentación mal escrita. Queda en 261 en verde.

Defectos del producto:

- La atribución de campaña se perdía. La captura vivía en un efecto de
  React, así que si la persona llegaba con parámetros de campaña y
  navegaba antes de hidratar, el primer toque nunca se guardaba y
  utmCampaign llegaba vacío al CRM. Ahora se captura en un script en
  línea que corre al analizar el documento.
- Siete páginas sin og:image: una página que declara su propio openGraph
  no hereda las imágenes del layout, las sobreescribe. Solo se nota al
  compartir el enlace. Se resuelve con una función compartida en seo.ts.
- El panel no se podía usar desde Safari por HTTP: la cookie Secure no se
  guarda y el acceso falla en silencio. Queda explícito y documentado.
- El panel se corría de lado en móvil: la cabecera no cabía a 375px.
- Las tarjetas de perfil del acceso se cortaban en móvil. Un elemento de
  rejilla nace con min-width auto y no puede encogerse bajo su contenido.
  Era además el origen de un desplazamiento que se arrastraba al tablero.
- Las tablas accesibles de las gráficas volvían desplazable la página:
  sr-only fija width 1px y el algoritmo de tablas lo ignora.
- El campo de búsqueda del catálogo no tenía nombre accesible.
- Límite de tasa de 5 envíos por IP: bloqueaba una demostración en vivo
  con varias personas en la misma red. Configurable, ahora 20.
- El salto de contenido pasa de left:-9999px a recorte de 1px.

Instrumentación corregida, con la razón escrita en cada caso:

- El desborde horizontal se mide intentando desplazar la página, no con
  documentElement.scrollWidth, que sobreinforma con contenedores propios.
- La comprobación de accesibilidad respeta el árbol de accesibilidad.
- El aislamiento entre roles se prueba pidiendo por URL una oportunidad
  ajena, no comparando folios entre listados paginados.
- Fuera las esperas por networkidle, que agotaban el tiempo sin fallar.
- Que el panel no esté enlazado se comprueba leyendo el HTML servido.
- Safari no enfoca enlaces con Tab: declarado como del navegador.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-08-28 02:37:42 -06:00

473 lines
21 KiB
TypeScript

import type { Metadata } from "next";
import Link from "next/link";
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
import { Migas } from "@/components/site/contenido/Migas";
import { FOTOS, HeroeLanding } from "@/components/site/HeroeLanding";
import { AVISO_DEMO, EMPRESA, SITIO_URL } from "@/data/empresa";
import { imagenesOg } from "@/lib/seo";
/**
* /aviso-de-privacidad
*
* Reproduce, reordenado para poder leerse, el aviso publicado por FSPM en su
* sitio público, más una sección que declara qué recaba realmente el formulario
* de esta demo —cuatro campos visibles y el contexto de la página— y un resumen
* en lenguaje llano arriba, para que alguien pueda enterarse sin leer nueve
* apartados. Lo que no estaba en la fuente no se agregó, y la versión
* definitiva es siempre la que publique la empresa.
*
* Va con `robots: { index: false }` a propósito: es la reproducción de un
* documento legal de un tercero dentro de una demo, y no queremos que compita
* en buscadores con el aviso que la empresa publica en su propio dominio.
*/
const TITULO = "Aviso de privacidad";
const DESCRIPCION = `Aviso de privacidad de ${EMPRESA.razonSocial}: quién es el responsable, qué datos recaba este sitio, para qué se usan, a quién se transfieren y cómo ejercer tus derechos de acceso, rectificación, cancelación y oposición.`;
export const metadata: Metadata = {
title: TITULO,
description: DESCRIPCION,
alternates: { canonical: `${SITIO_URL}/aviso-de-privacidad` },
robots: { index: false, follow: true },
openGraph: {
title: `${TITULO} | ${EMPRESA.nombreCorto}`,
description: DESCRIPCION,
url: `${SITIO_URL}/aviso-de-privacidad`,
...imagenesOg(),
},
};
const CORREO_ARCO = "[email protected]";
const MENSAJE_INICIAL = "Mi duda sobre este sitio es ";
/** El aviso en cinco frases, para quien no va a leer los nueve apartados. */
const EN_CORTO = [
"Tus datos los trata Enforcer Units Fire Service Plus México, S.A. de C.V.",
"Se usan para identificarte, contactarte y darle seguimiento a tu solicitud.",
"El aviso original contempla también envío de invitaciones, noticias y campañas.",
"Contempla transferencias a asociados y a terceros para esas mismas finalidades.",
`Puedes pedir acceso, corrección, cancelación u oposición escribiendo a ${CORREO_ARCO}.`,
];
/**
* Qué recaba el formulario de este sitio. Cuatro campos visibles y el contexto
* que se deduce de la página, sin preguntarlo.
*/
const CAMPOS_VISIBLES = [
{ campo: "Nombre completo", uso: "Identificarte y dirigirnos a ti por tu nombre en el seguimiento." },
{ campo: "Correo electrónico", uso: "Enviarte la respuesta técnica y la propuesta." },
{ campo: "Teléfono", uso: "Contactarte cuando la solicitud requiere una llamada." },
{
campo: "Mensaje",
uso: "Conocer el requerimiento en tus palabras. Lo que escribas aquí es lo único que decides compartir.",
},
{
campo: "Aceptación de este aviso",
uso: "Registrar que aceptaste el tratamiento antes de enviar. Sin esa casilla no se envía nada.",
},
];
const CAMPOS_CONTEXTO = [
{
campo: "Página desde la que escribiste",
uso: "Saber si llegaste desde una ficha de producto, un segmento o la página de cumplimiento, para responder sobre eso.",
},
{
campo: "Producto, categoría, industria o escenario de esa página",
uso: "Interpretar el escenario sin obligarte a llenar un cuestionario técnico.",
},
{
campo: "Clases de fuego asociadas a esa página",
uso: "Preparar la selección de equipo antes de la primera llamada.",
},
{
campo: "Origen de la visita (campaña, buscador o enlace)",
uso: "Medir qué contenido resuelve dudas reales. No identifica a nadie por sí solo.",
},
];
export default function PaginaAviso() {
return (
<>
<HeroeLanding
antes={<Migas actual="Aviso de privacidad" />}
eyebrow="Documento legal"
titulo="Aviso de privacidad"
entrada={`Reproducción del aviso publicado por ${EMPRESA.razonSocial}, reordenado para poder leerse. La versión vigente y definitiva es la que la empresa publique en su propio sitio.`}
foto={FOTOS.mapa}
enfoque="45% 45%"
prioridad
>
<div
role="note"
aria-label="Naturaleza de este sitio"
className="max-w-2xl rounded-[6px] border border-rule border-l-2 border-l-ember bg-ink-2/90 p-5"
>
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-ember">
Sobre este sitio
</p>
<p className="mt-3 text-sm leading-relaxed text-muted">{AVISO_DEMO}</p>
</div>
</HeroeLanding>
{/* ---------- el aviso en corto ---------- */}
<section className="border-t border-rule bg-ink-2 py-12 sm:py-14" aria-labelledby="en-corto">
<div className="shell">
<div className="max-w-3xl">
<p className="eyebrow eyebrow-muted mb-4">En corto</p>
<h2 id="en-corto" className="font-display text-xl font-extrabold sm:text-2xl">
Lo esencial en cinco frases
</h2>
<p className="mt-3 text-sm leading-relaxed text-muted">
Este resumen no sustituye al aviso: es una entrada para que puedas leerlo sabiendo qué
buscar. El texto que manda es el de los nueve apartados de abajo.
</p>
<ul className="mt-6 space-y-3">
{EN_CORTO.map((punto) => (
<li key={punto} className="flex gap-3 text-sm leading-relaxed text-bright">
<span
aria-hidden="true"
className="mt-2 h-1.5 w-1.5 shrink-0 rounded-full bg-signal"
/>
{punto}
</li>
))}
</ul>
</div>
</div>
</section>
{/* ---------- el aviso ---------- */}
<article className="border-t border-rule bg-paper py-14 text-deep sm:py-20">
<div className="shell">
<div className="max-w-3xl space-y-12">
<section aria-labelledby="responsable">
<h2
id="responsable"
className="font-display text-xl font-extrabold text-deep sm:text-2xl"
>
1. Responsable del tratamiento
</h2>
<div className="mt-4 space-y-4 text-sm leading-relaxed text-deep-muted">
<p>
{EMPRESA.razonSocial} ({EMPRESA.nombreCorto}), empresa legalmente constituida
conforme a las leyes de la República Mexicana, con domicilio en Avenida Instituto
Técnico Industrial número 11, Colonia {EMPRESA.colonia}, Alcaldía Cuauhtémoc, C.P.
06400, {EMPRESA.ciudad}.
</p>
<p>
{EMPRESA.nombreCorto} será el responsable del tratamiento que, con motivo de la
relación jurídica que haya sido celebrada y para el cumplimiento de la misma, se
le dé a tus datos personales.
</p>
<p>
En cumplimiento de la Ley Federal de Protección de Datos Personales en Posesión de
los Particulares, pone a tu disposición el presente aviso para hacer de tu
conocimiento que los datos personales que nos proporciones, incluso aquellos
sensibles y patrimoniales o financieros, serán tratados dentro de un marco que
garantice su privacidad y el derecho a la autodeterminación informativa de las
personas.
</p>
</div>
</section>
<hr className="hair-lite" />
<section aria-labelledby="datos">
<h2 id="datos" className="font-display text-xl font-extrabold text-deep sm:text-2xl">
2. Qué datos recaba este sitio
</h2>
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
Esta sección no viene del aviso original: describe exactamente lo que recoge el
formulario de este sitio, para que sepas qué estás enviando antes de enviarlo. Son
cuatro campos visibles, más el contexto de la página en la que estabas.
</p>
<h3 className="mt-8 font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
Lo que escribes tú
</h3>
<div
className="mt-4 overflow-x-auto rounded-[6px] border border-rule-lite bg-white [-webkit-overflow-scrolling:touch]"
tabIndex={0}
role="region"
aria-label="Campos que llena la persona, tabla desplazable en horizontal"
>
<table className="w-full min-w-[34rem] border-collapse text-left">
<caption className="sr-only">
Campos visibles del formulario de contacto y para qué se usa cada uno.
</caption>
<thead>
<tr className="border-b border-rule-lite bg-paper-2">
{["Dato", "Para qué se usa"].map((c) => (
<th
key={c}
scope="col"
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-deep-muted"
>
{c}
</th>
))}
</tr>
</thead>
<tbody>
{CAMPOS_VISIBLES.map((fila) => (
<tr key={fila.campo} className="border-b border-rule-lite last:border-b-0">
<th scope="row" className="px-4 py-3.5 text-left align-top">
<span className="font-sans text-sm font-semibold text-deep">
{fila.campo}
</span>
</th>
<td className="px-4 py-3.5 align-top text-sm leading-relaxed text-deep-muted">
{fila.uso}
</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="mt-8 font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
Lo que se deduce de la página, sin preguntártelo
</h3>
<div
className="mt-4 overflow-x-auto rounded-[6px] border border-rule-lite bg-white [-webkit-overflow-scrolling:touch]"
tabIndex={0}
role="region"
aria-label="Contexto que registra el formulario, tabla desplazable en horizontal"
>
<table className="w-full min-w-[34rem] border-collapse text-left">
<caption className="sr-only">
Contexto que el formulario registra a partir de la página de origen.
</caption>
<thead>
<tr className="border-b border-rule-lite bg-paper-2">
{["Dato", "Para qué se usa"].map((c) => (
<th
key={c}
scope="col"
className="whitespace-nowrap px-4 py-3 font-mono text-[0.625rem] font-medium uppercase tracking-[0.14em] text-deep-muted"
>
{c}
</th>
))}
</tr>
</thead>
<tbody>
{CAMPOS_CONTEXTO.map((fila) => (
<tr key={fila.campo} className="border-b border-rule-lite last:border-b-0">
<th scope="row" className="px-4 py-3.5 text-left align-top">
<span className="font-sans text-sm font-semibold text-deep">
{fila.campo}
</span>
</th>
<td className="px-4 py-3.5 align-top text-sm leading-relaxed text-deep-muted">
{fila.uso}
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mt-5 text-sm leading-relaxed text-deep-muted">
El formulario no pide datos de tarjeta, contraseñas ni información financiera. Si
necesitas compartir documentación sensible de tu operación, acuérdalo por correo
antes de enviarla.
</p>
</section>
<hr className="hair-lite" />
<section aria-labelledby="finalidades">
<h2
id="finalidades"
className="font-display text-xl font-extrabold text-deep sm:text-2xl"
>
3. Finalidades del tratamiento
</h2>
<div className="mt-4 space-y-4 text-sm leading-relaxed text-deep-muted">
<p>
Tus datos personales, incluso aquellos sensibles y patrimoniales o financieros,
serán utilizados para las siguientes finalidades: identificarte, localizarte,
contactarte, así como para cumplir las obligaciones derivadas de la relación
celebrada contigo, y cualquier otro fin compatible o análogo de los aquí
establecidos.
</p>
<p>
Esto incluye enviar, por cualquier medio físico, electrónico o magnético, de
manera enunciativa mas no limitativa: invitación a reuniones de trabajo, minutas,
noticias y anuncios en general relacionados con la institución, campañas
publicitarias afines e invitación a eventos.
</p>
</div>
</section>
<hr className="hair-lite" />
<section aria-labelledby="transferencias">
<h2
id="transferencias"
className="font-display text-xl font-extrabold text-deep sm:text-2xl"
>
4. Transferencias
</h2>
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
Queda convenido que aceptas la transferencia que {EMPRESA.nombreCorto} pudiera
realizar a sus asociados y a terceros, en cumplimiento de las finalidades
anteriormente descritas.
</p>
</section>
<hr className="hair-lite" />
<section aria-labelledby="seguridad">
<h2
id="seguridad"
className="font-display text-xl font-extrabold text-deep sm:text-2xl"
>
5. Medidas de seguridad
</h2>
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
Con el objeto de limitar el uso y divulgación de tus datos personales, se mantienen
medidas de seguridad administrativas, técnicas y físicas para evitar su daño,
pérdida, alteración, destrucción, uso, acceso o divulgación indebida.
</p>
</section>
<hr className="hair-lite" />
<section aria-labelledby="consentimiento">
<h2
id="consentimiento"
className="font-display text-xl font-extrabold text-deep sm:text-2xl"
>
6. Excepciones al consentimiento
</h2>
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
{EMPRESA.nombreCorto} no requerirá el consentimiento del titular para tratar datos
personales cuando lo haga con fundamento en lo dispuesto por la Ley Federal de
Protección de Datos Personales en Posesión de los Particulares.
</p>
</section>
<hr className="hair-lite" />
<section aria-labelledby="arco">
<h2 id="arco" className="font-display text-xl font-extrabold text-deep sm:text-2xl">
7. Derechos de acceso, rectificación, cancelación y oposición
</h2>
<div className="mt-4 space-y-4 text-sm leading-relaxed text-deep-muted">
<p>
El ejercicio de los derechos de Acceso, Rectificación, Cancelación y Oposición
respecto de tus datos personales, así como la revocación del consentimiento para
su tratamiento, podrá realizarse a través de los procedimientos implementados o
solicitándolo al siguiente correo electrónico:
</p>
<p>
<a href={`mailto:${CORREO_ARCO}`} className="link break-all font-mono text-sm">
{CORREO_ARCO}
</a>
</p>
</div>
</section>
<hr className="hair-lite" />
<section aria-labelledby="cambios">
<h2 id="cambios" className="font-display text-xl font-extrabold text-deep sm:text-2xl">
8. Cambios al aviso
</h2>
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
{EMPRESA.nombreCorto} podrá enmendar o modificar el presente aviso de privacidad
como lo estime conveniente, e informará y pondrá a tu disposición el aviso
actualizado en su sitio web.
</p>
</section>
<hr className="hair-lite" />
<section aria-labelledby="contacto-aviso">
<h2
id="contacto-aviso"
className="font-display text-xl font-extrabold text-deep sm:text-2xl"
>
9. Contacto
</h2>
<dl className="spec mt-5 sm:grid-cols-2">
<div className="spec-cell !bg-white">
<dt className="spec-key !text-deep-muted">Teléfono</dt>
<dd className="spec-val !text-deep">
<a href={`tel:${EMPRESA.telefonoTel}`} className="link">
{EMPRESA.telefono}
</a>
</dd>
</div>
<div className="spec-cell !bg-white">
<dt className="spec-key !text-deep-muted">Correo general</dt>
<dd className="spec-val !text-deep">
<a href={`mailto:${EMPRESA.correo}`} className="link break-all">
{EMPRESA.correo}
</a>
</dd>
</div>
</dl>
<p className="mt-6 text-xs leading-relaxed text-deep-muted">
Si detectas una diferencia entre este texto y el aviso publicado por la empresa,
prevalece el de la empresa.{" "}
<Link href="/contacto" className="link">
Avísanos
</Link>{" "}
y lo corregimos.
</p>
</section>
</div>
</div>
</article>
{/* ---------- salida ---------- */}
<section
className="border-t border-rule bg-ink py-14 sm:py-20"
aria-labelledby="dudas-titulo"
>
<div className="shell">
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
<div>
<p className="eyebrow mb-4">Si te quedó una duda</p>
<h2 id="dudas-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
Pregunta antes de enviar tus datos, no después
</h2>
<p className="mt-4 text-sm leading-relaxed text-muted sm:text-base">
Para ejercer tus derechos de acceso, rectificación, cancelación u oposición, el canal
es el correo del apartado 7:{" "}
<a href={`mailto:${CORREO_ARCO}`} className="link break-all font-mono text-xs">
{CORREO_ARCO}
</a>
. Este formulario es para lo demás: dudas sobre el sitio, sobre qué se registra o
sobre lo que necesitas proteger.
</p>
<p className="mt-4 text-sm leading-relaxed text-muted">
Y si lo que buscabas era información técnica,{" "}
<Link href="/cumplimiento-nom-002" className="link">
la página de cumplimiento
</Link>{" "}
es un mejor punto de partida.
</p>
</div>
<FormularioContacto
id="formulario-aviso"
canal="catalogo"
titulo="Escribirnos"
descripcion="Completa la frase del mensaje con tu duda. Aplica el mismo aviso de privacidad que acabas de leer."
mensajeInicial={MENSAJE_INICIAL}
textoBoton="Enviar mi duda"
/>
</div>
</div>
</section>
</>
);
}