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,78 @@
import type { TarjetaKpi } from "@/lib/crm/tipos";
import { valorKpi } from "@/components/admin/formato";
import { claseVariacion, subirEsMejor } from "@/components/admin/graficas/paleta";
/**
* Ficha de KPI. Rótulo, cifra y variación contra el periodo anterior.
*
* La dirección de mejora se resuelve por KPI, no por signo: en horas hasta la
* primera respuesta bajar es mejor. Si `variacion` es null no se pinta flecha
* —no hay base de comparación y no se inventa una.
*/
export function TarjetaKpiVista({ kpi, dias }: { kpi: TarjetaKpi; dias: number }) {
const mejorArriba = subirEsMejor(kpi.clave, kpi.formato);
const v = kpi.variacion;
return (
<div className="panel flex min-w-0 flex-col justify-between gap-3 px-4 py-3.5">
<p className="font-mono text-[0.625rem] uppercase leading-snug tracking-[0.12em] text-faint">
{kpi.etiqueta}
</p>
<p className="font-mono text-2xl font-semibold leading-none text-bright sm:text-[1.75rem]">
{valorKpi(kpi.valor, kpi.formato)}
</p>
<div className="flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-1">
{v == null ? (
<span className="font-mono text-[0.625rem] uppercase tracking-[0.1em] text-faint">
Sin base de comparación
</span>
) : (
<>
<span
className={`font-mono text-xs font-semibold ${claseVariacion(v, mejorArriba)}`}
>
<span aria-hidden="true">{v > 0 ? "▲" : v < 0 ? "▼" : "■"}</span>{" "}
{v > 0 ? "+" : ""}
{v.toFixed(1)} pp
</span>
<span className="font-mono text-[0.5625rem] uppercase tracking-[0.1em] text-faint">
vs. {dias} días previos
</span>
<span className="sr-only">
{v > 0 ? "sube" : v < 0 ? "baja" : "sin cambio"} {Math.abs(v).toFixed(1)} puntos
porcentuales contra los {dias} días previos, lo que{" "}
{v === 0 ? "no cambia el indicador" : (v > 0) === mejorArriba ? "es una mejora" : "es un retroceso"}.
</span>
</>
)}
</div>
{kpi.nota ? (
<p className="text-[0.6875rem] leading-snug text-muted">{kpi.nota}</p>
) : null}
</div>
);
}
export function FilaKpis({ kpis, dias }: { kpis: TarjetaKpi[]; dias: number }) {
if (kpis.length === 0) {
return (
<div className="panel px-4 py-6">
<p className="text-sm text-muted">
Todavía no hay indicadores para este periodo. Amplía el rango a 90 días o espera
los primeros envíos del sitio.
</p>
</div>
);
}
return (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2 xl:grid-cols-4">
{kpis.map((k) => (
<TarjetaKpiVista key={k.clave} kpi={k} dias={dias} />
))}
</div>
);
}