feat: sitio comercial FSPM con micro CRM interno

Sitio público orientado a venta consultiva e implementación de las
recomendaciones de la auditoría de marca y mercado:

- Inicio con tres rutas (industria, riesgo, cumplimiento) y clasificador
  de riesgo en el héroe, contra el hallazgo de que el visitante tenía que
  inferir qué solución le correspondía.
- Cinco páginas por segmento prioritario y catálogo comparable por clase
  de fuego, capacidad y agente.
- Los claims sin evidencia por modelo quedan aislados y se presentan como
  compromiso de documentación, nunca como característica del producto.
- SEO técnico completo: metadatos por página, sitemap derivado de los
  datos, JSON-LD, iconos y tarjeta social generadas.

Micro CRM:

- Captura por formulario embebido y por widget de WhatsApp que registra
  antes de abrir la conversación.
- Atribución de primer toque que sobrevive a la navegación.
- Deduplicación de contacto; un mismo contacto acumula oportunidades.
- Motor de asignación por reglas con reparto por turno de respaldo.
- Panel con dos roles de alcance distinto, tablero analítico, bitácora,
  pedidos y reglas. No enlazado desde el sitio público y con noindex.

Empaquetado en Docker multietapa con la base de demostración horneada en
la imagen y el volumen de datos separado, para que los leads capturados
sobrevivan a los redespliegues.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
Uriel Jareth
2026-08-28 00:07:20 -06:00
co-authored by Claude Opus 5
parent 122e06a4bb
commit 01b817d098
649 changed files with 26690 additions and 0 deletions
@@ -0,0 +1,453 @@
import type { Metadata } from "next";
import Link from "next/link";
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
import { Revelar } from "@/components/site/Revelar";
import { EMPRESA, NORMAS, SITIO_URL } from "@/data/empresa";
import { INDUSTRIAS } from "@/data/industrias";
/**
* /cumplimiento-nom-002 — página de intención de búsqueda alta.
*
* Regla del contenido: no se citan artículos, umbrales ni plazos que no estén
* en src/data/empresa.ts o en la fuente pública. Lo que no está verificado se
* declara como no reproducido, no se estima.
*/
const TITULO = "Cumplimiento NOM-002-STPS-2010";
const DESCRIPCION =
"Qué exige la NOM-002-STPS-2010 a un centro de trabajo, qué documentación se acumula y cómo se ve un expediente completo. Referencia orientativa, no dictamen técnico ni jurídico.";
export const metadata: Metadata = {
title: TITULO,
description: DESCRIPCION,
alternates: { canonical: `${SITIO_URL}/cumplimiento-nom-002` },
openGraph: {
title: `${TITULO} | ${EMPRESA.nombreCorto}`,
description: DESCRIPCION,
url: `${SITIO_URL}/cumplimiento-nom-002`,
},
};
const AVISO_ALCANCE =
"Esta página es una referencia orientativa construida con información pública. La normativa puede actualizarse y complementarse por estado, municipio, tipo de instalación y autoridad competente. No sustituye un dictamen técnico ni una opinión jurídica.";
/** Elementos que la norma contempla, según la referencia de NORMAS. */
const ELEMENTOS = [
{
n: "01",
titulo: "Clasificación del riesgo de incendio",
detalle:
"Es el primer paso y condiciona todo lo demás. De la clasificación depende qué equipo, qué sistemas y qué plan te corresponden. Se determina con el procedimiento de la propia norma; los umbrales no se reproducen aquí porque dependen del material y de la superficie de cada centro de trabajo.",
},
{
n: "02",
titulo: "Equipo contra incendio",
detalle:
"Extintores disponibles, accesibles y señalizados, seleccionados según la clase de fuego presente en cada área. El equipo correcto en el lugar equivocado no cumple.",
},
{
n: "03",
titulo: "Sistemas fijos y de alarma",
detalle:
"Según el nivel de riesgo, el centro de trabajo puede requerir sistemas fijos de supresión y medios de detección y alarma, no solo equipo portátil.",
},
{
n: "04",
titulo: "Plan de atención a emergencias",
detalle:
"Documento por escrito. Es lo primero que se pide en una verificación y lo que más frecuentemente no existe o está desactualizado.",
},
{
n: "05",
titulo: "Brigadas y capacitación",
detalle:
"Personal designado, entrenado y con constancia. La capacitación sin registro documental no es demostrable ante una autoridad.",
},
{
n: "06",
titulo: "Simulacros",
detalle:
"Ejecutados y documentados. El registro del simulacro es evidencia tanto del plan como de la capacitación.",
},
];
const EXPEDIENTE = [
{
documento: "Clasificación de riesgo de incendio",
quePrueba: "Que el nivel de exigencia aplicado al centro de trabajo es el correcto.",
quienLoPide: "Verificación STPS, Protección Civil, aseguradora",
},
{
documento: "Plan de atención a emergencias",
quePrueba: "Que existe un procedimiento escrito y vigente para el escenario.",
quienLoPide: "Verificación STPS, Protección Civil",
},
{
documento: "Inventario y ubicación del equipo",
quePrueba: "Qué hay instalado, dónde, de qué clase y con qué capacidad.",
quienLoPide: "Verificación STPS, auditoría interna",
},
{
documento: "Ficha técnica y certificado por modelo",
quePrueba: "Que el equipo instalado es el que se declara y cubre las clases que se afirman.",
quienLoPide: "Auditoría, aseguradora, cliente corporativo",
},
{
documento: "Hoja de datos de seguridad del agente",
quePrueba: "Manejo, almacenamiento y respuesta ante exposición del agente extintor.",
quienLoPide: "Seguridad industrial, ambiental, auditoría",
},
{
documento: "Bitácora de revisión mensual",
quePrueba: "Que la revisión de extintores se realiza con la periodicidad debida.",
quienLoPide: "Verificación STPS, Protección Civil",
},
{
documento: "Constancias de capacitación y brigadas",
quePrueba: "Que hay personal designado y entrenado, con nombre y fecha.",
quienLoPide: "Verificación STPS",
},
{
documento: "Registro de simulacros",
quePrueba: "Que el plan se ejecuta y se corrige con lo aprendido.",
quienLoPide: "Verificación STPS, Protección Civil",
},
];
export default function PaginaCumplimiento() {
return (
<>
{/* ---------- encabezado con la advertencia de alcance ---------- */}
<section className="grid-bg bg-ink pt-12 pb-14 sm:pt-16 sm:pb-20">
<div className="shell">
<nav aria-label="Ruta de navegación" className="mb-8">
<ol className="flex flex-wrap items-center gap-2 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
<li>
<Link href="/" className="hover:text-bright">
Inicio
</Link>
</li>
<li aria-hidden="true">/</li>
<li className="text-bright">Cumplimiento</li>
</ol>
</nav>
<div className="max-w-3xl anim-rise">
<p className="eyebrow mb-5">Referencia normativa</p>
<h1 className="font-display text-[2rem] font-black uppercase leading-[1.03] tracking-tight sm:text-[2.75rem] lg:text-5xl">
Cumplimiento NOM-002: qué te van a pedir y cómo se ve un expediente completo
</h1>
<p className="mt-6 text-base leading-relaxed text-muted sm:text-lg">
La norma no se cumple comprando extintores. Se cumple demostrando, por escrito, que el
riesgo está clasificado, el equipo corresponde a ese riesgo y el personal sabe qué
hacer. Esta página explica esa lógica y qué documentos la sostienen.
</p>
</div>
<div
role="note"
aria-label="Alcance de esta página"
className="mt-10 max-w-3xl rounded-[6px] border border-rule border-l-2 border-l-ember bg-ink-2 p-5 sm:p-6"
>
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-ember">
Alcance de esta página
</p>
<p className="mt-3 text-sm leading-relaxed text-muted">{AVISO_ALCANCE}</p>
</div>
</div>
</section>
{/* ---------- a quién aplica ---------- */}
<section className="grid-bg-lite bg-paper py-14 sm:py-20" aria-labelledby="aplica-titulo">
<div className="shell">
<div className="grid gap-10 lg:grid-cols-[1fr_1.1fr] lg:gap-16">
<div>
<Revelar>
<p className="eyebrow mb-4">A quién aplica</p>
<h2 id="aplica-titulo" className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
A todo centro de trabajo, sin mínimo de tamaño
</h2>
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
{NORMAS[0].aplica}
</p>
<p className="mt-4 text-sm leading-relaxed text-deep-muted">
Es la razón por la que una oficina de veinte personas y una planta petroquímica
están bajo la misma norma con obligaciones muy distintas: lo que cambia no es la
norma, es la clasificación del riesgo.
</p>
</Revelar>
</div>
<div className="panel-lite p-6 sm:p-8">
<h3 className="font-display text-lg font-extrabold text-deep sm:text-xl">
Dos niveles, dos conversaciones distintas
</h3>
<div className="mt-6 space-y-6">
<div className="border-l-2 border-l-rule-lite pl-4">
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-deep-muted">
Riesgo ordinario
</p>
<p className="mt-2 text-sm leading-relaxed text-deep-muted">
El foco está en tener el equipo correcto, señalizado y revisado, con un plan por
escrito y personal capacitado. La mayoría de las PyMEs y oficinas queda aquí y
el error caro es comprar de más por miedo.
</p>
</div>
<div className="border-l-2 border-l-signal pl-4">
<p className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-signal">
Riesgo alto
</p>
<p className="mt-2 text-sm leading-relaxed text-deep-muted">
La norma establece requerimientos mayores para los centros clasificados como de
riesgo alto de incendio: la conversación pasa de equipo portátil a sistemas
fijos, detección, brigadas formales y evidencia sostenida en el tiempo.
</p>
</div>
</div>
<hr className="hair-lite my-6" />
<p className="text-xs leading-relaxed text-deep-muted">
No reproducimos aquí los umbrales de clasificación: dependen del material y de la
superficie de cada centro de trabajo, y determinarlos mal invalida todo lo que se
construya encima. Ese es el primer punto de un diagnóstico.
</p>
</div>
</div>
</div>
</section>
{/* ---------- qué contempla la norma ---------- */}
<section className="border-t border-rule bg-ink py-14 sm:py-20" aria-labelledby="elementos-titulo">
<div className="shell">
<Revelar>
<p className="eyebrow mb-4">Qué contempla</p>
<h2 id="elementos-titulo" className="font-display text-2xl font-extrabold sm:text-3xl">
Seis frentes, y ninguno se cierra con una compra
</h2>
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-muted sm:text-base">
Cada uno genera un documento distinto. Un expediente incompleto suele fallar en los
frentes que no se compran: el plan, la capacitación y el registro.
</p>
</Revelar>
<ol className="mt-10 grid gap-px bg-rule sm:grid-cols-2 lg:grid-cols-3">
{ELEMENTOS.map((elemento) => (
<li key={elemento.n} className="bg-ink-2 p-6">
<p className="font-mono text-[0.625rem] font-semibold tracking-[0.1em] text-signal">
{elemento.n}
</p>
<h3 className="mt-3 font-display text-base font-extrabold leading-tight text-bright">
{elemento.titulo}
</h3>
<p className="mt-3 text-sm leading-relaxed text-muted">{elemento.detalle}</p>
</li>
))}
</ol>
<div className="mt-10 rounded-[6px] border border-rule bg-ink-2 p-5 sm:p-6">
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-faint">
Mantenimiento de extintores
</p>
<p className="mt-3 max-w-3xl text-sm leading-relaxed text-muted">
{NORMAS[1].aplica} Es la obligación recurrente que más seguido se pierde: no por
desconocerla, sino porque nadie quedó a cargo de la bitácora.
</p>
</div>
</div>
</section>
{/* ---------- el expediente ---------- */}
<section className="grid-bg-lite bg-paper py-14 sm:py-20" aria-labelledby="expediente-titulo">
<div className="shell-wide">
<Revelar>
<p className="eyebrow mb-4">Expediente</p>
<h2 id="expediente-titulo" className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
Cómo se ve un expediente que aguanta una verificación
</h2>
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-deep-muted sm:text-base">
Ocho documentos. Ninguno es opcional cuando alguien pregunta, y cada uno prueba algo
distinto.
</p>
</Revelar>
<p className="mt-6 font-mono text-[0.6875rem] uppercase tracking-[0.14em] text-deep-muted sm:hidden">
Desliza la tabla para ver todas las columnas
</p>
<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="Documentos del expediente, tabla desplazable en horizontal"
>
<table className="w-full min-w-[46rem] border-collapse text-left">
<caption className="sr-only">
Documentos que integran un expediente de cumplimiento, qué prueba cada uno y quién
lo solicita.
</caption>
<thead>
<tr className="border-b border-rule-lite bg-paper-2">
{["Documento", "Qué prueba", "Quién lo pide"].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>
{EXPEDIENTE.map((fila) => (
<tr key={fila.documento} className="border-b border-rule-lite last:border-b-0">
<th scope="row" className="px-4 py-4 text-left align-top">
<span className="font-sans text-sm font-semibold text-deep">
{fila.documento}
</span>
</th>
<td className="max-w-[24rem] px-4 py-4 align-top text-sm leading-relaxed text-deep-muted">
{fila.quePrueba}
</td>
<td className="px-4 py-4 align-top font-mono text-xs leading-relaxed text-deep-muted">
{fila.quienLoPide}
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mt-6 max-w-3xl text-xs leading-relaxed text-deep-muted">
En una compra con nosotros, los documentos que dependen del equipo se entregan por
número de modelo.{" "}
<Link href="/evidencia" className="link">
Ver el índice de documentación por categoría
</Link>
.
</p>
</div>
</section>
{/* ---------- referencias ---------- */}
<section className="border-t border-rule bg-ink py-14 sm:py-16" aria-labelledby="referencias-titulo">
<div className="shell">
<Revelar>
<p className="eyebrow mb-4">Referencias</p>
<h2 id="referencias-titulo" className="font-display text-xl font-extrabold sm:text-2xl">
Normas citadas en esta página
</h2>
</Revelar>
<ul className="mt-8 grid gap-4 lg:grid-cols-2">
{NORMAS.map((norma) => (
<li key={norma.clave} className="panel p-5 sm:p-6">
<p className="font-mono text-xs font-semibold uppercase tracking-[0.12em] text-signal">
{norma.clave}
</p>
<h3 className="mt-3 font-display text-base font-extrabold leading-tight text-bright">
{norma.titulo}
</h3>
<p className="mt-3 text-sm leading-relaxed text-muted">{norma.aplica}</p>
<a
href={norma.fuente}
target="_blank"
rel="noopener noreferrer"
className="mt-4 inline-block font-mono text-[0.625rem] uppercase tracking-[0.14em] text-bright underline decoration-rule-2 underline-offset-4 hover:decoration-signal"
>
Ver la fuente oficial
</a>
</li>
))}
</ul>
<p className="mt-8 max-w-3xl text-xs leading-relaxed text-faint">
Los enlaces apuntan al sitio del Diario Oficial de la Federación. Verifica siempre la
versión vigente: la publicación oficial manda sobre cualquier resumen, incluido este.
</p>
</div>
</section>
{/* ---------- por industria ---------- */}
<section className="border-t border-rule bg-ink-2 py-14 sm:py-16" aria-labelledby="segmentos-titulo">
<div className="shell">
<Revelar>
<p className="eyebrow eyebrow-muted mb-4">Por operación</p>
<h2 id="segmentos-titulo" className="font-display text-xl font-extrabold sm:text-2xl">
Lo que cambia según dónde trabajas
</h2>
</Revelar>
<ul className="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{INDUSTRIAS.map((industria) => (
<li key={industria.slug}>
<Link
href={`/soluciones/${industria.slug}`}
className="group flex h-full flex-col rounded-[6px] border border-rule bg-ink p-5 transition-colors duration-200 hover:border-rule-2 [-webkit-tap-highlight-color:transparent]"
>
<h3 className="font-display text-base font-extrabold leading-tight text-bright">
{industria.nombre}
</h3>
<ul className="mt-3 space-y-2">
{industria.cumplimiento.map((obligacion) => (
<li key={obligacion} className="flex gap-2.5 text-xs leading-relaxed text-muted">
<span
aria-hidden="true"
className="mt-1.5 h-1 w-1 shrink-0 rounded-full bg-signal"
/>
{obligacion}
</li>
))}
</ul>
<span className="mt-auto pt-5 font-mono text-[0.625rem] uppercase tracking-[0.14em] text-signal transition-colors group-hover:text-signal-hi">
Ver el segmento &rarr;
</span>
</Link>
</li>
))}
</ul>
</div>
</section>
{/* ---------- formulario y cierre con la advertencia ---------- */}
<section id="validar" className="grid-bg-lite border-t border-rule bg-paper py-14 sm:py-20">
<div className="shell">
<div className="grid gap-10 lg:grid-cols-[1fr_1.05fr] lg:items-start lg:gap-16">
<div>
<Revelar>
<p className="eyebrow mb-4">Siguiente paso</p>
<h2 className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
Validar cumplimiento NOM-002
</h2>
<p className="mt-4 text-sm leading-relaxed text-deep-muted sm:text-base">
Revisamos qué tienes, qué te falta y en qué orden conviene resolverlo. Si algo de tu
situación requiere un dictamen formal, te lo decimos en lugar de venderte equipo.
</p>
</Revelar>
<div
role="note"
aria-label="Alcance de esta página"
className="mt-8 rounded-[6px] border border-rule-lite border-l-2 border-l-ember bg-white p-5"
>
<p className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
Recordatorio de alcance
</p>
<p className="mt-3 text-sm leading-relaxed text-deep-muted">{AVISO_ALCANCE}</p>
</div>
</div>
<FormularioContacto
canal="cumplimiento"
variante="claro"
titulo="Validar mi cumplimiento"
descripcion="Dinos qué tipo de instalación es y qué te pidieron. Respondemos con el hueco documental que vemos y por dónde empezar."
/>
</div>
</div>
</section>
</>
);
}