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

@@ -1,10 +1,12 @@
import type { Unidad } from '../types'
import type { Unidad, TipoPropuesta } from '../types'
import { INVENTARIO_SEED } from '../data/unidades'
import { conAuth, manejar401 } from './auth'
// TODO produccion: '/api' same-origin detras de NGINX
const API_URL = import.meta.env.VITE_API_URL ?? 'http://192.168.10.123:3000/api'
export { API_URL }
export interface GetUnidadesResult {
unidades: Unidad[]
/** true cuando se usó el seed local por no poder contactar el API */
@@ -28,3 +30,79 @@ export async function getUnidades(): Promise<GetUnidadesResult> {
return { unidades: INVENTARIO_SEED, usandoSeed: true }
}
}
// ---------- Cotizaciones guardadas (historial del vendedor) ----------
/** Tipo de propuesta tal como lo espera el API (body de POST /cotizaciones) */
export type TipoPropuestaApi = 'BANCARIO' | 'INFONAVIT' | 'CONTADO' | 'SEMI_CONTADO'
export const TIPO_A_API: Record<TipoPropuesta, TipoPropuestaApi> = {
CREDITO_BANCARIO: 'BANCARIO',
CREDITO_INFONAVIT_COFINAVIT: 'INFONAVIT',
CONTADO: 'CONTADO',
SEMI_CONTADO: 'SEMI_CONTADO',
}
export const TIPO_DE_API: Record<TipoPropuestaApi, TipoPropuesta> = {
BANCARIO: 'CREDITO_BANCARIO',
INFONAVIT: 'CREDITO_INFONAVIT_COFINAVIT',
CONTADO: 'CONTADO',
SEMI_CONTADO: 'SEMI_CONTADO',
}
export interface CotizacionGuardada {
id: string
folio?: string
unidadCodigo: string
desarrollo: string
clienteNombre: string
clienteTelefono: string | null
clienteEmail: string | null
tipoPropuesta: TipoPropuestaApi
acabados: boolean
apartado: number
descuento: number
precioLista: number
precioFinal: number
enganche: number
numMensualidades: number
mensualidad: number | null
pagoFinal: number
createdAt: string
}
export interface GuardarCotizacionPayload {
unidadCodigo: string
clienteNombre: string
clienteEmail?: string
clienteTelefono?: string
tipoPropuesta: TipoPropuestaApi
acabados: boolean
apartado?: number
descuento?: number
}
/** URL pública de una cotización (destino del QR del PDF) */
export function urlPublicaCotizacion(id: string): string {
return `${API_URL}/cotizaciones/${id}/publico`
}
/** POST /cotizaciones — persiste y registra la oportunidad en Monday */
export async function guardarCotizacion(payload: GuardarCotizacionPayload): Promise<CotizacionGuardada> {
const resp = await fetch(`${API_URL}/cotizaciones`, conAuth({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}))
manejar401(resp)
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
return (await resp.json()) as CotizacionGuardada
}
/** GET /cotizaciones/mias — últimas 50 del vendedor autenticado */
export async function getMisCotizaciones(): Promise<CotizacionGuardada[]> {
const resp = await fetch(`${API_URL}/cotizaciones/mias`, conAuth())
manejar401(resp)
if (!resp.ok) throw new Error(`HTTP ${resp.status}`)
return (await resp.json()) as CotizacionGuardada[]
}