feat(desarrollos): modulo de alta de desarrollos con inventario, reglas del cotizador por desarrollo y micrositio publico
Modulo nuevo (solo admin) para dar de alta desarrollos sin tocar codigo: - CRUD de la ficha, modelos e inventario (alta manual y generador por rango) - Reglas del cotizador por desarrollo, editables y persistidas en la BD - El cotizador consume las reglas desde /desarrollos/reglas-cotizador y cae a las constantes con aviso cuando un desarrollo aun no tiene reglas guardadas - Micrositio publico en /d/:clave (sin sesion) con ficha, modelos y disponibilidad; jamas expone datos de clientes ni cotizaciones Integracion: - La landing monta el modulo en el grid (clave 'desarrollos', solo admin) y el micrositio fuera del login; el rol manager NO accede (matriz y API con 403) - La tabla de roles de Configuracion ahora lista todos los modulos - Se alinea la FK de ReglaCotizadorDesarrollo con la BD viva (Desarrollo.id) Verificado: 708 pruebas del API, 115 del cotizador y 89 de la landing en verde; 12/12 cotizaciones identicas antes y despues del cambio de reglas; prueba en vivo del API con alta, modelos, inventario, reglas, micrositio y baja (30/30).
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { Unidad, DescuentosPropuestas, ParametrosDesarrollo } from '../types'
|
||||
import {
|
||||
APARTADO_DEFAULT,
|
||||
@@ -8,7 +8,14 @@ import {
|
||||
validarEnganchePct,
|
||||
type PropuestaResultado,
|
||||
} from '../lib/calculations'
|
||||
import { DESARROLLO_DEFAULT, descuentosDefault, obtenerDesarrollo } from '../data/desarrollos'
|
||||
import { DESARROLLO_DEFAULT } from '../data/desarrollos'
|
||||
import {
|
||||
SIN_REGLAS,
|
||||
descuentosDefaultEfectivo,
|
||||
desarrolloEfectivo,
|
||||
propuestasVigentes,
|
||||
type ReglasDesarrollo,
|
||||
} from '../lib/reglas'
|
||||
|
||||
interface UseCotizacionResult {
|
||||
desarrollo: string
|
||||
@@ -40,6 +47,8 @@ interface UseCotizacionResult {
|
||||
/** Número efectivo de mensualidades (la elegida o la calculada) */
|
||||
numMeses: number | null
|
||||
propuestas: PropuestaResultado[]
|
||||
/** Aviso cuando el admin desactivó la propuesta que estaba elegida; null si no aplica */
|
||||
avisoPropuestaDesactivada: string | null
|
||||
/** Restaura el estado completo desde una cotización guardada (historial) */
|
||||
restaurar: (estado: EstadoRestaurar) => void
|
||||
/** Reinicia la cotización (botón "Nueva") */
|
||||
@@ -59,24 +68,47 @@ export interface EstadoRestaurar {
|
||||
numMensualidades?: number | null
|
||||
}
|
||||
|
||||
/** Estado central de la cotización: desarrollo, unidad, acabados, apartado y descuentos */
|
||||
export function useCotizacion(): UseCotizacionResult {
|
||||
/**
|
||||
* Estado central de la cotización: desarrollo, unidad, acabados, apartado y
|
||||
* descuentos. Las reglas comerciales llegan del API (con respaldo en las
|
||||
* constantes): si el admin desactiva una propuesta, deja de ofrecerse y, si era
|
||||
* la elegida, se avisa y se cae a la primera vigente.
|
||||
*/
|
||||
export function useCotizacion(reglas: ReglasDesarrollo = SIN_REGLAS): UseCotizacionResult {
|
||||
const [desarrollo, setDesarrolloState] = useState<string>(DESARROLLO_DEFAULT)
|
||||
const [unidad, setUnidad] = useState<Unidad | null>(null)
|
||||
const [acabados, setAcabados] = useState(false)
|
||||
const [apartado, setApartado] = useState<number>(APARTADO_DEFAULT)
|
||||
const [enganchePct, setEnganchePct] = useState<number | null>(null)
|
||||
const [descuentos, setDescuentos] = useState<DescuentosPropuestas>(descuentosDefault(DESARROLLO_DEFAULT))
|
||||
const [descuentos, setDescuentos] = useState<DescuentosPropuestas>(
|
||||
descuentosDefaultEfectivo(reglas, DESARROLLO_DEFAULT),
|
||||
)
|
||||
const [propuestaIndex, setPropuestaIndex] = useState(0)
|
||||
const [numMensualidades, setNumMensualidades] = useState<number | null>(null)
|
||||
const [avisoPropuestaDesactivada, setAvisoPropuestaDesactivada] = useState<string | null>(null)
|
||||
|
||||
const params = obtenerDesarrollo(desarrollo)
|
||||
const params = desarrolloEfectivo(reglas, desarrollo)
|
||||
|
||||
// Los descuentos por defecto se re-aplican cuando cambian las reglas del
|
||||
// desarrollo (p. ej. al llegar las del API), sin pisar lo que capturó el
|
||||
// vendedor mientras las reglas no cambian.
|
||||
const descuentosAplicados = useRef<string>('')
|
||||
const defaults = useMemo(
|
||||
() => descuentosDefaultEfectivo(reglas, desarrollo),
|
||||
[reglas, desarrollo],
|
||||
)
|
||||
useEffect(() => {
|
||||
const clave = JSON.stringify(defaults)
|
||||
if (descuentosAplicados.current === clave) return
|
||||
descuentosAplicados.current = clave
|
||||
setDescuentos(defaults)
|
||||
}, [defaults])
|
||||
|
||||
const setDesarrollo = (id: string) => {
|
||||
setDesarrolloState(id)
|
||||
setUnidad(null)
|
||||
setAcabados(false)
|
||||
setDescuentos(descuentosDefault(id))
|
||||
setDescuentos(descuentosDefaultEfectivo(reglas, id))
|
||||
setPropuestaIndex(0)
|
||||
setNumMensualidades(null)
|
||||
}
|
||||
@@ -90,10 +122,12 @@ export function useCotizacion(): UseCotizacionResult {
|
||||
setUnidad(estado.unidad)
|
||||
setAcabados(estado.acabados)
|
||||
setApartado(estado.apartado)
|
||||
const base = descuentosDefault(estado.desarrollo)
|
||||
const base = descuentosDefaultEfectivo(reglas, estado.desarrollo)
|
||||
const paramsRestaurados = desarrolloEfectivo(reglas, estado.desarrollo)
|
||||
const vigentes = propuestasVigentes(paramsRestaurados)
|
||||
if (estado.descuento !== undefined) {
|
||||
const idx = estado.propuestaIndex ?? 0
|
||||
const clave = obtenerDesarrollo(estado.desarrollo).propuestas[idx]?.claveDescuento
|
||||
const clave = vigentes[idx]?.claveDescuento
|
||||
if (clave) base[clave] = estado.descuento
|
||||
}
|
||||
setDescuentos(base)
|
||||
@@ -106,9 +140,10 @@ export function useCotizacion(): UseCotizacionResult {
|
||||
setAcabados(false)
|
||||
setApartado(APARTADO_DEFAULT)
|
||||
setEnganchePct(null)
|
||||
setDescuentos(descuentosDefault(desarrollo))
|
||||
setDescuentos(descuentosDefaultEfectivo(reglas, desarrollo))
|
||||
setPropuestaIndex(0)
|
||||
setNumMensualidades(null)
|
||||
setAvisoPropuestaDesactivada(null)
|
||||
}
|
||||
|
||||
const numMesesCalculado = useMemo(
|
||||
@@ -134,8 +169,40 @@ export function useCotizacion(): UseCotizacionResult {
|
||||
|
||||
const propuestas = useMemo(() => {
|
||||
if (!unidad) return []
|
||||
return calcularPropuestas({ unidad, acabados, apartado: apartadoEfectivo, descuentos, desarrollo, enganchePct: engancheEfectivo, numMeses: numMeses ?? undefined })
|
||||
}, [unidad, acabados, apartadoEfectivo, descuentos, desarrollo, engancheEfectivo, numMeses])
|
||||
return calcularPropuestas({
|
||||
unidad,
|
||||
acabados,
|
||||
apartado: apartadoEfectivo,
|
||||
descuentos,
|
||||
desarrollo,
|
||||
reglas: params,
|
||||
enganchePct: engancheEfectivo,
|
||||
numMeses: numMeses ?? undefined,
|
||||
})
|
||||
}, [unidad, acabados, apartadoEfectivo, descuentos, desarrollo, params, engancheEfectivo, numMeses])
|
||||
|
||||
// Propuesta elegida: si el índice se sale de la lista vigente (p. ej. el admin
|
||||
// desactivó una propuesta) se cae a la primera y, si la que estaba elegida ya
|
||||
// no existe, se avisa con claridad.
|
||||
const propuestaActiva = propuestas[Math.min(propuestaIndex, Math.max(propuestas.length - 1, 0))]
|
||||
const tipoPrevio = useRef<{ tipo: string; titulo: string } | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const previo = tipoPrevio.current
|
||||
const vigentes = propuestasVigentes(params)
|
||||
if (previo !== null && !vigentes.some((p) => p.tipo === previo.tipo)) {
|
||||
const sustituto = propuestas[0]?.titulo ?? 'otra propuesta'
|
||||
setAvisoPropuestaDesactivada(
|
||||
`El administrador desactivó la propuesta ${previo.titulo} para ${params.nombre}. Se seleccionó ${sustituto}; elige otra si lo prefieres.`,
|
||||
)
|
||||
setPropuestaIndex(0)
|
||||
} else if (propuestaIndex >= propuestas.length) {
|
||||
setPropuestaIndex(0)
|
||||
}
|
||||
tipoPrevio.current = propuestaActiva
|
||||
? { tipo: propuestaActiva.tipo, titulo: propuestaActiva.titulo }
|
||||
: null
|
||||
}, [params, propuestas, propuestaActiva, propuestaIndex])
|
||||
|
||||
return {
|
||||
desarrollo,
|
||||
@@ -160,6 +227,7 @@ export function useCotizacion(): UseCotizacionResult {
|
||||
setNumMensualidades,
|
||||
numMeses,
|
||||
propuestas,
|
||||
avisoPropuestaDesactivada,
|
||||
restaurar,
|
||||
nueva,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user