"use client"; import { useMemo, useState } from "react"; import { IVA } from "@/lib/crm/constantes"; import type { ItemPedido } from "@/lib/crm/tipos"; import { CATEGORIAS, PRODUCTOS } from "@/data/catalogo"; import { accionCrearPedido } from "@/lib/crm/acciones"; import { moneda } from "@/components/admin/formato"; import { AvisoAccion, usarAccion } from "@/components/admin/usarAccion"; /** * Alta de pedido desde una oportunidad ganada. * * Las partidas salen del catálogo real: no se puede teclear un producto que no * existe. El precio unitario sí se captura a mano y a propósito —el catálogo de * FSPM no publica precios, así que inventarlos aquí sería fabricar un dato. * * Subtotal, IVA y total se calculan en vivo, antes de enviar: quien cotiza ve * la cifra final mientras arma la partida, no después. */ type Partida = { /** Clave local de la fila, no viaja al servidor. */ llave: string; productoSlug: string; cantidad: string; precioUnitario: string; }; const ESTILO_CAMPO = { appearance: "none", WebkitAppearance: "none" } as const; const PRODUCTOS_POR_CATEGORIA = CATEGORIAS.map((c) => ({ categoria: c, productos: PRODUCTOS.filter((p) => p.categoria === c.slug).sort((a, b) => a.orden - b.orden), })).filter((g) => g.productos.length > 0); let contador = 0; function nuevaPartida(productoSlug = ""): Partida { contador += 1; return { llave: `p${contador}`, productoSlug, cantidad: "1", precioUnitario: "" }; } function numero(v: string): number { const n = Number(v.replace(/[^0-9.]/g, "")); return Number.isFinite(n) && n >= 0 ? n : 0; } export function CrearPedido({ oportunidadId, productoSlugSugerido, }: { oportunidadId: string; /** El producto que declaró el lead, para no empezar de cero. */ productoSlugSugerido: string | null; }) { const { enviando, aviso, ejecutar } = usarAccion(); const [abierto, setAbierto] = useState(false); const [partidas, setPartidas] = useState([ nuevaPartida( productoSlugSugerido && PRODUCTOS.some((p) => p.slug === productoSlugSugerido) ? productoSlugSugerido : "", ), ]); const [notas, setNotas] = useState(""); const totales = useMemo(() => { const subtotal = partidas.reduce( (s, p) => s + numero(p.cantidad) * numero(p.precioUnitario), 0, ); const iva = subtotal * IVA; return { subtotal, iva, total: subtotal + iva }; }, [partidas]); const validas = partidas.filter( (p) => p.productoSlug !== "" && numero(p.cantidad) > 0 && numero(p.precioUnitario) > 0, ); function actualizar(llave: string, campo: keyof Omit, valor: string) { setPartidas((prev) => prev.map((p) => (p.llave === llave ? { ...p, [campo]: valor } : p)), ); } function enviar() { if (validas.length === 0) return; const items: ItemPedido[] = validas.map((p) => { const producto = PRODUCTOS.find((x) => x.slug === p.productoSlug); return { productoSlug: p.productoSlug, nombre: producto?.nombre ?? p.productoSlug, cantidad: numero(p.cantidad), precioUnitario: numero(p.precioUnitario), }; }); void ejecutar(async () => { const r = await accionCrearPedido(oportunidadId, items, notas.trim() || undefined); if (r.ok) { setPartidas([nuevaPartida()]); setNotas(""); setAbierto(false); } return r; }, "Pedido creado. Búscalo en el listado de pedidos."); } if (!abierto) { return (

Esta oportunidad está ganada: ya se le puede levantar el pedido con las partidas acordadas.

); } return (
{ e.preventDefault(); enviar(); }} >
    {partidas.map((p, i) => { const producto = PRODUCTOS.find((x) => x.slug === p.productoSlug); const importe = numero(p.cantidad) * numero(p.precioUnitario); return (
  • actualizar(p.llave, "cantidad", e.target.value)} className="field w-full py-2 text-right font-mono text-sm" style={ESTILO_CAMPO} />
    actualizar(p.llave, "precioUnitario", e.target.value)} placeholder="0.00" className="field w-full py-2 text-right font-mono text-sm" style={ESTILO_CAMPO} />

    {producto ? `${producto.modelo ?? "sin modelo"} · ${producto.capacidad ?? "capacidad no declarada"}` : "Sin producto elegido"}

    {moneda(importe)}

    {partidas.length > 1 ? ( ) : null}
  • ); })}