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
+144
View File
@@ -0,0 +1,144 @@
"use client";
import Link from "next/link";
import { useId, useMemo, useState } from "react";
import { RielClasesSelector } from "@/components/marca/RielClases";
import { serializeClases, type ClaseFuego } from "@/data/clases-fuego";
import { INDUSTRIAS } from "@/data/industrias";
/**
* EL CLASIFICADOR DE RIESGO — el elemento firma en acción.
*
* El hallazgo central del estudio de marca es que el visitante tenía que
* adivinar qué solución le correspondía. Esto lo resuelve en dos pasos:
* elige su tipo de operación, enciende las clases de fuego que tiene, y el
* botón lo deja en la ruta correcta con su riesgo ya declarado
* (`/soluciones/<slug>?clases=B,C`), que la página de destino y el formulario
* leen como preselección.
*
* Degradación sin JavaScript: el `<noscript>` oculta la interfaz interactiva y
* publica las cinco rutas como enlaces directos, cada una con las clases
* dominantes de su segmento en la query string.
*/
export function Clasificador() {
const [slug, setSlug] = useState<string>(INDUSTRIAS[0].slug);
const [clases, setClases] = useState<ClaseFuego[]>([]);
const idPaso1 = useId();
const idPaso2 = useId();
const industria = useMemo(
() => INDUSTRIAS.find((i) => i.slug === slug) ?? INDUSTRIAS[0],
[slug],
);
// Sin clases encendidas viajan las dominantes del segmento: el destino
// nunca queda sin contexto de riesgo.
const clasesEfectivas = clases.length > 0 ? clases : industria.clases;
const destino = `/soluciones/${industria.slug}?clases=${serializeClases(clasesEfectivas)}`;
return (
<div className="panel bg-ink-2/85 p-4 sm:p-5" style={{ WebkitBackdropFilter: "blur(6px)", backdropFilter: "blur(6px)" }}>
<noscript>
<style>{".clasif-js{display:none!important}.clasif-nojs{display:block!important}"}</style>
</noscript>
<div className="clasif-js">
{/* Paso 01 — tipo de operación */}
<p
id={idPaso1}
className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint"
>
<span className="text-signal">01</span> Tu tipo de operación
</p>
<div
role="radiogroup"
aria-labelledby={idPaso1}
className="mt-2.5 flex flex-wrap gap-1.5"
>
{INDUSTRIAS.map((ind) => {
const activa = ind.slug === industria.slug;
return (
<button
key={ind.slug}
type="button"
role="radio"
aria-checked={activa}
onClick={() => setSlug(ind.slug)}
className={[
"rounded-[3px] border px-2.5 py-2 text-left font-mono text-[0.6875rem] uppercase tracking-[0.06em] transition-colors duration-150",
activa
? "border-signal bg-signal/12 text-bright"
: "border-rule-2 text-muted hover:border-muted hover:text-bright",
].join(" ")}
style={{
WebkitAppearance: "none",
appearance: "none",
WebkitTapHighlightColor: "transparent",
}}
>
{ind.nombre}
</button>
);
})}
</div>
<hr className="hair my-4" />
{/* Paso 02 — clases de fuego presentes */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<p
id={idPaso2}
className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint"
>
<span className="text-signal">02</span> Clases de fuego que tienes
</p>
{/* El riel se enciende una sola vez al montar: es el elemento firma
presentándose, no un efecto que se repite. */}
<div className="anim-ignite mt-2.5" aria-describedby={idPaso2}>
<RielClasesSelector valor={clases} onChange={setClases} tamano="lg" />
</div>
</div>
<p className="max-w-[16rem] text-xs leading-relaxed text-faint">
{clases.length === 0
? `Si no lo sabes, seguimos con las clases habituales del segmento: ${industria.clases.join(", ")}.`
: `Riesgo declarado: ${clases.join(", ")}. Viaja contigo a la siguiente pantalla.`}
</p>
</div>
<Link href={destino} className="btn btn-signal mt-4 w-full sm:w-auto">
Ver mi ruta
<svg width="14" height="10" viewBox="0 0 14 10" aria-hidden="true">
<path d="M1 5h11M8.5 1.5L12 5l-3.5 3.5" stroke="currentColor" strokeWidth="1.5" fill="none" />
</svg>
</Link>
</div>
{/* Respaldo sin JavaScript: las cinco rutas como enlaces. */}
<div className="clasif-nojs hidden">
<p className="font-mono text-[0.625rem] uppercase tracking-[0.16em] text-faint">
Elige tu tipo de operación
</p>
<ul className="mt-2.5 divide-y divide-rule">
{INDUSTRIAS.map((ind) => (
<li key={ind.slug}>
<Link
href={`/soluciones/${ind.slug}?clases=${serializeClases(ind.clases)}`}
className="flex items-baseline justify-between gap-3 py-2.5"
>
<span className="text-sm font-semibold text-bright">{ind.nombre}</span>
<span className="font-mono text-[0.6875rem] text-signal">
{ind.clases.join("·")}
</span>
</Link>
</li>
))}
</ul>
</div>
</div>
);
}