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]>
96 lines
2.8 KiB
TypeScript
96 lines
2.8 KiB
TypeScript
"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>
|
|
);
|
|
}
|