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
+248
View File
@@ -0,0 +1,248 @@
import type { Metadata } from "next";
import Link from "next/link";
import { FormularioContacto } from "@/components/formulario/FormularioContacto";
import { Revelar } from "@/components/site/Revelar";
import { EMPRESA, SITIO_URL, WHATSAPP } from "@/data/empresa";
/**
* /contacto
*
* Solo datos verificados: teléfono, correo, ciudad y colonia. No hay horario de
* atención, ni domicilio con número, ni mapa: nada de eso está confirmado, y
* una dirección equivocada en un sitio de protección contra incendios es un
* problema real, no un detalle.
*/
const TITULO = "Contacto";
const DESCRIPCION = `Habla con ${EMPRESA.nombre}. Teléfono, correo y solicitud de diagnóstico para operaciones que necesitan protección contra incendios documentada.`;
export const metadata: Metadata = {
title: TITULO,
description: DESCRIPCION,
alternates: { canonical: `${SITIO_URL}/contacto` },
openGraph: {
title: `${TITULO} | ${EMPRESA.nombreCorto}`,
description: DESCRIPCION,
url: `${SITIO_URL}/contacto`,
},
};
const MENSAJE_WHATSAPP = encodeURIComponent(
"Hola, me gustaría información sobre protección contra incendios.",
);
export default function PaginaContacto() {
return (
<>
{/* ---------- encabezado ---------- */}
<section className="grid-bg bg-ink pt-12 pb-14 sm:pt-16 sm:pb-16">
<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">Contacto</li>
</ol>
</nav>
<div className="max-w-3xl anim-rise">
<p className="eyebrow mb-5">Contacto</p>
<h1 className="font-display text-[2rem] font-black uppercase leading-[1.03] tracking-tight sm:text-[2.75rem] lg:text-5xl">
Si tu situación no puede esperar, marca
</h1>
<p className="mt-6 text-base leading-relaxed text-muted sm:text-lg">
Para todo lo demás, el formulario sirve mejor: llega con la clase de fuego, la
superficie y el tipo de operación, así que la primera respuesta puede ser técnica en
lugar de una pregunta de vuelta.
</p>
</div>
</div>
</section>
{/* ---------- datos verificados ---------- */}
<section className="border-t border-rule bg-ink-2 py-12 sm:py-14" aria-labelledby="datos-titulo">
<div className="shell">
<h2 id="datos-titulo" className="sr-only">
Datos de contacto
</h2>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<a
href={`tel:${EMPRESA.telefonoTel}`}
className="group flex flex-col rounded-[6px] border border-rule bg-ink p-5 transition-colors duration-200 hover:border-signal [-webkit-tap-highlight-color:transparent]"
>
<span className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
Teléfono
</span>
<span className="mt-2.5 font-mono text-lg font-semibold text-bright transition-colors group-hover:text-signal-hi">
{EMPRESA.telefono}
</span>
<span className="mt-2 text-xs leading-relaxed text-muted">
Marca directo desde el móvil.
</span>
</a>
<a
href={`mailto:${EMPRESA.correo}`}
className="group flex flex-col rounded-[6px] border border-rule bg-ink p-5 transition-colors duration-200 hover:border-signal [-webkit-tap-highlight-color:transparent]"
>
<span className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
Correo
</span>
<span className="mt-2.5 break-all font-mono text-sm font-semibold text-bright transition-colors group-hover:text-signal-hi">
{EMPRESA.correo}
</span>
<span className="mt-2 text-xs leading-relaxed text-muted">
Útil si necesitas adjuntar planos o el requerimiento de tu auditoría.
</span>
</a>
<a
href={`https://api.whatsapp.com/send?phone=${WHATSAPP}&text=${MENSAJE_WHATSAPP}`}
target="_blank"
rel="noopener noreferrer"
className="group flex flex-col rounded-[6px] border border-rule bg-ink p-5 transition-colors duration-200 hover:border-signal [-webkit-tap-highlight-color:transparent]"
>
<span className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
WhatsApp
</span>
<span className="mt-2.5 font-mono text-sm font-semibold text-bright transition-colors group-hover:text-signal-hi">
Abrir conversación
</span>
<span className="mt-2 text-xs leading-relaxed text-muted">
El mismo número, si prefieres escribir antes de llamar.
</span>
</a>
<div className="flex flex-col rounded-[6px] border border-rule bg-ink p-5">
<span className="font-mono text-[0.625rem] uppercase tracking-[0.14em] text-faint">
Ubicación
</span>
<span className="mt-2.5 font-mono text-sm font-semibold text-bright">
{EMPRESA.colonia}
</span>
<span className="mt-1 font-mono text-sm text-muted">
{EMPRESA.ciudad}, {EMPRESA.pais}
</span>
<span className="mt-2 text-xs leading-relaxed text-muted">
Dinos en el formulario dónde está la instalación y te confirmamos cómo podemos
atenderla.
</span>
</div>
</div>
<p className="mt-6 max-w-3xl text-xs leading-relaxed text-faint">
Publicamos únicamente los datos que pudimos verificar en la información pública de la
empresa. Si necesitas domicilio fiscal, horario de atención o una visita presencial,
pídelos por cualquiera de estos medios y te los confirmamos por escrito.
</p>
</div>
</section>
{/* ---------- formulario completo ---------- */}
<section id="formulario" 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.1fr] lg:items-start lg:gap-16">
<div>
<Revelar>
<p className="eyebrow mb-4">Solicitud</p>
<h2 className="font-display text-2xl font-extrabold text-deep sm:text-3xl">
Qué pasa cuando envías el formulario
</h2>
</Revelar>
<ol className="mt-8 space-y-6">
{[
{
n: "01",
t: "Se registra con folio",
d: "Recibes un folio en pantalla. Tu solicitud queda asignada a una persona, no a un buzón compartido.",
},
{
n: "02",
t: "Se interpreta el escenario",
d: "Con la clase de fuego, la superficie y el tipo de operación que declaraste, alguien revisa qué configuración aplica y qué habría que descartar.",
},
{
n: "03",
t: "Respondemos con documentación, no con folleto",
d: "La respuesta incluye la selección propuesta y el índice de documentos que debería acompañarla. Si algo no podemos sostener por escrito, va dicho ahí.",
},
].map((paso) => (
<li key={paso.n} className="flex gap-4">
<span className="font-mono text-[0.625rem] font-semibold tracking-[0.08em] text-signal">
{paso.n}
</span>
<div>
<h3 className="font-display text-sm font-bold uppercase tracking-wide text-deep">
{paso.t}
</h3>
<p className="mt-1.5 text-sm leading-relaxed text-deep-muted">{paso.d}</p>
</div>
</li>
))}
</ol>
<hr className="hair-lite my-8" />
<h3 className="font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
Antes de escribir, quizá ya está respondido
</h3>
<ul className="mt-4 flex flex-wrap gap-2">
{[
{ href: "/preguntas-frecuentes", label: "Preguntas frecuentes" },
{ href: "/cumplimiento-nom-002", label: "Cumplimiento NOM-002" },
{ href: "/evidencia", label: "Centro de evidencia" },
{ href: "/catalogo", label: "Catálogo" },
].map((enlace) => (
<li key={enlace.href}>
<Link
href={enlace.href}
className="badge !border-rule-lite !text-deep-muted hover:!border-signal hover:!text-deep [-webkit-tap-highlight-color:transparent]"
>
{enlace.label}
</Link>
</li>
))}
</ul>
<h3 className="mt-8 font-mono text-[0.6875rem] uppercase tracking-[0.16em] text-deep-muted">
Redes
</h3>
<ul className="mt-4 flex flex-wrap gap-4">
{[
{ url: EMPRESA.redes.linkedin, label: "LinkedIn" },
{ url: EMPRESA.redes.facebook, label: "Facebook" },
{ url: EMPRESA.redes.youtube, label: "YouTube" },
].map((red) => (
<li key={red.label}>
<a
href={red.url}
target="_blank"
rel="noopener noreferrer"
className="font-mono text-[0.6875rem] uppercase tracking-[0.12em] text-deep underline decoration-rule-lite underline-offset-4 hover:decoration-signal"
>
{red.label}
</a>
</li>
))}
</ul>
</div>
<FormularioContacto
canal="contacto"
variante="claro"
compacto={false}
titulo="Solicitar diagnóstico"
descripcion="Cuéntanos qué operación hay que proteger. Te respondemos con el escenario, el equipo que aplica y qué documentación te corresponde."
/>
</div>
</div>
</section>
</>
);
}