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:
co-authored by
Claude Opus 5
parent
122e06a4bb
commit
01b817d098
@@ -0,0 +1,95 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import {
|
||||
ESTADOS_PEDIDO,
|
||||
ESTADO_PEDIDO_ETIQUETA,
|
||||
type EstadoPedido,
|
||||
} from "@/lib/crm/constantes";
|
||||
import { accionCambiarEstadoPedido } from "@/lib/crm/acciones";
|
||||
import { AvisoAccion, usarAccion } from "@/components/admin/usarAccion";
|
||||
|
||||
/**
|
||||
* Cambio de estado del pedido. Cancelar pide confirmación explícita: es el
|
||||
* único estado del que no se sale sin explicar por qué.
|
||||
*/
|
||||
export function CambiarEstadoPedido({
|
||||
id,
|
||||
estado,
|
||||
}: {
|
||||
id: string;
|
||||
estado: EstadoPedido;
|
||||
}) {
|
||||
const { enviando, aviso, ejecutar } = usarAccion();
|
||||
const [elegido, setElegido] = useState<EstadoPedido>(estado);
|
||||
|
||||
const confirmandoCancelacion = elegido === "CANCELADO" && estado !== "CANCELADO";
|
||||
|
||||
function alCambiar(v: string) {
|
||||
const siguiente = v as EstadoPedido;
|
||||
setElegido(siguiente);
|
||||
if (siguiente === "CANCELADO") return;
|
||||
if (siguiente === estado) return;
|
||||
void ejecutar(
|
||||
() => accionCambiarEstadoPedido(id, siguiente),
|
||||
`Pedido en ${ESTADO_PEDIDO_ETIQUETA[siguiente].toLowerCase()}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid gap-2">
|
||||
<label htmlFor="pedido-estado" className="field-label">
|
||||
Estado del pedido
|
||||
</label>
|
||||
<select
|
||||
id="pedido-estado"
|
||||
value={elegido}
|
||||
disabled={enviando}
|
||||
onChange={(e) => alCambiar(e.target.value)}
|
||||
className="field w-full py-2 text-sm"
|
||||
style={{ appearance: "none", WebkitAppearance: "none" }}
|
||||
>
|
||||
{ESTADOS_PEDIDO.map((e) => (
|
||||
<option key={e} value={e}>
|
||||
{ESTADO_PEDIDO_ETIQUETA[e]}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
{confirmandoCancelacion ? (
|
||||
<div className="grid gap-2 rounded-[3px] border border-signal-dim bg-ink px-3 py-3">
|
||||
<p className="text-xs leading-relaxed text-muted">
|
||||
Cancelar el pedido lo saca del flujo de surtido. Confírmalo si es lo que quieres.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
disabled={enviando}
|
||||
onClick={() =>
|
||||
void ejecutar(
|
||||
() => accionCambiarEstadoPedido(id, "CANCELADO"),
|
||||
"Pedido cancelado.",
|
||||
)
|
||||
}
|
||||
className="btn btn-signal btn-sm"
|
||||
style={{ appearance: "none" }}
|
||||
>
|
||||
{enviando ? "Cancelando…" : "Cancelar pedido"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={enviando}
|
||||
onClick={() => setElegido(estado)}
|
||||
className="btn btn-ghost btn-sm"
|
||||
style={{ appearance: "none" }}
|
||||
>
|
||||
No cancelar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<AvisoAccion aviso={aviso} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user