feat(mejoras): 25 mejoras UX en todos los módulos + purga Monday
Backend: mi-dia siempre local, vocabulario canónico, AplicacionPago con usuario Mi día: modal español real, cita desde modal, score falso eliminado Cotizador: barra sticky, persistencia local con folio Alta Clientes: redirect eliminado, foto conectada, asesor desde sesión, refs dinámicas Cobranza: contrato Mora arreglado, link a Plan, abono próximo pago, WhatsApp Landing: contactos locales, ModalContacto eliminado, ModoTrabajo eliminado Dashboards: MiTablero unificado, funnel con descartados, drill-down, rango fechas Chat FRASA: corpus 5 desarrollos, 6 herramientas, historial conversaciones 69 archivos, 7 apps construidas
This commit is contained in:
@@ -9,6 +9,7 @@ import type {
|
||||
TipoCredito,
|
||||
SlotDocumento,
|
||||
} from '../types'
|
||||
import { getSesion } from '../services/auth'
|
||||
|
||||
export const PROYECTOS = [
|
||||
'RESIDENCIAL MONTEBELLO',
|
||||
@@ -102,5 +103,9 @@ export const DESARROLLOS_LEAD = ['ATELIER', 'DIAGONAL', 'ESTANZA', 'SENDA', 'SOL
|
||||
// El modelo de interés sale de MODELOS_POR_PROYECTO según el desarrollo elegido.
|
||||
export const OPCIONES_CALIFICA = ['Sí', 'No', 'Por confirmar'] as const
|
||||
|
||||
export const ASESOR_DEFAULT = 'ESMERALDA BECERRA DOMINGUEZ'
|
||||
/**
|
||||
* Nombre del asesor para el campo de firma del alta: se toma de la sesión
|
||||
* activa en el momento en que arranca el alta. Sin sesión queda vacío.
|
||||
*/
|
||||
export const asesorDeSesion = (): string => getSesion()?.usuario.nombre ?? ''
|
||||
export const AUTORIDAD_ID_DEFAULT = 'INSTITUTO NACIONAL ELECTORAL'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { z } from 'zod'
|
||||
import { useForm, FormProvider } from 'react-hook-form'
|
||||
import { useForm, FormProvider, useFieldArray } from 'react-hook-form'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import type { Referencia } from '../types'
|
||||
import { referenciasSchema } from '../lib/schemas'
|
||||
@@ -11,13 +11,18 @@ import type { PasoProps } from './DatosGeneralesForm'
|
||||
// RHF trabaja con objetos raíz; el array va envuelto
|
||||
const schemaFormulario = z.object({ referencias: referenciasSchema })
|
||||
|
||||
/** Paso 5: referencias personales (4 filas, mínimo 1 con nombre y teléfono) */
|
||||
const MAX_REFERENCIAS = 4
|
||||
|
||||
const referenciaVacia = (): Referencia => ({ nombre: '', telefono: '', parentesco: '', direccion: '' })
|
||||
|
||||
/** Paso 5: referencias personales (1 a 4 bloques; al menos 1 con nombre y teléfono) */
|
||||
export default function ReferenciasForm({ valores, onAnterior, onSiguiente }: PasoProps<Referencia[]>) {
|
||||
const metodos = useForm<{ referencias: Referencia[] }>({
|
||||
resolver: zodResolver(schemaFormulario),
|
||||
defaultValues: { referencias: valores },
|
||||
mode: 'onSubmit',
|
||||
})
|
||||
const { fields, append, remove } = useFieldArray({ control: metodos.control, name: 'referencias' })
|
||||
const errorRaiz = metodos.formState.errors.referencias
|
||||
|
||||
return (
|
||||
@@ -30,9 +35,24 @@ export default function ReferenciasForm({ valores, onAnterior, onSiguiente }: Pa
|
||||
titulo="Referencias personales"
|
||||
descripcion="Captura al menos 1 referencia con nombre y teléfono."
|
||||
>
|
||||
{[0, 1, 2, 3].map((i) => (
|
||||
<div key={i} className="rounded-xl border border-[#E5E7EB] p-6">
|
||||
<p className="mb-4 text-xs font-semibold uppercase tracking-wider text-[#64748B]">Referencia {i + 1}</p>
|
||||
{fields.map((field, i) => (
|
||||
<div key={field.id} className="rounded-xl border border-[#E5E7EB] p-6">
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
Referencia {i + 1}
|
||||
</p>
|
||||
{fields.length > 1 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => remove(i)}
|
||||
aria-label={`Eliminar referencia ${i + 1}`}
|
||||
title="Eliminar referencia"
|
||||
className="flex h-7 w-7 items-center justify-center rounded-full border border-red-200 text-sm leading-none text-red-600 transition-colors duration-100 hover:bg-red-50"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<CampoTexto nombre={`referencias.${i}.nombre`} etiqueta="Nombre completo" />
|
||||
<CampoTexto nombre={`referencias.${i}.telefono`} etiqueta="Teléfono" />
|
||||
@@ -41,6 +61,17 @@ export default function ReferenciasForm({ valores, onAnterior, onSiguiente }: Pa
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="md:col-span-2 lg:col-span-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => append(referenciaVacia())}
|
||||
disabled={fields.length >= MAX_REFERENCIAS}
|
||||
title={fields.length >= MAX_REFERENCIAS ? `Máximo ${MAX_REFERENCIAS} referencias` : undefined}
|
||||
className="rounded-lg border border-accent bg-white px-4 py-2 text-sm font-medium text-accent transition-colors duration-100 hover:bg-accent hover:text-white disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
+ Agregar referencia
|
||||
</button>
|
||||
</div>
|
||||
</SectionCard>
|
||||
{errorRaiz && typeof errorRaiz.message === 'string' && (
|
||||
<p className="mt-2 px-6 text-xs font-semibold text-red-600">{errorRaiz.message}</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import type { AltaCliente } from '../types'
|
||||
import { fechaISOHoy } from '../lib/formatters'
|
||||
import { ASESOR_DEFAULT, AUTORIDAD_ID_DEFAULT } from '../data/constantes'
|
||||
import { asesorDeSesion, AUTORIDAD_ID_DEFAULT } from '../data/constantes'
|
||||
import { useDocumentos } from './useDocumentos'
|
||||
|
||||
const datosGeneralesVacios = (): AltaCliente['datosGenerales'] => ({
|
||||
@@ -56,10 +56,18 @@ const economicosVacios = (): AltaCliente['economicos'] => ({
|
||||
dependientes: { personas: '', parentesco: '', edad: '' },
|
||||
})
|
||||
|
||||
const referenciaVacia = (): AltaCliente['referencias'][number] => ({
|
||||
nombre: '',
|
||||
telefono: '',
|
||||
parentesco: '',
|
||||
direccion: '',
|
||||
})
|
||||
|
||||
const estadoInicial = (): Omit<AltaCliente, 'documentos'> => ({
|
||||
leadMondayId: null,
|
||||
apartadoMondayId: null,
|
||||
progresoId: null,
|
||||
foto: null,
|
||||
datosGenerales: datosGeneralesVacios(),
|
||||
empleo: empleoVacio(),
|
||||
economicos: economicosVacios(),
|
||||
@@ -69,12 +77,7 @@ const estadoInicial = (): Omit<AltaCliente, 'documentos'> => ({
|
||||
empleo: empleoVacio(),
|
||||
economicos: economicosVacios(),
|
||||
},
|
||||
referencias: [
|
||||
{ nombre: '', telefono: '', parentesco: '', direccion: '' },
|
||||
{ nombre: '', telefono: '', parentesco: '', direccion: '' },
|
||||
{ nombre: '', telefono: '', parentesco: '', direccion: '' },
|
||||
{ nombre: '', telefono: '', parentesco: '', direccion: '' },
|
||||
],
|
||||
referencias: [referenciaVacia()],
|
||||
viviendaCredito: {
|
||||
proyecto: '',
|
||||
proyectoOtro: '',
|
||||
@@ -107,7 +110,7 @@ const estadoInicial = (): Omit<AltaCliente, 'documentos'> => ({
|
||||
pais: 'MEXICO',
|
||||
},
|
||||
},
|
||||
firma: { firmaDataUrl: '', asesorNombre: ASESOR_DEFAULT },
|
||||
firma: { firmaDataUrl: '', asesorNombre: asesorDeSesion() },
|
||||
})
|
||||
|
||||
export const PASOS = [
|
||||
|
||||
@@ -12,6 +12,7 @@ export const fixtureAlta: AltaCliente = {
|
||||
leadMondayId: null,
|
||||
apartadoMondayId: null,
|
||||
progresoId: null,
|
||||
foto: null,
|
||||
datosGenerales: {
|
||||
campaniaVenta: '',
|
||||
fecha: '2026-07-19',
|
||||
|
||||
@@ -170,4 +170,17 @@ describe('referenciasSchema', () => {
|
||||
])
|
||||
expect(r.success).toBe(true)
|
||||
})
|
||||
|
||||
it('acepta un solo bloque de referencia (arreglo dinámico, ya no exige 4)', () => {
|
||||
const r = referenciasSchema.safeParse([
|
||||
{ nombre: 'JOSE ARNOLDO MENDOZA', telefono: '6641887799', parentesco: 'AMIGO', direccion: 'ALTAMIRA' },
|
||||
])
|
||||
expect(r.success).toBe(true)
|
||||
})
|
||||
|
||||
it('rechaza más de 4 referencias', () => {
|
||||
const valida = { nombre: 'JOSE ARNOLDO MENDOZA', telefono: '6641887799', parentesco: '', direccion: '' }
|
||||
const r = referenciasSchema.safeParse([valida, valida, valida, valida, valida])
|
||||
expect(r.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -149,7 +149,8 @@ const referenciaSchema = z.object({
|
||||
|
||||
export const referenciasSchema = z
|
||||
.array(referenciaSchema)
|
||||
.length(4, 'Se capturan 4 referencias')
|
||||
.min(1, 'Captura al menos 1 referencia con nombre y teléfono')
|
||||
.max(4, 'Máximo 4 referencias')
|
||||
.refine(
|
||||
(refs) => refs.some((r) => r.nombre.trim() !== '' && r.telefono.trim() !== ''),
|
||||
'Captura al menos 1 referencia con nombre y teléfono',
|
||||
|
||||
@@ -146,6 +146,11 @@ describe('AltaClientePage — guardado progresivo (etapa por etapa)', () => {
|
||||
),
|
||||
).toHaveLength(0)
|
||||
// El flujo posterior queda idéntico: alta registrada con el id del progreso
|
||||
expect(await screen.findByText(/Alta registrada en el servidor: prog-1/)).toBeTruthy()
|
||||
expect(await screen.findByText(/Alta registrada correctamente/)).toBeTruthy()
|
||||
expect(screen.getByText(/Folio prog-1/)).toBeTruthy()
|
||||
// Pantalla de éxito con las acciones post-registro (sin redirect automático)
|
||||
expect(screen.getByRole('button', { name: 'Generar liga de firma' })).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'Ir a Mi día' })).toBeTruthy()
|
||||
expect(screen.getByRole('button', { name: 'Registrar otro cliente' })).toBeTruthy()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -226,9 +226,9 @@ export default function AltaClientePage() {
|
||||
setPantalla('wizard')
|
||||
}
|
||||
|
||||
const guardar = async () => {
|
||||
const guardar = async (pasoDestino: number = paso) => {
|
||||
try {
|
||||
const b = await guardarBorrador({ ...(borradorId ? { id: borradorId } : {}), paso, datos })
|
||||
const b = await guardarBorrador({ ...(borradorId ? { id: borradorId } : {}), paso: pasoDestino, datos })
|
||||
toast.success("Borrador guardado")
|
||||
setBorradorId(b.id)
|
||||
setAvisoBorrador(
|
||||
@@ -239,6 +239,49 @@ export default function AltaClientePage() {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- Guard de cambios sin guardar (saltos con el Stepper) ----------
|
||||
// El formulario del paso guarda sus capturas solo al avanzar con "Siguiente";
|
||||
// si el usuario salta a otro paso con el Stepper, se le ofrece guardar antes.
|
||||
const [sucio, setSucio] = useState(false)
|
||||
// Paso elegido en el Stepper mientras el confirm de cambios sin guardar está abierto
|
||||
const [destinoPendiente, setDestinoPendiente] = useState<number | null>(null)
|
||||
|
||||
// Al cambiar de paso el formulario se desmonta y remonta: se parte de cero
|
||||
useEffect(() => {
|
||||
setSucio(false)
|
||||
}, [paso])
|
||||
|
||||
/** Cualquier edición dentro del paso actual marca cambios sin guardar */
|
||||
const marcarSucio = () => setSucio(true)
|
||||
|
||||
/** Cambio de paso vía Stepper: confirma antes de saltar si hay cambios sin guardar */
|
||||
const irAPasoConGuard = (destino: number) => {
|
||||
if (destino === paso) return
|
||||
if (sucio) {
|
||||
setDestinoPendiente(destino)
|
||||
return
|
||||
}
|
||||
irAPaso(destino)
|
||||
}
|
||||
|
||||
/** "Guardar": persiste el borrador apuntando al paso destino y continúa hacia él */
|
||||
const guardarYContinuar = async () => {
|
||||
const destino = destinoPendiente
|
||||
if (destino === null) return
|
||||
setDestinoPendiente(null)
|
||||
await guardar(destino)
|
||||
irAPaso(destino)
|
||||
}
|
||||
|
||||
/** "Continuar sin guardar": descarta lo capturado en el paso y salta al destino */
|
||||
const continuarSinGuardar = () => {
|
||||
const destino = destinoPendiente
|
||||
if (destino === null) return
|
||||
setDestinoPendiente(null)
|
||||
setSucio(false)
|
||||
irAPaso(destino)
|
||||
}
|
||||
|
||||
// ---------- Guardado progresivo (borrador local + Contacto en Monday) ----------
|
||||
// Guard in-flight: con clicks rápidos se omite el sync en curso; el siguiente
|
||||
// avance manda el estado completo (el PATCH es idempotente).
|
||||
@@ -309,14 +352,20 @@ export default function AltaClientePage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Redirigir a Mi día después de completar el alta
|
||||
useEffect(() => {
|
||||
if (!registro) return
|
||||
const timer = setTimeout(() => {
|
||||
window.location.href = '/hoy/'
|
||||
}, 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}, [registro])
|
||||
// Pantalla de éxito tras registrar el alta (en lugar del redirect automático a Mi día)
|
||||
const registrarOtro = () => {
|
||||
setRegistro('')
|
||||
setSinConexion(false)
|
||||
setErrorValidacion('')
|
||||
setLigaUrl('')
|
||||
setLigaCopiada(false)
|
||||
setErrorLiga('')
|
||||
empezarNuevo()
|
||||
}
|
||||
|
||||
const irAMiDia = () => {
|
||||
window.location.href = '/hoy/'
|
||||
}
|
||||
|
||||
// Genera la liga pública y copia al portapapeles el texto listo para WhatsApp
|
||||
const generarLiga = async () => {
|
||||
@@ -485,6 +534,77 @@ export default function AltaClientePage() {
|
||||
)
|
||||
}
|
||||
|
||||
// ---------- Pantalla de éxito: el alta quedó registrada en el servidor ----------
|
||||
if (registro) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A]">
|
||||
<AppHeader titulo="Alta de Clientes" />
|
||||
<main className="px-4 py-8 sm:px-6 sm:py-12 lg:px-8">
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<div className="rounded-xl border border-green-200 bg-green-50 p-6 dark:border-green-900 dark:bg-green-950/40">
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-green-100 text-lg text-green-700 dark:bg-green-900 dark:text-green-400"
|
||||
>
|
||||
✓
|
||||
</span>
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-lg font-semibold text-green-800 dark:text-green-300">
|
||||
Alta registrada correctamente
|
||||
</h1>
|
||||
<p className="mt-0.5 truncate text-xs text-green-700 dark:text-green-400">
|
||||
{alta.datosGenerales.nombreCompleto || 'Cliente'} · Folio {registro}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 flex flex-col gap-3 sm:flex-row sm:flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void generarLiga()}
|
||||
className="rounded-lg bg-accent px-5 py-2.5 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover"
|
||||
>
|
||||
Generar liga de firma
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={irAMiDia}
|
||||
className="rounded-lg border border-[#D1D5DB] bg-white px-5 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
>
|
||||
Ir a Mi día
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={registrarOtro}
|
||||
className="rounded-lg border border-[#D1D5DB] bg-white px-5 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
>
|
||||
Registrar otro cliente
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="mt-6 text-xs text-green-800/80 dark:text-green-400/80">
|
||||
Con la liga de firma, el cliente firma su expediente y sube documentos desde su
|
||||
celular (vigencia 72 h, sin usuario ni contraseña).
|
||||
</p>
|
||||
{ligaUrl && (
|
||||
<p className="mt-3 break-all text-xs text-[#111827] dark:text-[#E2E8F0]">
|
||||
{ligaUrl}
|
||||
{ligaCopiada && (
|
||||
<span role="status" className="ml-2 font-medium text-accent">
|
||||
Liga y mensaje de WhatsApp copiados
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{errorLiga && <p className="mt-2 text-xs text-red-700">{errorLiga}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A]">
|
||||
<AppHeader
|
||||
@@ -515,10 +635,10 @@ export default function AltaClientePage() {
|
||||
<main className="px-4 py-6 sm:px-6 sm:py-8 lg:px-8">
|
||||
<div className="md:grid md:grid-cols-[260px_minmax(0,1fr)] md:gap-8 lg:gap-8">
|
||||
<div className="mb-6 md:sticky md:top-20 md:mb-0 md:self-start">
|
||||
<Stepper pasos={[...PASOS]} pasoActual={paso} onIrAPaso={irAPaso} />
|
||||
<Stepper pasos={[...PASOS]} pasoActual={paso} onIrAPaso={irAPasoConGuard} />
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-3xl">
|
||||
<div className="w-full max-w-3xl" onInput={marcarSucio} onChange={marcarSucio}>
|
||||
{errorCarga && (
|
||||
<p role="alert" className="mb-6 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-700">
|
||||
{errorCarga}
|
||||
@@ -549,9 +669,10 @@ export default function AltaClientePage() {
|
||||
</p>
|
||||
<div className="mt-3">
|
||||
<FotoCliente
|
||||
foto={null}
|
||||
onFoto={() => {
|
||||
// Foto capturada (el componente gestiona el preview internamente)
|
||||
foto={datos.foto}
|
||||
onFoto={(foto) => {
|
||||
actualizar('foto', foto || null)
|
||||
marcarSucio()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -662,16 +783,6 @@ export default function AltaClientePage() {
|
||||
)}
|
||||
{paso === 7 && (
|
||||
<>
|
||||
{registro && (
|
||||
<p className="mb-6 rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-700">
|
||||
Alta registrada en el servidor: {registro}
|
||||
</p>
|
||||
)}
|
||||
{registro && (
|
||||
<p className="mb-6 text-xs text-[#64748B] dark:text-[#94A3B8]">
|
||||
Redirigiendo a Mi día en unos segundos…
|
||||
</p>
|
||||
)}
|
||||
{sinConexion && (
|
||||
<p
|
||||
role="alert"
|
||||
@@ -688,35 +799,6 @@ export default function AltaClientePage() {
|
||||
El servidor rechazó el alta: {errorValidacion}
|
||||
</p>
|
||||
)}
|
||||
{registro && (
|
||||
<div className="mb-6 rounded-xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
|
||||
Firma en remoto
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-[#64748B] dark:text-[#94A3B8]">
|
||||
Genera una liga pública (vigencia 72 h) para que el cliente firme y suba sus
|
||||
documentos desde su celular, sin usuario ni contraseña.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void generarLiga()}
|
||||
className="mt-4 rounded-lg border border-[#D1D5DB] bg-white px-5 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
>
|
||||
Generar liga para firmar en remoto
|
||||
</button>
|
||||
{ligaUrl && (
|
||||
<p className="mt-4 break-all text-xs text-[#111827] dark:text-[#E2E8F0]">
|
||||
{ligaUrl}
|
||||
{ligaCopiada && (
|
||||
<span role="status" className="ml-2 font-medium text-accent">
|
||||
Liga y mensaje de WhatsApp copiados
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{errorLiga && <p className="mt-2 text-xs text-red-700">{errorLiga}</p>}
|
||||
</div>
|
||||
)}
|
||||
<TarjetaPaso>
|
||||
<FirmasForm
|
||||
alta={alta}
|
||||
@@ -729,6 +811,44 @@ export default function AltaClientePage() {
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Confirmación de cambios sin guardar al saltar de paso con el Stepper */}
|
||||
{destinoPendiente !== null && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-neutral-900/50 p-4"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="guard-cambios-titulo"
|
||||
>
|
||||
<div className="w-full max-w-md rounded-2xl border border-[#E5E7EB] bg-white p-6 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-xl">
|
||||
<h2
|
||||
id="guard-cambios-titulo"
|
||||
className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]"
|
||||
>
|
||||
Cambios sin guardar
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-[#4B5563] dark:text-[#CBD5E1]">
|
||||
¿Deseas guardar antes de continuar?
|
||||
</p>
|
||||
<div className="mt-6 flex justify-end gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={continuarSinGuardar}
|
||||
className="rounded-lg border border-[#D1D5DB] bg-white px-4 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
>
|
||||
Continuar sin guardar
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void guardarYContinuar()}
|
||||
className="rounded-lg bg-accent px-4 py-2 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover"
|
||||
>
|
||||
Guardar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -157,6 +157,9 @@ export interface AltaCliente {
|
||||
// ID del Cliente creado por el guardado progresivo (POST /clientes/progreso/
|
||||
// iniciar); viaja con el borrador (datosJson) y no se captura en el formato.
|
||||
progresoId: string | null
|
||||
// Foto del cliente (data URL base64) capturada en el paso de datos generales;
|
||||
// null cuando aún no se toma. Viaja en el payload del alta y en los borradores.
|
||||
foto: string | null
|
||||
datosGenerales: DatosGenerales
|
||||
empleo: DatosEmpleo
|
||||
economicos: DatosEconomicos
|
||||
|
||||
Reference in New Issue
Block a user