feat(cotizador): historial, compartir WhatsApp, folio+QR, comparador de unidades, alertas de inventario

- Mis cotizaciones (guardar con cliente, retomar, nueva) asociadas al vendedor del token
- Compartir: resumen copiable + deep link wa.me; PDF con folio, vigencia y QR
- GET /api/cotizaciones/:id/publico (público, datos mínimos sin PII del cliente)
- Comparador lado a lado de 2 unidades (mismo o distinto desarrollo)
- POST /api/cotizaciones/alertas-inventario: avisa al vendedor si una unidad
  cotizada cambia de estatus (dedup; wire con syncs documentado)
- Fix: labels de propuesta mapeados a los reales del board Oportunidades
- Tests: api 201, cotizador 82
This commit is contained in:
2026-08-28 06:58:10 +00:00
parent bc0f9da8fe
commit 4f31d1c171
24 changed files with 1938 additions and 20 deletions

View File

@@ -0,0 +1,120 @@
import { useState } from 'react'
import type { CotizacionGuardada } from '../services/api'
import { formatoMoneda } from '../lib/formatters'
interface Props {
cotizaciones: CotizacionGuardada[]
cargando: boolean
error: string | null
/** Restaura la cotización en el cotizador (unidad + parámetros) */
onRetomar: (c: CotizacionGuardada) => void
}
const TITULO_PROPUESTA: Record<string, string> = {
BANCARIO: 'Crédito Bancario',
INFONAVIT: 'Crédito Infonavit-Cofinavit',
COFINAVIT: 'Crédito Infonavit-Cofinavit',
CONTADO: 'Contado',
SEMI_CONTADO: 'Semi Contado',
}
const formatoFechaHora = (iso: string): string =>
new Date(iso).toLocaleString('es-MX', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})
/** Sección colapsable "Mis cotizaciones": historial del vendedor logueado */
export function MisCotizaciones({ cotizaciones, cargando, error, onRetomar }: Props) {
const [abierto, setAbierto] = useState(false)
return (
<section aria-labelledby="mis-cotizaciones-titulo">
<div className="flex flex-wrap items-center justify-between gap-4">
<h2
id="mis-cotizaciones-titulo"
className="text-xs font-bold tracking-widest text-neutral-500 uppercase"
>
Mis cotizaciones
</h2>
<button
type="button"
aria-expanded={abierto}
aria-controls="mis-cotizaciones-contenido"
onClick={() => setAbierto((v) => !v)}
className="border border-neutral-900 bg-white px-5 py-2.5 text-xs font-bold tracking-widest text-neutral-900 uppercase transition-colors duration-150 hover:bg-neutral-100"
>
{abierto ? 'Ocultar historial' : `Ver historial (${cotizaciones.length})`}
</button>
</div>
{abierto && (
<div id="mis-cotizaciones-contenido" className="mt-6 border border-neutral-200 bg-white p-6">
{cargando && <p className="text-sm text-neutral-500">Cargando historial…</p>}
{error && <p className="text-xs text-red-700">{error}</p>}
{!cargando && !error && cotizaciones.length === 0 && (
<p className="text-sm text-neutral-500">
Aún no tienes cotizaciones guardadas. Se guardan al generar el PDF.
</p>
)}
{cotizaciones.length > 0 && (
<div className="overflow-x-auto">
<table className="w-full min-w-[640px] border-collapse text-sm">
<thead>
<tr>
{['Fecha', 'Folio', 'Cliente', 'Unidad', 'Propuesta', 'Precio final', ''].map(
(h) => (
<th
key={h}
scope="col"
className="p-3 text-left text-xs font-bold tracking-widest text-neutral-500 uppercase"
>
{h}
</th>
),
)}
</tr>
</thead>
<tbody>
{cotizaciones.map((c) => (
<tr key={c.id}>
<td className="border-t border-neutral-200 p-3 whitespace-nowrap">
{formatoFechaHora(c.createdAt)}
</td>
<td className="border-t border-neutral-200 p-3 whitespace-nowrap">
{c.folio ?? '—'}
</td>
<td className="border-t border-neutral-200 p-3">{c.clienteNombre}</td>
<td className="border-t border-neutral-200 p-3 whitespace-nowrap">
{c.unidadCodigo}
<span className="ml-1 text-xs text-neutral-400">{c.desarrollo}</span>
</td>
<td className="border-t border-neutral-200 p-3 whitespace-nowrap">
{TITULO_PROPUESTA[c.tipoPropuesta] ?? c.tipoPropuesta}
</td>
<td className="border-t border-neutral-200 p-3 text-right whitespace-nowrap">
{formatoMoneda(c.precioFinal)}
</td>
<td className="border-t border-neutral-200 p-3 text-right">
<button
type="button"
onClick={() => onRetomar(c)}
className="border border-accent bg-white px-3 py-1.5 text-xs font-bold tracking-widest text-accent uppercase transition-colors duration-150 hover:bg-accent hover:text-white"
>
Retomar
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</section>
)
}