style: rediseno de layout profesional en las 7 apps
Headers sticky con marca y acciones, contenedores max-w-[1280px], grids de contenido con rails laterales sticky (cotizador, mi-dia, detalle de cobranza), stepper vertical en el wizard de alta, toolbar+ tabla unificadas en cobranza, tabs como segmented control en dashboards, chat con barra de entrada fija y preguntas sugeridas, landing con navbar + hero + grid de modulos + footer.
This commit is contained in:
@@ -5,8 +5,8 @@ import LoginScreen from './LoginScreen'
|
||||
|
||||
/**
|
||||
* Gate de autenticación: sin sesión solo se ve el login; con sesión se ve la
|
||||
* app con una barra discreta "nombre · rol", "Contraseña" (modal de cambio)
|
||||
* y botón "Salir".
|
||||
* app con un header blanco sticky (marca a la izquierda; "nombre · rol",
|
||||
* "Contraseña" (modal de cambio) y botón "Salir" a la derecha).
|
||||
*/
|
||||
export default function AuthGate({ children }: { children: ReactNode }) {
|
||||
const [sesion, setSesion] = useState<Sesion | null>(() => getSesion())
|
||||
@@ -18,16 +18,22 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
|
||||
<div className="border-b border-[#E5E7EB] bg-white">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
||||
<span className="text-xs font-medium text-[#64748B]">
|
||||
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
||||
</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<header className="sticky top-0 z-10 border-b border-[#E5E7EB] bg-white/95 backdrop-blur">
|
||||
<div className="mx-auto flex h-16 max-w-[1280px] items-center justify-between gap-4 px-4 sm:px-6">
|
||||
<p className="text-sm text-[#111827]">
|
||||
<span className="font-bold">
|
||||
FRASA DESARROLLOS<span className="text-[#F97316]">.</span>
|
||||
</span>{' '}
|
||||
<span className="hidden font-normal text-[#64748B] sm:inline">— Cotizador</span>
|
||||
</p>
|
||||
<div className="flex items-center gap-3 sm:gap-4">
|
||||
<span className="hidden text-xs font-medium text-[#64748B] md:inline">
|
||||
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setModalPassword(true)}
|
||||
className="text-xs font-medium text-[#64748B] transition-colors duration-100 hover:text-[#111827]"
|
||||
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
>
|
||||
Contraseña
|
||||
</button>
|
||||
@@ -43,7 +49,7 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
{children}
|
||||
{modalPassword && <CambiarPasswordModal onCerrar={() => setModalPassword(false)} />}
|
||||
</div>
|
||||
|
||||
@@ -72,7 +72,7 @@ export function ComparadorPropuestas({ precioLista, apartado, propuestas, titulo
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="mt-16" aria-labelledby="comparador-titulo">
|
||||
<section aria-labelledby="comparador-titulo">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<h2
|
||||
id="comparador-titulo"
|
||||
|
||||
@@ -99,7 +99,7 @@ export function ComparadorUnidades({ unidades, tipo, apartado, acabados }: Props
|
||||
)
|
||||
|
||||
return (
|
||||
<section className="mt-16" aria-labelledby="comparador-unidades-titulo">
|
||||
<section aria-labelledby="comparador-unidades-titulo">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<h2
|
||||
id="comparador-unidades-titulo"
|
||||
|
||||
@@ -255,278 +255,276 @@ export default function CotizadorPage() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC]">
|
||||
<header className="border-b border-[#E5E7EB] bg-white">
|
||||
<div className="mx-auto flex max-w-7xl flex-wrap items-baseline justify-between gap-2 px-4 py-6">
|
||||
<h1 className="text-lg text-[#111827]">
|
||||
<span className="font-bold">FRASA DESARROLLOS<span className="text-[#F97316]">.</span></span>{' '}
|
||||
<span className="font-normal text-[#64748B]">— Cotizador</span>
|
||||
</h1>
|
||||
<div className="text-right">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B] capitalize">
|
||||
{formatoFechaLarga(new Date())}
|
||||
</p>
|
||||
<p className="mt-1 text-xs text-[#64748B]">
|
||||
Vigencia: hasta el {formatoFechaLarga(calcularVigencia())}
|
||||
</p>
|
||||
</div>
|
||||
<main className="mx-auto max-w-[1280px] px-4 py-8 sm:px-6 lg:py-10">
|
||||
{/* Encabezado de página */}
|
||||
<div className="mb-8">
|
||||
<h1 className="text-xl font-semibold text-[#111827]">Cotizador</h1>
|
||||
<p className="mt-1 text-sm text-[#64748B] capitalize">{formatoFechaLarga(new Date())}</p>
|
||||
<p className="text-sm text-[#64748B]">
|
||||
Vigencia: hasta el {formatoFechaLarga(calcularVigencia())}
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main className="mx-auto grid max-w-7xl gap-x-8 gap-y-16 px-4 py-16 lg:grid-cols-[minmax(320px,380px)_1fr]">
|
||||
{/* Panel de configuración */}
|
||||
<aside className="space-y-16">
|
||||
<section className="space-y-8 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||
{error && <p className="text-xs text-red-700">{error}</p>}
|
||||
{usandoSeed && !cargando && (
|
||||
<p className="text-xs text-amber-700">Datos de demostración (sin conexión al servidor)</p>
|
||||
)}
|
||||
{cargando ? (
|
||||
<p className="text-sm text-[#64748B]">Cargando unidades…</p>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<span className={etiquetaCampo}>Desarrollo</span>
|
||||
<div className="flex flex-wrap overflow-hidden rounded-lg border border-[#D1D5DB]" role="group">
|
||||
{LISTA_DESARROLLOS.map((id) => {
|
||||
const activo = id === desarrollo
|
||||
const total = unidades.filter((u) => (u.desarrollo ?? 'DIAGONAL') === id).length
|
||||
const disponibles = unidades.filter(
|
||||
(u) => (u.desarrollo ?? 'DIAGONAL') === id && u.status === 'DISPONIBLE',
|
||||
).length
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
aria-pressed={activo}
|
||||
onClick={() => setDesarrollo(id)}
|
||||
className={`px-4 py-2.5 text-xs font-medium transition-colors duration-100 ${
|
||||
activo
|
||||
? 'bg-accent text-white'
|
||||
: 'bg-white text-[#4B5563] hover:bg-[#F9FAFB]'
|
||||
}`}
|
||||
>
|
||||
{obtenerDesarrollo(id).nombre}
|
||||
<span className={`ml-1 font-normal ${activo ? 'text-blue-100' : 'text-[#64748B]'}`}>
|
||||
{disponibles}/{total}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<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">
|
||||
<section className="space-y-6 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||
{error && <p className="text-xs text-red-700">{error}</p>}
|
||||
{usandoSeed && !cargando && (
|
||||
<p className="text-xs text-amber-700">Datos de demostración (sin conexión al servidor)</p>
|
||||
)}
|
||||
{cargando ? (
|
||||
<p className="text-sm text-[#64748B]">Cargando unidades…</p>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<span className={etiquetaCampo}>Desarrollo</span>
|
||||
<div className="flex flex-wrap overflow-hidden rounded-lg border border-[#D1D5DB]" role="group">
|
||||
{LISTA_DESARROLLOS.map((id) => {
|
||||
const activo = id === desarrollo
|
||||
const total = unidades.filter((u) => (u.desarrollo ?? 'DIAGONAL') === id).length
|
||||
const disponibles = unidades.filter(
|
||||
(u) => (u.desarrollo ?? 'DIAGONAL') === id && u.status === 'DISPONIBLE',
|
||||
).length
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
type="button"
|
||||
aria-pressed={activo}
|
||||
onClick={() => setDesarrollo(id)}
|
||||
className={`px-4 py-2.5 text-xs font-medium transition-colors duration-100 ${
|
||||
activo
|
||||
? 'bg-accent text-white'
|
||||
: 'bg-white text-[#4B5563] hover:bg-[#F9FAFB]'
|
||||
}`}
|
||||
>
|
||||
{obtenerDesarrollo(id).nombre}
|
||||
<span className={`ml-1 font-normal ${activo ? 'text-blue-100' : 'text-[#64748B]'}`}>
|
||||
{disponibles}/{total}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-[#64748B]">
|
||||
{unidadesDesarrollo.length} unidades · {disponiblesDesarrollo} disponibles
|
||||
</p>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-[#64748B]">
|
||||
{unidadesDesarrollo.length} unidades · {disponiblesDesarrollo} disponibles
|
||||
<UnitSelector
|
||||
unidades={unidadesDesarrollo}
|
||||
valor={unidad?.codigo ?? ''}
|
||||
onChange={(codigo) =>
|
||||
setUnidad(unidadesDesarrollo.find((u) => u.codigo === codigo) ?? null)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!params.precioUnico && (
|
||||
<div>
|
||||
<span className={etiquetaCampo}>Cocina y closet</span>
|
||||
<div className="inline-flex overflow-hidden rounded-lg border border-[#D1D5DB]" role="group">
|
||||
{(['SI', 'NO'] as const).map((opcion) => {
|
||||
const activo = (opcion === 'SI') === acabados
|
||||
return (
|
||||
<button
|
||||
key={opcion}
|
||||
type="button"
|
||||
aria-pressed={activo}
|
||||
onClick={() => setAcabados(opcion === 'SI')}
|
||||
className={`px-5 py-2.5 text-xs font-medium transition-colors duration-100 ${
|
||||
activo
|
||||
? 'bg-accent text-white'
|
||||
: 'bg-white text-[#4B5563] hover:bg-[#F9FAFB]'
|
||||
}`}
|
||||
>
|
||||
{opcion}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="apartado" className={etiquetaCampo}>
|
||||
Monto de apartado (MXN)
|
||||
</label>
|
||||
<input
|
||||
id="apartado"
|
||||
type="number"
|
||||
min={0}
|
||||
step={1000}
|
||||
value={apartado}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value)
|
||||
setApartado(Number.isFinite(v) && v >= 0 ? v : 0)
|
||||
}}
|
||||
className={errorApartado ? campoInputError : campoInput}
|
||||
/>
|
||||
{errorApartado ? (
|
||||
<p className="mt-1 text-xs text-red-700">{errorApartado}</p>
|
||||
) : (
|
||||
<p className="mt-1 text-xs text-[#64748B]">{formatoMoneda(apartado)}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="nombre" className={etiquetaCampo}>
|
||||
Nombre del cliente *
|
||||
</label>
|
||||
<input
|
||||
id="nombre"
|
||||
type="text"
|
||||
className={errors.nombre ? campoInputError : campoInput}
|
||||
{...register('nombre')}
|
||||
/>
|
||||
{errors.nombre && <p className="mt-1 text-xs text-red-700">{errors.nombre.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className={etiquetaCampo}>
|
||||
Correo electrónico (opcional)
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
className={errors.email ? campoInputError : campoInput}
|
||||
{...register('email')}
|
||||
/>
|
||||
{errors.email && <p className="mt-1 text-xs text-red-700">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="telefono" className={etiquetaCampo}>
|
||||
Teléfono (opcional)
|
||||
</label>
|
||||
<input
|
||||
id="telefono"
|
||||
type="tel"
|
||||
className={errors.telefono ? campoInputError : campoInput}
|
||||
{...register('telefono')}
|
||||
/>
|
||||
{errors.telefono && (
|
||||
<p className="mt-1 text-xs text-red-700">{errors.telefono.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{unidad && (
|
||||
<div className="border-t border-[#E5E7EB] pt-6">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
Precio de lista
|
||||
</p>
|
||||
<p className="mt-2 text-3xl font-semibold text-[#111827]">
|
||||
{formatoMoneda(precioLista)}
|
||||
</p>
|
||||
</div>
|
||||
<UnitSelector
|
||||
unidades={unidadesDesarrollo}
|
||||
valor={unidad?.codigo ?? ''}
|
||||
onChange={(codigo) =>
|
||||
setUnidad(unidadesDesarrollo.find((u) => u.codigo === codigo) ?? null)
|
||||
}
|
||||
)}
|
||||
|
||||
{unidad && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNueva}
|
||||
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
>
|
||||
Nueva cotización
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{unidad && <UnitInfoCard unidad={unidad} numMeses={numMeses} />}
|
||||
</aside>
|
||||
|
||||
{/* Panel de propuestas */}
|
||||
<section className="min-w-0">
|
||||
{!unidad ? (
|
||||
<div className="flex h-64 items-center justify-center rounded-xl border border-dashed border-[#D1D5DB] bg-white">
|
||||
<p className="text-sm text-[#64748B]">
|
||||
Selecciona una unidad para ver las propuestas comerciales.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-8">
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{propuestas.map((p, i) => (
|
||||
<ProposalCard
|
||||
key={p.tipo}
|
||||
numero={i + 1}
|
||||
titulo={p.titulo}
|
||||
propuesta={p}
|
||||
onDescuentoChange={(v) => setDescuento(params.propuestas[i].claveDescuento, v)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<ComparadorPropuestas
|
||||
precioLista={precioLista}
|
||||
apartado={apartadoEfectivo}
|
||||
propuestas={propuestas}
|
||||
titulos={propuestas.map((p) => p.titulo)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!params.precioUnico && (
|
||||
<div>
|
||||
<span className={etiquetaCampo}>Cocina y closet</span>
|
||||
<div className="inline-flex overflow-hidden rounded-lg border border-[#D1D5DB]" role="group">
|
||||
{(['SI', 'NO'] as const).map((opcion) => {
|
||||
const activo = (opcion === 'SI') === acabados
|
||||
return (
|
||||
<div className="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-sm">
|
||||
<div className="flex flex-wrap items-center justify-end gap-4">
|
||||
<div>
|
||||
<label htmlFor="propuesta-elegida" className={etiquetaCampo}>
|
||||
Propuesta elegida
|
||||
</label>
|
||||
<select
|
||||
id="propuesta-elegida"
|
||||
value={Math.min(propuestaIndex, propuestas.length - 1)}
|
||||
onChange={(e) => setPropuestaIndex(Number(e.target.value))}
|
||||
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:ring-2 focus:ring-accent/20 focus:outline-none"
|
||||
>
|
||||
{propuestas.map((p, i) => (
|
||||
<option key={p.tipo} value={i}>
|
||||
#{i + 1} {p.titulo}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
key={opcion}
|
||||
type="button"
|
||||
aria-pressed={activo}
|
||||
onClick={() => setAcabados(opcion === 'SI')}
|
||||
className={`px-5 py-2.5 text-xs font-medium transition-colors duration-100 ${
|
||||
activo
|
||||
? 'bg-accent text-white'
|
||||
: 'bg-white text-[#4B5563] hover:bg-[#F9FAFB]'
|
||||
}`}
|
||||
onClick={handleCompartir}
|
||||
disabled={!puedeGenerarPdf}
|
||||
className="rounded-lg border border-accent bg-white px-5 py-2.5 text-sm font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-accent hover:text-white disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{opcion}
|
||||
Compartir
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerarPdf}
|
||||
disabled={!puedeGenerarPdf}
|
||||
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 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{guardando ? 'Guardando…' : 'Guardar y generar PDF'}
|
||||
</button>
|
||||
</div>
|
||||
{aviso && <p className="mt-2 text-right text-xs text-[#64748B]">{aviso}</p>}
|
||||
{!puedeGenerarPdf && !guardando && (
|
||||
<p className="mt-2 text-right text-xs text-[#64748B]">
|
||||
Selecciona una unidad para guardar y generar el PDF.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label htmlFor="apartado" className={etiquetaCampo}>
|
||||
Monto de apartado (MXN)
|
||||
</label>
|
||||
<input
|
||||
id="apartado"
|
||||
type="number"
|
||||
min={0}
|
||||
step={1000}
|
||||
value={apartado}
|
||||
onChange={(e) => {
|
||||
const v = Number(e.target.value)
|
||||
setApartado(Number.isFinite(v) && v >= 0 ? v : 0)
|
||||
}}
|
||||
className={errorApartado ? campoInputError : campoInput}
|
||||
/>
|
||||
{errorApartado ? (
|
||||
<p className="mt-1 text-xs text-red-700">{errorApartado}</p>
|
||||
) : (
|
||||
<p className="mt-1 text-xs text-[#64748B]">{formatoMoneda(apartado)}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="nombre" className={etiquetaCampo}>
|
||||
Nombre del cliente *
|
||||
</label>
|
||||
<input
|
||||
id="nombre"
|
||||
type="text"
|
||||
className={errors.nombre ? campoInputError : campoInput}
|
||||
{...register('nombre')}
|
||||
/>
|
||||
{errors.nombre && <p className="mt-1 text-xs text-red-700">{errors.nombre.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="email" className={etiquetaCampo}>
|
||||
Correo electrónico (opcional)
|
||||
</label>
|
||||
<input
|
||||
id="email"
|
||||
type="email"
|
||||
className={errors.email ? campoInputError : campoInput}
|
||||
{...register('email')}
|
||||
/>
|
||||
{errors.email && <p className="mt-1 text-xs text-red-700">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="telefono" className={etiquetaCampo}>
|
||||
Teléfono (opcional)
|
||||
</label>
|
||||
<input
|
||||
id="telefono"
|
||||
type="tel"
|
||||
className={errors.telefono ? campoInputError : campoInput}
|
||||
{...register('telefono')}
|
||||
/>
|
||||
{errors.telefono && (
|
||||
<p className="mt-1 text-xs text-red-700">{errors.telefono.message}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{unidad && (
|
||||
<div className="border-t border-[#E5E7EB] pt-8">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
Precio de lista
|
||||
</p>
|
||||
<p className="mt-2 text-3xl font-semibold text-[#111827]">
|
||||
{formatoMoneda(precioLista)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{unidad && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNueva}
|
||||
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||
>
|
||||
Nueva cotización
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{unidad && <UnitInfoCard unidad={unidad} numMeses={numMeses} />}
|
||||
</aside>
|
||||
|
||||
{/* Panel de propuestas */}
|
||||
<section>
|
||||
{/* Secciones de ancho completo */}
|
||||
<div className="mt-10 space-y-8">
|
||||
<MisCotizaciones
|
||||
cotizaciones={misCotizaciones}
|
||||
cargando={cargandoMias}
|
||||
error={errorMias}
|
||||
onRetomar={handleRetomar}
|
||||
/>
|
||||
|
||||
{!unidad ? (
|
||||
<div className="mt-16 flex h-64 items-center justify-center rounded-xl border border-dashed border-[#D1D5DB] bg-white">
|
||||
<p className="text-sm text-[#64748B]">
|
||||
Selecciona una unidad para ver las propuestas comerciales.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-16">
|
||||
<div className="grid gap-8 md:grid-cols-2">
|
||||
{propuestas.map((p, i) => (
|
||||
<ProposalCard
|
||||
key={p.tipo}
|
||||
numero={i + 1}
|
||||
titulo={p.titulo}
|
||||
propuesta={p}
|
||||
onDescuentoChange={(v) => setDescuento(params.propuestas[i].claveDescuento, v)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<ComparadorPropuestas
|
||||
precioLista={precioLista}
|
||||
apartado={apartadoEfectivo}
|
||||
propuestas={propuestas}
|
||||
titulos={propuestas.map((p) => p.titulo)}
|
||||
/>
|
||||
<div className="mt-16 flex flex-wrap items-center justify-end gap-4">
|
||||
<div>
|
||||
<label htmlFor="propuesta-elegida" className={etiquetaCampo}>
|
||||
Propuesta elegida
|
||||
</label>
|
||||
<select
|
||||
id="propuesta-elegida"
|
||||
value={Math.min(propuestaIndex, propuestas.length - 1)}
|
||||
onChange={(e) => setPropuestaIndex(Number(e.target.value))}
|
||||
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:ring-2 focus:ring-accent/20 focus:outline-none"
|
||||
>
|
||||
{propuestas.map((p, i) => (
|
||||
<option key={p.tipo} value={i}>
|
||||
#{i + 1} {p.titulo}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleCompartir}
|
||||
disabled={!puedeGenerarPdf}
|
||||
className="rounded-lg border border-accent bg-white px-5 py-2.5 text-sm font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-accent hover:text-white disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
Compartir
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerarPdf}
|
||||
disabled={!puedeGenerarPdf}
|
||||
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 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
{guardando ? 'Guardando…' : 'Guardar y generar PDF'}
|
||||
</button>
|
||||
</div>
|
||||
{aviso && <p className="mt-2 text-right text-xs text-[#64748B]">{aviso}</p>}
|
||||
{!puedeGenerarPdf && !guardando && (
|
||||
<p className="mt-2 text-right text-xs text-[#64748B]">
|
||||
Selecciona una unidad para guardar y generar el PDF.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ComparadorUnidades
|
||||
unidades={unidades}
|
||||
tipo={propuestaActiva?.tipo ?? 'CREDITO_BANCARIO'}
|
||||
apartado={apartadoEfectivo}
|
||||
acabados={acabados}
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<ModalCliente
|
||||
|
||||
Reference in New Issue
Block a user