feat(mi-dia): tabs Mis oportunidades/Mis apartados con fases y cotizador vinculado
- Resumen Mi día: misOportunidades.items (lista de trabajo con perfil y último cambio) y misApartados (Vendedor = yo, estados no finales) - Nuevos endpoints: POST /oportunidades/:id/etapa (Agendo Cita, Afluencia, Cotizador, Aparto, No Asistio, Abandono; con fecha de cita) y POST /apartados/:id/estado (pipeline Enkontrol: Pago Apartado → Firma Cliente (Venta) / Cancelacion Apartado); ambos mueven el item al grupo Monday correspondiente - Mi día: tabs Mis leads / Mis oportunidades / Mis apartados; botón Cotizar abre /quote con el cliente prellenado y la oportunidad ligada - Cotizador: prefill desde query params (cliente, teléfono, email, desarrollo, unidad) y envío de oportunidadId; al guardar, el API actualiza ESE item (Precio final, Tipo propuesta, etapa Cotizador) en vez de buscar por teléfono o crear duplicado - grupos-estado: 'Cancelacion Apartado' → Historico - Estados finales de apartado: agrega Firma Cliente (Venta) y Cancelacion Apartado - Tests: 455 API + 14 mi-dia + 88 cotizador, todo en verde
This commit is contained in:
@@ -81,6 +81,10 @@ export default function CotizadorPage() {
|
||||
const [modalClienteAbierto, setModalClienteAbierto] = useState(false)
|
||||
const [guardando, setGuardando] = useState(false)
|
||||
const [aviso, setAviso] = useState<string | null>(null)
|
||||
// Prefill desde Mi día (botón "Cotizar" de una oportunidad): el id queda
|
||||
// ligado a la cotización al guardar; la unidad se preselecciona al cargar
|
||||
const [oportunidadId, setOportunidadId] = useState<string | null>(null)
|
||||
const [unidadPendiente, setUnidadPendiente] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
let vivo = true
|
||||
@@ -113,6 +117,41 @@ export default function CotizadorPage() {
|
||||
})
|
||||
|
||||
const nombreCliente = watch('nombre') ?? ''
|
||||
|
||||
// Prefill desde la URL (botón "Cotizar" de Mi día): cliente, contacto,
|
||||
// desarrollo, unidad y el id de la oportunidad a vincular al guardar
|
||||
useEffect(() => {
|
||||
const qs = new URLSearchParams(window.location.search)
|
||||
const cliente = qs.get('cliente')
|
||||
const telefono = qs.get('telefono')
|
||||
const email = qs.get('email')
|
||||
const desarrolloParam = qs.get('desarrollo')
|
||||
const unidadParam = qs.get('unidad')
|
||||
const opId = qs.get('oportunidadId')
|
||||
if (cliente) setValue('nombre', cliente, { shouldValidate: true })
|
||||
if (telefono) setValue('telefono', telefono, { shouldValidate: true })
|
||||
if (email) setValue('email', email, { shouldValidate: true })
|
||||
if (desarrolloParam && LISTA_DESARROLLOS.includes(desarrolloParam)) setDesarrollo(desarrolloParam)
|
||||
if (unidadParam) setUnidadPendiente(unidadParam)
|
||||
if (opId) setOportunidadId(opId)
|
||||
// Solo al montar: es el punto de entrada desde Mi día
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
|
||||
// Preselecciona la unidad cuando el inventario terminó de cargar
|
||||
useEffect(() => {
|
||||
if (unidadPendiente === null || cargando) return
|
||||
const encontrada = unidades.find((u) => u.codigo === unidadPendiente)
|
||||
if (encontrada) {
|
||||
const desarrolloUnidad = encontrada.desarrollo ?? 'DIAGONAL'
|
||||
if (desarrolloUnidad !== desarrollo) setDesarrollo(desarrolloUnidad)
|
||||
setUnidad(encontrada)
|
||||
setAviso(`Unidad ${encontrada.codigo} preseleccionada desde Mi día.`)
|
||||
}
|
||||
setUnidadPendiente(null)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [unidades, cargando, unidadPendiente])
|
||||
|
||||
// La propuesta elegida (para guardar/compartir); acotada por si cambia el desarrollo
|
||||
const propuestaActiva = propuestas[Math.min(propuestaIndex, Math.max(propuestas.length - 1, 0))]
|
||||
const puedeGenerarPdf = Boolean(unidad) && !errorApartado && !guardando
|
||||
@@ -130,6 +169,7 @@ export default function CotizadorPage() {
|
||||
acabados,
|
||||
apartado: apartadoEfectivo,
|
||||
descuento: propuestaActiva.descuento,
|
||||
...(oportunidadId !== null ? { oportunidadId } : {}),
|
||||
})
|
||||
const conFolio = { ...guardada, folio: guardada.folio ?? folioCotizacion(guardada.id) }
|
||||
setUltimaGuardada(conFolio)
|
||||
@@ -250,6 +290,7 @@ export default function CotizadorPage() {
|
||||
nueva()
|
||||
reset({ nombre: '', email: '', telefono: '' })
|
||||
setUltimaGuardada(null)
|
||||
setOportunidadId(null) // la nueva cotización ya no va ligada a la oportunidad anterior
|
||||
setAviso(null)
|
||||
}
|
||||
|
||||
@@ -265,6 +306,15 @@ export default function CotizadorPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{oportunidadId !== null && nombreCliente.trim() !== '' && (
|
||||
<p
|
||||
role="status"
|
||||
className="mb-6 rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm font-medium text-blue-800"
|
||||
>
|
||||
Cotizando para {nombreCliente} — al guardar, la cotización queda vinculada a su oportunidad en Monday.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="grid items-start gap-6 lg:grid-cols-[380px_minmax(0,1fr)] lg:gap-8">
|
||||
{/* Panel de configuración */}
|
||||
<aside className="space-y-6 lg:sticky lg:top-24">
|
||||
|
||||
@@ -80,6 +80,8 @@ export interface GuardarCotizacionPayload {
|
||||
acabados: boolean
|
||||
apartado?: number
|
||||
descuento?: number
|
||||
/** Item del tablero Oportunidades a vincular (botón "Cotizar" de Mi día) */
|
||||
oportunidadId?: string
|
||||
}
|
||||
|
||||
/** URL pública de una cotización (destino del QR del PDF) */
|
||||
|
||||
Reference in New Issue
Block a user