style: reskin NovaSpark en las 7 apps
Del look editorial (verde #1B4332, esquinas a 0, botones en mayusculas) al sistema NovaSpark: primario azul #2563EB, detalle naranja #F97316, fondo #F8FAFC con tarjetas blancas rounded-xl + shadow-sm, chips rounded-full semanticos, focus ring azul, transiciones 100-200ms. Incluye headers de tabla en los PDFs (cotizador y estado de cuenta).
This commit is contained in:
@@ -17,17 +17,17 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white text-neutral-900">
|
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
|
||||||
<div className="border-b border-neutral-200">
|
<div className="border-b border-[#E5E7EB] bg-white">
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="text-xs font-medium text-[#64748B]">
|
||||||
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setModalPassword(true)}
|
onClick={() => setModalPassword(true)}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:text-neutral-900"
|
className="rounded-md px-2 py-1.5 text-xs font-medium text-[#64748B] transition-all duration-100 ease-out hover:text-[#111827]"
|
||||||
>
|
>
|
||||||
Contraseña
|
Contraseña
|
||||||
</button>
|
</button>
|
||||||
@@ -37,7 +37,7 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
logout()
|
logout()
|
||||||
setSesion(null)
|
setSesion(null)
|
||||||
}}
|
}}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:text-accent-hover"
|
className="rounded-md px-2 py-1.5 text-xs font-medium text-accent transition-all duration-100 ease-out hover:text-accent-hover"
|
||||||
>
|
>
|
||||||
Salir
|
Salir
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ function Burbuja({ mensaje }: { mensaje: Mensaje }) {
|
|||||||
<div className={`flex ${esUsuario ? 'justify-end' : 'justify-start'}`}>
|
<div className={`flex ${esUsuario ? 'justify-end' : 'justify-start'}`}>
|
||||||
<div className={`max-w-[80%] ${esUsuario ? 'text-right' : 'text-left'}`}>
|
<div className={`max-w-[80%] ${esUsuario ? 'text-right' : 'text-left'}`}>
|
||||||
<div
|
<div
|
||||||
className={`inline-block whitespace-pre-wrap px-4 py-3 text-sm leading-relaxed ${
|
className={`inline-block whitespace-pre-wrap rounded-2xl px-4 py-3 text-sm leading-relaxed ${
|
||||||
esUsuario ? 'bg-accent text-white' : 'bg-neutral-100 text-neutral-900'
|
esUsuario
|
||||||
|
? 'rounded-br-md bg-accent text-white shadow-sm'
|
||||||
|
: 'rounded-bl-md border border-[#E5E7EB] bg-white text-[#111827] shadow-sm'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{mensaje.contenido}
|
{mensaje.contenido}
|
||||||
@@ -18,7 +20,7 @@ function Burbuja({ mensaje }: { mensaje: Mensaje }) {
|
|||||||
<span
|
<span
|
||||||
key={f.titulo}
|
key={f.titulo}
|
||||||
title={`Relevancia: ${(f.score * 100).toFixed(0)}%`}
|
title={`Relevancia: ${(f.score * 100).toFixed(0)}%`}
|
||||||
className="border border-neutral-300 px-2 py-1 text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-blue-700"
|
||||||
>
|
>
|
||||||
{f.titulo}
|
{f.titulo}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { validarConfirmacion, validarPasswordNueva } from '../lib/password'
|
|||||||
import { cambiarPassword } from '../services/auth'
|
import { cambiarPassword } from '../services/auth'
|
||||||
|
|
||||||
const CLASE_INPUT =
|
const CLASE_INPUT =
|
||||||
'w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none'
|
'w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
|
||||||
const CLASE_LABEL = 'mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500'
|
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
||||||
@@ -45,19 +45,19 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Cambiar contraseña"
|
aria-label="Cambiar contraseña"
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-sm border border-neutral-200 bg-white p-6">
|
<div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl">
|
||||||
<h2 className="mb-6 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h2 className="mb-6 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Cambiar contraseña
|
Cambiar contraseña
|
||||||
</h2>
|
</h2>
|
||||||
{exito ? (
|
{exito ? (
|
||||||
<>
|
<>
|
||||||
<p className="border border-accent px-4 py-3 text-xs font-bold uppercase tracking-widest text-accent">
|
<p className="rounded-lg bg-green-50 px-4 py-3 text-xs font-medium text-green-700">
|
||||||
Contraseña actualizada
|
Contraseña actualizada
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCerrar}
|
onClick={onCerrar}
|
||||||
className="mt-6 w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover"
|
className="mt-6 w-full rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover"
|
||||||
>
|
>
|
||||||
Cerrar
|
Cerrar
|
||||||
</button>
|
</button>
|
||||||
@@ -92,7 +92,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
required
|
required
|
||||||
className={CLASE_INPUT}
|
className={CLASE_INPUT}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 text-xs text-neutral-400">Mínimo 8 caracteres y un número</p>
|
<p className="mt-2 text-xs text-[#94A3B8]">Mínimo 8 caracteres y un número</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="password-confirmar" className={CLASE_LABEL}>
|
<label htmlFor="password-confirmar" className={CLASE_LABEL}>
|
||||||
@@ -111,7 +111,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="rounded-lg bg-red-50 px-4 py-3 text-xs font-medium text-red-700"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@@ -120,14 +120,14 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="flex-1 bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="flex-1 rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Guardando…' : 'Guardar'}
|
{cargando ? 'Guardando…' : 'Guardar'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCerrar}
|
onClick={onCerrar}
|
||||||
className="border border-neutral-300 px-5 py-3 text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:border-neutral-500"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-3 text-sm font-medium text-[#374151] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, type FormEvent } from 'react'
|
import { useState, type FormEvent } from 'react'
|
||||||
import { login, ErrorLogin, type UsuarioSesion } from '../services/auth'
|
import { login, ErrorLogin, type UsuarioSesion } from '../services/auth'
|
||||||
|
|
||||||
/** Pantalla de login StudioBlank: marca FRASA, usuario + contraseña, Enter envía */
|
/** Pantalla de login NovaSpark: marca FRASA, usuario + contraseña, Enter envía */
|
||||||
export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSesion) => void }) {
|
export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSesion) => void }) {
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
@@ -22,11 +22,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-white px-4">
|
<div className="flex min-h-screen items-center justify-center bg-[#F8FAFC] px-4">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm">
|
||||||
<div className="mb-10 text-center">
|
<div className="mb-10 text-center">
|
||||||
<p className="text-lg font-bold tracking-widest">FRASA</p>
|
<p className="text-lg font-bold tracking-wide">
|
||||||
<p className="mt-2 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
FRASA<span className="text-[#F97316]">.</span>
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Acceso al ecosistema
|
Acceso al ecosistema
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +36,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="login-usuario"
|
htmlFor="login-usuario"
|
||||||
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Usuario
|
Usuario
|
||||||
</label>
|
</label>
|
||||||
@@ -46,13 +48,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="login-password"
|
htmlFor="login-password"
|
||||||
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Contraseña
|
Contraseña
|
||||||
</label>
|
</label>
|
||||||
@@ -63,13 +65,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="rounded-lg bg-red-50 px-4 py-3 text-xs font-medium text-red-700"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@@ -77,7 +79,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="w-full rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Entrando…' : 'Entrar'}
|
{cargando ? 'Entrando…' : 'Entrar'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,22 +2,17 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
--color-accent: #1B4332;
|
--color-accent: #2563EB;
|
||||||
--radius-sm: 0;
|
--color-accent-hover: #1D4ED8;
|
||||||
--radius-md: 0;
|
|
||||||
--radius-lg: 0;
|
|
||||||
--radius-xl: 0;
|
|
||||||
--radius-2xl: 0;
|
|
||||||
--radius-3xl: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply bg-white text-neutral-900 antialiased;
|
@apply bg-[#F8FAFC] text-[#111827] antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
*:focus-visible {
|
*:focus-visible {
|
||||||
outline: 2px solid #1B4332;
|
outline: none;
|
||||||
outline-offset: 2px;
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,17 +69,18 @@ export default function ChatPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col bg-white text-neutral-900">
|
<div className="flex min-h-screen flex-col bg-[#F8FAFC] text-[#111827]">
|
||||||
<header className="border-b border-neutral-200">
|
<header className="border-b border-[#E5E7EB] bg-white">
|
||||||
<div className="mx-auto flex w-full max-w-3xl items-center justify-between px-4 py-4">
|
<div className="mx-auto flex w-full max-w-3xl items-center justify-between px-4 py-4">
|
||||||
<span className="text-sm tracking-widest">
|
<span className="text-sm tracking-wide">
|
||||||
<span className="font-bold">FRASA</span>{' '}
|
<span className="font-bold">FRASA</span>
|
||||||
<span className="font-light text-neutral-500">ASISTENTE IA</span>
|
<span className="text-[#F97316]"> · </span>
|
||||||
|
<span className="font-light text-[#64748B]">ASISTENTE IA</span>
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={nuevaConversacion}
|
onClick={nuevaConversacion}
|
||||||
className="border border-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:bg-accent hover:text-white"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-xs font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Nueva conversación
|
Nueva conversación
|
||||||
</button>
|
</button>
|
||||||
@@ -88,19 +89,23 @@ export default function ChatPage() {
|
|||||||
|
|
||||||
<main className="mx-auto flex w-full max-w-3xl flex-1 flex-col px-4 py-8">
|
<main className="mx-auto flex w-full max-w-3xl flex-1 flex-col px-4 py-8">
|
||||||
{sinConexion && (
|
{sinConexion && (
|
||||||
<p className="mb-6 text-xs text-amber-700">
|
<p className="mb-6 rounded-lg bg-amber-50 px-4 py-3 text-xs font-medium text-amber-700">
|
||||||
Sin conexión al API del asistente. Revisa que el servicio esté disponible e inténtalo de
|
Sin conexión al API del asistente. Revisa que el servicio esté disponible e inténtalo de
|
||||||
nuevo.
|
nuevo.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{error && <p className="mb-6 text-xs text-red-700">{error}</p>}
|
{error && (
|
||||||
|
<p className="mb-6 rounded-lg bg-red-50 px-4 py-3 text-xs font-medium text-red-700">
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{mensajes.length === 0 && !escribiendo ? (
|
{mensajes.length === 0 && !escribiendo ? (
|
||||||
<div className="flex flex-1 flex-col items-center justify-center gap-4 text-center">
|
<div className="flex flex-1 flex-col items-center justify-center gap-4 text-center">
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Asistente interno
|
Asistente interno
|
||||||
</p>
|
</p>
|
||||||
<p className="max-w-md text-2xl font-light text-neutral-900">
|
<p className="max-w-md text-2xl font-light text-[#111827]">
|
||||||
Pregunta sobre el catálogo DIAGONAL, las propuestas del cotizador, el proceso de venta
|
Pregunta sobre el catálogo DIAGONAL, las propuestas del cotizador, el proceso de venta
|
||||||
o las políticas de Frasa.
|
o las políticas de Frasa.
|
||||||
</p>
|
</p>
|
||||||
@@ -112,7 +117,7 @@ export default function ChatPage() {
|
|||||||
))}
|
))}
|
||||||
{escribiendo && (
|
{escribiendo && (
|
||||||
<div className="flex justify-start">
|
<div className="flex justify-start">
|
||||||
<p className="bg-neutral-100 px-4 py-3 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<p className="rounded-full bg-white px-4 py-2 text-xs font-medium text-[#64748B] shadow-sm">
|
||||||
Escribiendo…
|
Escribiendo…
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -122,7 +127,7 @@ export default function ChatPage() {
|
|||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="border-t border-neutral-200">
|
<footer className="border-t border-[#E5E7EB] bg-white">
|
||||||
<form
|
<form
|
||||||
className="mx-auto flex w-full max-w-3xl items-stretch gap-4 px-4 py-4"
|
className="mx-auto flex w-full max-w-3xl items-stretch gap-4 px-4 py-4"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -140,12 +145,12 @@ export default function ChatPage() {
|
|||||||
onChange={(e) => setEntrada(e.target.value)}
|
onChange={(e) => setEntrada(e.target.value)}
|
||||||
placeholder="Escribe tu pregunta…"
|
placeholder="Escribe tu pregunta…"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="flex-1 border border-neutral-300 px-4 py-3 text-sm placeholder:text-neutral-400"
|
className="flex-1 rounded-lg border border-[#D1D5DB] bg-white px-4 py-3 text-sm placeholder:text-[#94A3B8] transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={escribiendo || entrada.trim().length === 0}
|
disabled={escribiendo || entrada.trim().length === 0}
|
||||||
className="bg-accent px-6 py-3 text-xs font-bold uppercase tracking-widest text-white transition-opacity duration-150 hover:opacity-90 disabled:opacity-40"
|
className="rounded-lg bg-accent px-6 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-40"
|
||||||
>
|
>
|
||||||
Enviar
|
Enviar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ interface Props {
|
|||||||
const emailSchema = z.string().trim().email()
|
const emailSchema = z.string().trim().email()
|
||||||
|
|
||||||
const inputClase =
|
const inputClase =
|
||||||
'w-full border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none'
|
'w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
|
||||||
|
|
||||||
const labelClase = 'mb-1 block text-xs font-bold uppercase tracking-widest text-neutral-500'
|
const labelClase = 'mb-1 block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
interface Errores {
|
interface Errores {
|
||||||
nombre?: string
|
nombre?: string
|
||||||
@@ -141,7 +141,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
|
|||||||
<div className="max-w-md">
|
<div className="max-w-md">
|
||||||
<p
|
<p
|
||||||
role="status"
|
role="status"
|
||||||
className="border border-accent bg-white px-4 py-3 text-xs font-bold uppercase tracking-widest text-accent"
|
className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-700"
|
||||||
>
|
>
|
||||||
Registrado en afluencia — se asignará por round-robin
|
Registrado en afluencia — se asignará por round-robin
|
||||||
</p>
|
</p>
|
||||||
@@ -149,21 +149,21 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onPasarAAltaCliente(leadCreado)}
|
onClick={() => onPasarAAltaCliente(leadCreado)}
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-neutral-900"
|
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"
|
||||||
>
|
>
|
||||||
Pasar a Alta de Cliente
|
Pasar a Alta de Cliente
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={capturarOtro}
|
onClick={capturarOtro}
|
||||||
className="border border-accent bg-white px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:bg-accent hover:text-white"
|
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"
|
||||||
>
|
>
|
||||||
Capturar otro
|
Capturar otro
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onVolver}
|
onClick={onVolver}
|
||||||
className="border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="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]"
|
||||||
>
|
>
|
||||||
Volver
|
Volver
|
||||||
</button>
|
</button>
|
||||||
@@ -239,7 +239,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
|
|||||||
value={form.modelo}
|
value={form.modelo}
|
||||||
onChange={cambiar('modelo')}
|
onChange={cambiar('modelo')}
|
||||||
disabled={form.desarrollo === ''}
|
disabled={form.desarrollo === ''}
|
||||||
className={`${inputClase} disabled:bg-neutral-100 disabled:text-neutral-400`}
|
className={`${inputClase} disabled:bg-[#F1F5F9] disabled:text-[#94A3B8]`}
|
||||||
>
|
>
|
||||||
<option value="">
|
<option value="">
|
||||||
{form.desarrollo === '' ? 'Selecciona primero el desarrollo' : '— Selecciona —'}
|
{form.desarrollo === '' ? 'Selecciona primero el desarrollo' : '— Selecciona —'}
|
||||||
@@ -424,7 +424,7 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{aviso && (
|
{aviso && (
|
||||||
<p role="alert" className="mt-6 border border-red-300 bg-red-50 px-4 py-3 text-xs text-red-700">
|
<p role="alert" className="mt-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-700">
|
||||||
{aviso}
|
{aviso}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -433,14 +433,14 @@ export default function AltaAfluenciaForm({ onVolver, onPasarAAltaCliente }: Pro
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={enviando}
|
disabled={enviando}
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-neutral-900 disabled:opacity-50"
|
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:opacity-50"
|
||||||
>
|
>
|
||||||
{enviando ? 'Registrando…' : 'Registrar en afluencia'}
|
{enviando ? 'Registrando…' : 'Registrar en afluencia'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onVolver}
|
onClick={onVolver}
|
||||||
className="border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="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]"
|
||||||
>
|
>
|
||||||
Volver
|
Volver
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white text-neutral-900">
|
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
|
||||||
<div className="border-b border-neutral-200">
|
<div className="border-b border-[#E5E7EB] bg-white">
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="text-xs font-medium text-[#64748B]">
|
||||||
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setModalPassword(true)}
|
onClick={() => setModalPassword(true)}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:text-neutral-900"
|
className="rounded-lg px-2 py-1 text-xs font-medium text-[#64748B] transition-colors duration-100 hover:text-[#111827]"
|
||||||
>
|
>
|
||||||
Contraseña
|
Contraseña
|
||||||
</button>
|
</button>
|
||||||
@@ -37,7 +37,7 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
logout()
|
logout()
|
||||||
setSesion(null)
|
setSesion(null)
|
||||||
}}
|
}}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:text-accent-hover"
|
className="rounded-lg px-2 py-1 text-xs font-medium text-accent transition-colors duration-100 hover:text-accent-hover"
|
||||||
>
|
>
|
||||||
Salir
|
Salir
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -84,53 +84,53 @@ export default function BotonOcrIne({ archivo, onAplicar }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border border-neutral-200 bg-white px-4 py-3">
|
<div className="rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm">
|
||||||
<div className="flex items-center justify-between gap-4">
|
<div className="flex items-center justify-between gap-4">
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">Lectura automática (OCR)</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">Lectura automática (OCR)</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void leer()}
|
onClick={() => void leer()}
|
||||||
disabled={estado.fase === 'procesando'}
|
disabled={estado.fase === 'procesando'}
|
||||||
className="shrink-0 border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white disabled:opacity-40"
|
className="shrink-0 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] disabled:opacity-40"
|
||||||
>
|
>
|
||||||
{estado.fase === 'procesando' ? `Leyendo… ${estado.progreso}%` : 'Leer INE con OCR'}
|
{estado.fase === 'procesando' ? `Leyendo… ${estado.progreso}%` : 'Leer INE con OCR'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{estado.fase === 'procesando' && (
|
{estado.fase === 'procesando' && (
|
||||||
<p className="mt-3 text-xs font-light text-neutral-500" role="status">
|
<p className="mt-3 text-xs text-[#64748B]" role="status">
|
||||||
Reconociendo texto de la credencial… {estado.progreso}%
|
Reconociendo texto de la credencial… {estado.progreso}%
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{estado.fase === 'error' && (
|
{estado.fase === 'error' && (
|
||||||
<p className="mt-3 border border-red-400 px-3 py-2 text-xs text-red-700" role="alert">
|
<p className="mt-3 rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-700" role="alert">
|
||||||
{estado.mensaje}
|
{estado.mensaje}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{estado.fase === 'listo' && (
|
{estado.fase === 'listo' && (
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<p className="text-xs font-light text-neutral-500">
|
<p className="text-xs text-[#64748B]">
|
||||||
Sugerencias detectadas — nada se sobreescribe sin tu confirmación:
|
Sugerencias detectadas — nada se sobreescribe sin tu confirmación:
|
||||||
</p>
|
</p>
|
||||||
<ul className="mt-2 space-y-2">
|
<ul className="mt-2 space-y-2">
|
||||||
{aVisibles(estado.sugerencias).map((s) => (
|
{aVisibles(estado.sugerencias).map((s) => (
|
||||||
<li key={s.clave} className="flex items-baseline justify-between gap-4 text-sm">
|
<li key={s.clave} className="flex items-baseline justify-between gap-4 text-sm">
|
||||||
<span className="font-light text-neutral-900">
|
<span className="text-[#111827]">
|
||||||
<span className="mr-2 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="mr-2 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
{s.etiqueta}
|
{s.etiqueta}
|
||||||
</span>
|
</span>
|
||||||
{s.valor}
|
{s.valor}
|
||||||
</span>
|
</span>
|
||||||
{s.aplicable &&
|
{s.aplicable &&
|
||||||
(aplicados.includes(s.clave) ? (
|
(aplicados.includes(s.clave) ? (
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-accent">✓</span>
|
<span className="rounded-full bg-green-50 px-2.5 py-0.5 text-xs font-medium text-green-700">✓</span>
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => aplicar(s)}
|
onClick={() => aplicar(s)}
|
||||||
className="shrink-0 border border-neutral-900 bg-transparent px-3 py-1.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="shrink-0 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]"
|
||||||
>
|
>
|
||||||
Aplicar
|
Aplicar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export default function BusquedaLead({ onSeleccion }: Props) {
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="busqueda-lead"
|
htmlFor="busqueda-lead"
|
||||||
className="mb-1 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-1 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Buscar lead (nombre, teléfono o email)
|
Buscar lead (nombre, teléfono o email)
|
||||||
</label>
|
</label>
|
||||||
@@ -55,30 +55,30 @@ export default function BusquedaLead({ onSeleccion }: Props) {
|
|||||||
value={q}
|
value={q}
|
||||||
onChange={(e) => setQ(e.target.value)}
|
onChange={(e) => setQ(e.target.value)}
|
||||||
placeholder="Escribe al menos 2 caracteres…"
|
placeholder="Escribe al menos 2 caracteres…"
|
||||||
className="w-full max-w-md border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 focus:border-neutral-900"
|
className="w-full max-w-md rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm text-[#111827] transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{buscando && <p className="mt-3 text-xs font-light text-neutral-500">Buscando…</p>}
|
{buscando && <p className="mt-3 text-xs text-[#64748B]">Buscando…</p>}
|
||||||
{error && (
|
{error && (
|
||||||
<p role="alert" className="mt-3 text-xs text-red-700">
|
<p role="alert" className="mt-3 text-xs text-red-700">
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{!buscando && !error && resultados !== null && resultados.length === 0 && (
|
{!buscando && !error && resultados !== null && resultados.length === 0 && (
|
||||||
<p className="mt-3 text-xs font-light text-neutral-500">Sin leads que coincidan.</p>
|
<p className="mt-3 text-xs text-[#64748B]">Sin leads que coincidan.</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{resultados !== null && resultados.length > 0 && (
|
{resultados !== null && resultados.length > 0 && (
|
||||||
<ul className="mt-3 max-w-md divide-y divide-neutral-200 border border-neutral-200 bg-white">
|
<ul className="mt-3 max-w-md divide-y divide-[#E5E7EB] rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
|
||||||
{resultados.map((lead) => (
|
{resultados.map((lead) => (
|
||||||
<li key={lead.id}>
|
<li key={lead.id}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onSeleccion(lead)}
|
onClick={() => onSeleccion(lead)}
|
||||||
className="flex w-full flex-wrap items-baseline justify-between gap-2 px-4 py-3 text-left transition-colors duration-150 hover:bg-neutral-50"
|
className="flex w-full flex-wrap items-baseline justify-between gap-2 px-4 py-3 text-left transition-colors duration-100 hover:bg-[#F8FAFC]"
|
||||||
>
|
>
|
||||||
<span className="text-sm text-neutral-900">{lead.nombre}</span>
|
<span className="text-sm text-[#111827]">{lead.nombre}</span>
|
||||||
<span className="text-xs font-light text-neutral-500">
|
<span className="text-xs text-[#64748B]">
|
||||||
{lead.telefono || 'sin teléfono'}
|
{lead.telefono || 'sin teléfono'}
|
||||||
{lead.estado ? ` · ${lead.estado}` : ''}
|
{lead.estado ? ` · ${lead.estado}` : ''}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { validarConfirmacion, validarPasswordNueva } from '../lib/password'
|
|||||||
import { cambiarPassword } from '../services/auth'
|
import { cambiarPassword } from '../services/auth'
|
||||||
|
|
||||||
const CLASE_INPUT =
|
const CLASE_INPUT =
|
||||||
'w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none'
|
'w-full rounded-lg border border-[#D1D5DB] bg-white px-4 py-3 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
|
||||||
const CLASE_LABEL = 'mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500'
|
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
||||||
@@ -45,19 +45,19 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Cambiar contraseña"
|
aria-label="Cambiar contraseña"
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-sm border border-neutral-200 bg-white p-6">
|
<div className="w-full max-w-sm rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-xl">
|
||||||
<h2 className="mb-6 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h2 className="mb-6 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Cambiar contraseña
|
Cambiar contraseña
|
||||||
</h2>
|
</h2>
|
||||||
{exito ? (
|
{exito ? (
|
||||||
<>
|
<>
|
||||||
<p className="border border-accent px-4 py-3 text-xs font-bold uppercase tracking-widest text-accent">
|
<p className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-700">
|
||||||
Contraseña actualizada
|
Contraseña actualizada
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCerrar}
|
onClick={onCerrar}
|
||||||
className="mt-6 w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover"
|
className="mt-6 w-full rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover"
|
||||||
>
|
>
|
||||||
Cerrar
|
Cerrar
|
||||||
</button>
|
</button>
|
||||||
@@ -92,7 +92,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
required
|
required
|
||||||
className={CLASE_INPUT}
|
className={CLASE_INPUT}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 text-xs text-neutral-400">Mínimo 8 caracteres y un número</p>
|
<p className="mt-2 text-xs text-[#64748B]">Mínimo 8 caracteres y un número</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="password-confirmar" className={CLASE_LABEL}>
|
<label htmlFor="password-confirmar" className={CLASE_LABEL}>
|
||||||
@@ -111,7 +111,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-700"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@@ -120,14 +120,14 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="flex-1 bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="flex-1 rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Guardando…' : 'Guardar'}
|
{cargando ? 'Guardando…' : 'Guardar'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCerrar}
|
onClick={onCerrar}
|
||||||
className="border border-neutral-300 px-5 py-3 text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:border-neutral-500"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-3 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ export interface CampoBaseProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const inputClase =
|
const inputClase =
|
||||||
'w-full border border-neutral-300 bg-white px-3 py-2.5 text-sm uppercase transition-colors duration-150 focus:border-neutral-900 focus:outline-none'
|
'w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm uppercase transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
|
||||||
|
|
||||||
const labelClase = 'mb-1 block text-xs font-bold uppercase tracking-widest text-neutral-500'
|
const labelClase = 'mb-1 block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
function Etiqueta({ etiqueta, requerido }: { etiqueta: string; requerido?: boolean }) {
|
function Etiqueta({ etiqueta, requerido }: { etiqueta: string; requerido?: boolean }) {
|
||||||
return (
|
return (
|
||||||
@@ -52,7 +52,7 @@ export function CampoTexto({ nombre, etiqueta, tipo = 'text', placeholder, reque
|
|||||||
type={tipo}
|
type={tipo}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
readOnly={soloLectura}
|
readOnly={soloLectura}
|
||||||
className={`${claseInput(nombre, errors)} ${soloLectura ? 'bg-neutral-50' : ''}`}
|
className={`${claseInput(nombre, errors)} ${soloLectura ? 'bg-[#F8FAFC]' : ''}`}
|
||||||
{...register(nombre)}
|
{...register(nombre)}
|
||||||
/>
|
/>
|
||||||
<MensajeError nombre={nombre} />
|
<MensajeError nombre={nombre} />
|
||||||
|
|||||||
@@ -12,19 +12,19 @@ export default function ChecklistExpediente({ documentos, firmaDataUrl }: Props)
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
aria-label="Checklist del expediente"
|
aria-label="Checklist del expediente"
|
||||||
className="border border-neutral-200 bg-white px-4 py-3"
|
className="rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm"
|
||||||
>
|
>
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Expediente: {estado.completos}/{estado.total}
|
Expediente: {estado.completos}/{estado.total}
|
||||||
</p>
|
</p>
|
||||||
<ul className="mt-3 space-y-2">
|
<ul className="mt-3 space-y-2">
|
||||||
{estado.items.map((item) => (
|
{estado.items.map((item) => (
|
||||||
<li key={item.clave} className="flex items-baseline justify-between gap-4 text-sm">
|
<li key={item.clave} className="flex items-baseline justify-between gap-4 text-sm">
|
||||||
<span className="font-light text-neutral-900">{item.etiqueta}</span>
|
<span className="text-[#111827]">{item.etiqueta}</span>
|
||||||
{item.completo ? (
|
{item.completo ? (
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-accent">✓</span>
|
<span className="rounded-full bg-green-50 px-2.5 py-0.5 text-xs font-medium text-green-700">✓</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs uppercase tracking-widest text-neutral-400">Pendiente</span>
|
<span className="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">Pendiente</span>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -25,14 +25,14 @@ function Miniatura({ documento, url }: { documento: DocumentoCargado; url: strin
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
title={`Abrir ${documento.nombre} en una pestaña nueva`}
|
title={`Abrir ${documento.nombre} en una pestaña nueva`}
|
||||||
aria-label={`Abrir ${documento.nombre} en una pestaña nueva`}
|
aria-label={`Abrir ${documento.nombre} en una pestaña nueva`}
|
||||||
className="block h-20 w-20 shrink-0 border border-neutral-200 bg-neutral-50"
|
className="block h-20 w-20 shrink-0 overflow-hidden rounded-lg border border-[#E5E7EB] bg-[#F8FAFC]"
|
||||||
>
|
>
|
||||||
{esImagen ? (
|
{esImagen ? (
|
||||||
<img src={url} alt={`Vista previa de ${documento.nombre}`} className="h-full w-full object-cover" />
|
<img src={url} alt={`Vista previa de ${documento.nombre}`} className="h-full w-full object-cover" />
|
||||||
) : (
|
) : (
|
||||||
<span className="flex h-full w-full flex-col items-center justify-center gap-1">
|
<span className="flex h-full w-full flex-col items-center justify-center gap-1">
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-neutral-900">PDF</span>
|
<span className="text-xs font-semibold text-[#111827]">PDF</span>
|
||||||
<span className="max-w-full truncate px-1 text-[10px] font-light text-neutral-500">{documento.nombre}</span>
|
<span className="max-w-full truncate px-1 text-[10px] text-[#64748B]">{documento.nombre}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</a>
|
</a>
|
||||||
@@ -76,12 +76,12 @@ export default function FileUploader({ slot, etiqueta, documento, onAgregar, onQ
|
|||||||
|
|
||||||
if (documento) {
|
if (documento) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-4 border border-neutral-200 bg-white px-4 py-3">
|
<div className="flex items-center justify-between gap-4 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm">
|
||||||
<div className="flex min-w-0 items-center gap-4">
|
<div className="flex min-w-0 items-center gap-4">
|
||||||
{urlObjeto && <Miniatura documento={documento} url={urlObjeto} />}
|
{urlObjeto && <Miniatura documento={documento} url={urlObjeto} />}
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">{etiqueta}</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">{etiqueta}</p>
|
||||||
<p className="mt-1 truncate text-sm text-neutral-900">
|
<p className="mt-1 truncate text-sm text-[#111827]">
|
||||||
{documento.nombre} · {formatoTamano(documento.tamano)}
|
{documento.nombre} · {formatoTamano(documento.tamano)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,7 +89,7 @@ export default function FileUploader({ slot, etiqueta, documento, onAgregar, onQ
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onQuitar(slot)}
|
onClick={() => onQuitar(slot)}
|
||||||
className="shrink-0 border border-red-300 bg-white px-3 py-1.5 text-xs font-bold uppercase tracking-widest text-red-700 transition-colors duration-150 hover:bg-red-50"
|
className="shrink-0 rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 transition-all duration-100 ease-out hover:bg-red-50"
|
||||||
>
|
>
|
||||||
Quitar
|
Quitar
|
||||||
</button>
|
</button>
|
||||||
@@ -101,16 +101,16 @@ export default function FileUploader({ slot, etiqueta, documento, onAgregar, onQ
|
|||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
{...getRootProps()}
|
{...getRootProps()}
|
||||||
className={`cursor-pointer border border-dashed border-neutral-300 bg-white px-4 py-6 text-center transition-colors duration-150 ${
|
className={`cursor-pointer rounded-xl border border-dashed border-[#D1D5DB] bg-white px-4 py-6 text-center transition-colors duration-200 ${
|
||||||
isDragActive ? 'border-neutral-900 bg-neutral-50' : 'hover:border-neutral-500'
|
isDragActive ? 'border-accent bg-accent/5' : 'hover:border-accent'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<input {...getInputProps()} aria-label={`Seleccionar archivo para ${etiqueta}`} />
|
<input {...getInputProps()} aria-label={`Seleccionar archivo para ${etiqueta}`} />
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">{etiqueta}</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">{etiqueta}</p>
|
||||||
<p className="mt-2 text-xs font-light text-neutral-500">Arrastra el archivo aquí · PDF/JPG/PNG · máx 10 MB</p>
|
<p className="mt-2 text-xs text-[#64748B]">Arrastra el archivo aquí · PDF/JPG/PNG · máx 10 MB</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="mt-4 border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="mt-4 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]"
|
||||||
>
|
>
|
||||||
Seleccionar archivo
|
Seleccionar archivo
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -22,11 +22,11 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-white px-4">
|
<div className="flex min-h-screen items-center justify-center bg-[#F8FAFC] px-4">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm">
|
||||||
<div className="mb-10 text-center">
|
<div className="mb-10 text-center">
|
||||||
<p className="text-lg font-bold tracking-widest">FRASA</p>
|
<p className="text-lg font-bold tracking-widest text-[#111827]">FRASA<span className="text-[#F97316]">.</span></p>
|
||||||
<p className="mt-2 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<p className="mt-2 text-xs font-medium uppercase tracking-wider text-[#64748B]">
|
||||||
Acceso al ecosistema
|
Acceso al ecosistema
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +34,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="login-usuario"
|
htmlFor="login-usuario"
|
||||||
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Usuario
|
Usuario
|
||||||
</label>
|
</label>
|
||||||
@@ -46,13 +46,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-4 py-3 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="login-password"
|
htmlFor="login-password"
|
||||||
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Contraseña
|
Contraseña
|
||||||
</label>
|
</label>
|
||||||
@@ -63,13 +63,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-4 py-3 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-700"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@@ -77,7 +77,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="w-full rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Entrando…' : 'Entrar'}
|
{cargando ? 'Entrando…' : 'Entrar'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ interface Props {
|
|||||||
/** Tarjeta de sección del wizard */
|
/** Tarjeta de sección del wizard */
|
||||||
export default function SectionCard({ titulo, descripcion, children }: Props) {
|
export default function SectionCard({ titulo, descripcion, children }: Props) {
|
||||||
return (
|
return (
|
||||||
<section className="border border-neutral-200 bg-white p-6">
|
<section className="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||||
<h3 className="text-xs font-bold uppercase tracking-widest text-neutral-500">{titulo}</h3>
|
<h3 className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">{titulo}</h3>
|
||||||
{descripcion && <p className="mt-2 text-sm font-light text-neutral-500">{descripcion}</p>}
|
{descripcion && <p className="mt-2 text-sm text-[#64748B]">{descripcion}</p>}
|
||||||
<div className="mt-8 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">{children}</div>
|
<div className="mt-8 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">{children}</div>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function SignaturePad({ onFirma }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="border border-neutral-900 bg-white">
|
<div className="rounded-lg border border-[#D1D5DB] bg-white shadow-sm">
|
||||||
<SignatureCanvas
|
<SignatureCanvas
|
||||||
ref={ref}
|
ref={ref}
|
||||||
onEnd={alTerminar}
|
onEnd={alTerminar}
|
||||||
@@ -39,7 +39,7 @@ export default function SignaturePad({ onFirma }: Props) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={limpiar}
|
onClick={limpiar}
|
||||||
className="mt-4 border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="mt-4 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]"
|
||||||
>
|
>
|
||||||
Limpiar firma
|
Limpiar firma
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ interface Props {
|
|||||||
export default function Stepper({ pasos, pasoActual, onIrAPaso }: Props) {
|
export default function Stepper({ pasos, pasoActual, onIrAPaso }: Props) {
|
||||||
return (
|
return (
|
||||||
<nav aria-label="Progreso">
|
<nav aria-label="Progreso">
|
||||||
<ol className="flex flex-wrap items-baseline gap-x-6 gap-y-3 border-b border-neutral-200 pb-4">
|
<ol className="flex flex-wrap items-baseline gap-x-6 gap-y-3 border-b border-[#E5E7EB] pb-4">
|
||||||
{pasos.map((nombre, i) => {
|
{pasos.map((nombre, i) => {
|
||||||
const activo = i === pasoActual
|
const activo = i === pasoActual
|
||||||
const completado = i < pasoActual
|
const completado = i < pasoActual
|
||||||
@@ -19,12 +19,12 @@ export default function Stepper({ pasos, pasoActual, onIrAPaso }: Props) {
|
|||||||
onClick={() => completado && onIrAPaso(i)}
|
onClick={() => completado && onIrAPaso(i)}
|
||||||
disabled={!completado}
|
disabled={!completado}
|
||||||
aria-current={activo ? 'step' : undefined}
|
aria-current={activo ? 'step' : undefined}
|
||||||
className={`text-xs uppercase tracking-widest transition-colors duration-150 ${
|
className={`text-xs font-medium transition-colors duration-150 ${
|
||||||
activo
|
activo
|
||||||
? 'font-bold text-accent'
|
? 'font-semibold text-accent'
|
||||||
: completado
|
: completado
|
||||||
? 'font-medium text-neutral-900 hover:text-accent'
|
? 'text-[#111827] hover:text-accent'
|
||||||
: 'font-light text-neutral-400'
|
: 'text-[#94A3B8]'
|
||||||
}`}
|
}`}
|
||||||
title={nombre}
|
title={nombre}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ export default function CoacreditadoForm({ valores, onAnterior, onSiguiente }: P
|
|||||||
return (
|
return (
|
||||||
<FormProvider {...metodos}>
|
<FormProvider {...metodos}>
|
||||||
<form onSubmit={metodos.handleSubmit(onSiguiente)} noValidate>
|
<form onSubmit={metodos.handleSubmit(onSiguiente)} noValidate>
|
||||||
<label htmlFor="tieneCoacreditado" className="flex items-center gap-3 border border-neutral-200 bg-white p-6">
|
<label htmlFor="tieneCoacreditado" className="flex items-center gap-3 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||||
<input
|
<input
|
||||||
id="tieneCoacreditado"
|
id="tieneCoacreditado"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="h-5 w-5 accent-accent"
|
className="h-5 w-5 accent-accent"
|
||||||
{...metodos.register('tieneCoacreditado')}
|
{...metodos.register('tieneCoacreditado')}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-neutral-900">¿Hay co-acreditado o cónyuge?</span>
|
<span className="text-sm font-medium text-[#111827]">¿Hay co-acreditado o cónyuge?</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{tieneCoacreditado && (
|
{tieneCoacreditado && (
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export default function DatosGeneralesCampos({ prefix, compacto = false }: Props
|
|||||||
<CampoTexto nombre={p('nivelEstudios')} etiqueta="Nivel de estudios" />
|
<CampoTexto nombre={p('nivelEstudios')} etiqueta="Nivel de estudios" />
|
||||||
<CampoTexto nombre={p('curp')} etiqueta="CURP" requerido={!compacto} placeholder="XXXX000000XXXXXXX0" />
|
<CampoTexto nombre={p('curp')} etiqueta="CURP" requerido={!compacto} placeholder="XXXX000000XXXXXXX0" />
|
||||||
{advertenciasCurp.length > 0 && (
|
{advertenciasCurp.length > 0 && (
|
||||||
<div role="alert" className="border border-amber-300 bg-amber-50 px-4 py-3 sm:col-span-2">
|
<div role="alert" className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 sm:col-span-2">
|
||||||
{advertenciasCurp.map((aviso) => (
|
{advertenciasCurp.map((aviso) => (
|
||||||
<p key={aviso} className="text-xs text-amber-700">
|
<p key={aviso} className="text-xs text-amber-700">
|
||||||
{aviso}
|
{aviso}
|
||||||
|
|||||||
@@ -22,25 +22,25 @@ export default function EconomicosCampos({ prefix }: Props) {
|
|||||||
<CampoNumero nombre={p('ingresosMensuales')} etiqueta="Ingresos mensuales" />
|
<CampoNumero nombre={p('ingresosMensuales')} etiqueta="Ingresos mensuales" />
|
||||||
<CampoNumero nombre={p('otrosIngresos')} etiqueta="Otros ingresos" />
|
<CampoNumero nombre={p('otrosIngresos')} etiqueta="Otros ingresos" />
|
||||||
<label htmlFor={`${prefix}totalIngresos`} className="block">
|
<label htmlFor={`${prefix}totalIngresos`} className="block">
|
||||||
<span className="mb-1 block text-xs font-bold uppercase tracking-widest text-neutral-500">Total ingresos</span>
|
<span className="mb-1 block text-xs font-medium text-[#4B5563]">Total ingresos</span>
|
||||||
<input
|
<input
|
||||||
id={`${prefix}totalIngresos`}
|
id={`${prefix}totalIngresos`}
|
||||||
type="text"
|
type="text"
|
||||||
readOnly
|
readOnly
|
||||||
value={formatoMoneda(ingresosMensuales + otrosIngresos)}
|
value={formatoMoneda(ingresosMensuales + otrosIngresos)}
|
||||||
className="w-full border border-neutral-200 bg-neutral-50 px-3 py-2.5 text-sm font-bold text-neutral-900"
|
className="w-full rounded-lg border border-[#E5E7EB] bg-[#F8FAFC] px-3 py-2.5 text-sm font-semibold text-[#111827]"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<CampoNumero nombre={p('gastosLuz')} etiqueta="Gastos luz" />
|
<CampoNumero nombre={p('gastosLuz')} etiqueta="Gastos luz" />
|
||||||
<CampoNumero nombre={p('gastosOtros')} etiqueta="Otros gastos" />
|
<CampoNumero nombre={p('gastosOtros')} etiqueta="Otros gastos" />
|
||||||
<label htmlFor={`${prefix}gastosTotal`} className="block">
|
<label htmlFor={`${prefix}gastosTotal`} className="block">
|
||||||
<span className="mb-1 block text-xs font-bold uppercase tracking-widest text-neutral-500">Gastos total</span>
|
<span className="mb-1 block text-xs font-medium text-[#4B5563]">Gastos total</span>
|
||||||
<input
|
<input
|
||||||
id={`${prefix}gastosTotal`}
|
id={`${prefix}gastosTotal`}
|
||||||
type="text"
|
type="text"
|
||||||
readOnly
|
readOnly
|
||||||
value={formatoMoneda(gastosLuz + gastosOtros)}
|
value={formatoMoneda(gastosLuz + gastosOtros)}
|
||||||
className="w-full border border-neutral-200 bg-neutral-50 px-3 py-2.5 text-sm font-bold text-neutral-900"
|
className="w-full rounded-lg border border-[#E5E7EB] bg-[#F8FAFC] px-3 py-2.5 text-sm font-semibold text-[#111827]"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<CampoTexto nombre={p('dependientes.personas')} etiqueta="Dependientes económicos (personas)" />
|
<CampoTexto nombre={p('dependientes.personas')} etiqueta="Dependientes económicos (personas)" />
|
||||||
|
|||||||
@@ -66,42 +66,42 @@ export default function FirmasForm({ alta, onCambiarFirma, onAnterior }: Props)
|
|||||||
<SectionCard titulo="Resumen del alta">
|
<SectionCard titulo="Resumen del alta">
|
||||||
<dl className="grid grid-cols-2 gap-x-6 gap-y-4 text-sm sm:col-span-2 lg:col-span-3 lg:grid-cols-3">
|
<dl className="grid grid-cols-2 gap-x-6 gap-y-4 text-sm sm:col-span-2 lg:col-span-3 lg:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Cliente</dt>
|
<dt className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">Cliente</dt>
|
||||||
<dd className="mt-1 font-light text-neutral-900">{g.nombreCompleto}</dd>
|
<dd className="mt-1 text-[#111827]">{g.nombreCompleto}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">CURP</dt>
|
<dt className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">CURP</dt>
|
||||||
<dd className="mt-1 font-light text-neutral-900">{g.curp}</dd>
|
<dd className="mt-1 text-[#111827]">{g.curp}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">RFC</dt>
|
<dt className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">RFC</dt>
|
||||||
<dd className="mt-1 font-light text-neutral-900">{g.rfc}</dd>
|
<dd className="mt-1 text-[#111827]">{g.rfc}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Proyecto</dt>
|
<dt className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">Proyecto</dt>
|
||||||
<dd className="mt-1 font-light text-neutral-900">{proyecto || '—'}</dd>
|
<dd className="mt-1 text-[#111827]">{proyecto || '—'}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Ubicación / Modelo</dt>
|
<dt className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">Ubicación / Modelo</dt>
|
||||||
<dd className="mt-1 font-light text-neutral-900">
|
<dd className="mt-1 text-[#111827]">
|
||||||
{v.ubicacion || '—'} {v.modelo}
|
{v.ubicacion || '—'} {v.modelo}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Valor venta</dt>
|
<dt className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">Valor venta</dt>
|
||||||
<dd className="mt-1 font-light text-neutral-900">{formatoMoneda(Number(v.valorVenta) || 0)}</dd>
|
<dd className="mt-1 text-[#111827]">{formatoMoneda(Number(v.valorVenta) || 0)}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Total ingresos</dt>
|
<dt className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">Total ingresos</dt>
|
||||||
<dd className="mt-1 font-light text-neutral-900">{formatoMoneda(calcularTotalIngresos(economicos))}</dd>
|
<dd className="mt-1 text-[#111827]">{formatoMoneda(calcularTotalIngresos(economicos))}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Fecha</dt>
|
<dt className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">Fecha</dt>
|
||||||
<dd className="mt-1 font-light text-neutral-900">{formatoFechaCorta(g.fecha)}</dd>
|
<dd className="mt-1 text-[#111827]">{formatoFechaCorta(g.fecha)}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<dt className="text-xs font-bold uppercase tracking-widest text-neutral-500">Documentos</dt>
|
<dt className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">Documentos</dt>
|
||||||
<dd className="mt-1 font-light text-neutral-900">{alta.documentos.length} de 5</dd>
|
<dd className="mt-1 text-[#111827]">{alta.documentos.length} de 5</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
@@ -113,7 +113,7 @@ export default function FirmasForm({ alta, onCambiarFirma, onAnterior }: Props)
|
|||||||
<SignaturePad onFirma={(dataUrl) => onCambiarFirma({ ...firma, firmaDataUrl: dataUrl })} />
|
<SignaturePad onFirma={(dataUrl) => onCambiarFirma({ ...firma, firmaDataUrl: dataUrl })} />
|
||||||
</div>
|
</div>
|
||||||
<label htmlFor="asesorNombre" className="block">
|
<label htmlFor="asesorNombre" className="block">
|
||||||
<span className="mb-1 block text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="mb-1 block text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Nombre del asesor <span className="text-red-700">*</span>
|
Nombre del asesor <span className="text-red-700">*</span>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
@@ -121,7 +121,7 @@ export default function FirmasForm({ alta, onCambiarFirma, onAnterior }: Props)
|
|||||||
type="text"
|
type="text"
|
||||||
value={firma.asesorNombre}
|
value={firma.asesorNombre}
|
||||||
onChange={(e) => onCambiarFirma({ ...firma, asesorNombre: e.target.value.toUpperCase() })}
|
onChange={(e) => onCambiarFirma({ ...firma, asesorNombre: e.target.value.toUpperCase() })}
|
||||||
className="w-full border border-neutral-300 bg-white px-3 py-2.5 text-sm uppercase transition-colors duration-150 focus:border-neutral-900 focus:outline-none"
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm uppercase transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
</SectionCard>
|
</SectionCard>
|
||||||
@@ -132,7 +132,7 @@ export default function FirmasForm({ alta, onCambiarFirma, onAnterior }: Props)
|
|||||||
{avisoExpediente && (
|
{avisoExpediente && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-amber-300 bg-amber-50 px-4 py-3 text-xs text-amber-700 sm:col-span-2 lg:col-span-3"
|
className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-700 sm:col-span-2 lg:col-span-3"
|
||||||
>
|
>
|
||||||
{avisoExpediente}
|
{avisoExpediente}
|
||||||
</p>
|
</p>
|
||||||
@@ -143,7 +143,7 @@ export default function FirmasForm({ alta, onCambiarFirma, onAnterior }: Props)
|
|||||||
key={g3.tipo}
|
key={g3.tipo}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => generar(g3.descargar)}
|
onClick={() => generar(g3.descargar)}
|
||||||
className="border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="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]"
|
||||||
>
|
>
|
||||||
{g3.tipo}
|
{g3.tipo}
|
||||||
</button>
|
</button>
|
||||||
@@ -151,7 +151,7 @@ export default function FirmasForm({ alta, onCambiarFirma, onAnterior }: Props)
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={generarTodos}
|
onClick={generarTodos}
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-neutral-900 disabled:opacity-40"
|
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:opacity-40"
|
||||||
>
|
>
|
||||||
Generar todos
|
Generar todos
|
||||||
</button>
|
</button>
|
||||||
@@ -162,7 +162,7 @@ export default function FirmasForm({ alta, onCambiarFirma, onAnterior }: Props)
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onAnterior}
|
onClick={onAnterior}
|
||||||
className="border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="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]"
|
||||||
>
|
>
|
||||||
Anterior
|
Anterior
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export default function Navegacion({ onAnterior, onSiguiente, textoSiguiente = '
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onAnterior}
|
onClick={onAnterior}
|
||||||
className="border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="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]"
|
||||||
>
|
>
|
||||||
Anterior
|
Anterior
|
||||||
</button>
|
</button>
|
||||||
@@ -23,7 +23,7 @@ export default function Navegacion({ onAnterior, onSiguiente, textoSiguiente = '
|
|||||||
<button
|
<button
|
||||||
type={onSiguiente ? 'button' : 'submit'}
|
type={onSiguiente ? 'button' : 'submit'}
|
||||||
onClick={onSiguiente}
|
onClick={onSiguiente}
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-neutral-900 disabled:opacity-40"
|
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:opacity-40"
|
||||||
>
|
>
|
||||||
{textoSiguiente}
|
{textoSiguiente}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ export default function ReferenciasForm({ valores, onAnterior, onSiguiente }: Pa
|
|||||||
descripcion="Captura al menos 1 referencia con nombre y teléfono."
|
descripcion="Captura al menos 1 referencia con nombre y teléfono."
|
||||||
>
|
>
|
||||||
{[0, 1, 2, 3].map((i) => (
|
{[0, 1, 2, 3].map((i) => (
|
||||||
<div key={i} className="border border-neutral-200 p-6">
|
<div key={i} className="rounded-xl border border-[#E5E7EB] p-6">
|
||||||
<p className="mb-4 text-xs font-bold uppercase tracking-widest text-neutral-500">Referencia {i + 1}</p>
|
<p className="mb-4 text-xs font-semibold uppercase tracking-wider text-[#64748B]">Referencia {i + 1}</p>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<CampoTexto nombre={`referencias.${i}.nombre`} etiqueta="Nombre completo" />
|
<CampoTexto nombre={`referencias.${i}.nombre`} etiqueta="Nombre completo" />
|
||||||
<CampoTexto nombre={`referencias.${i}.telefono`} etiqueta="Teléfono" />
|
<CampoTexto nombre={`referencias.${i}.telefono`} etiqueta="Teléfono" />
|
||||||
|
|||||||
@@ -80,10 +80,10 @@ export default function ViviendaCreditoForm({ valores, onAnterior, onSiguiente }
|
|||||||
<label
|
<label
|
||||||
key={tc.valor}
|
key={tc.valor}
|
||||||
htmlFor={`tipoCredito-${tc.valor}`}
|
htmlFor={`tipoCredito-${tc.valor}`}
|
||||||
className={`cursor-pointer border px-3 py-1.5 text-xs font-bold uppercase tracking-wider transition-colors duration-150 ${
|
className={`cursor-pointer rounded-full border px-3 py-1.5 text-xs font-medium transition-all duration-100 ease-out ${
|
||||||
tiposSeleccionados.includes(tc.valor)
|
tiposSeleccionados.includes(tc.valor)
|
||||||
? 'border-accent bg-accent text-white'
|
? 'border-accent bg-accent text-white shadow-sm'
|
||||||
: 'border-neutral-300 bg-white text-neutral-600 hover:border-neutral-900'
|
: 'border-[#D1D5DB] bg-white text-[#4B5563] hover:border-accent hover:text-accent'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -2,22 +2,17 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
--color-accent: #1B4332;
|
--color-accent: #2563EB;
|
||||||
--radius-sm: 0;
|
--color-accent-hover: #1D4ED8;
|
||||||
--radius-md: 0;
|
|
||||||
--radius-lg: 0;
|
|
||||||
--radius-xl: 0;
|
|
||||||
--radius-2xl: 0;
|
|
||||||
--radius-3xl: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply bg-white text-neutral-900 antialiased;
|
@apply bg-[#F8FAFC] text-[#111827] antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline: 2px solid #1b4332;
|
outline: none;
|
||||||
outline-offset: 2px;
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -238,9 +238,9 @@ export default function AltaClientePage() {
|
|||||||
|
|
||||||
if (pantalla === 'cargando') {
|
if (pantalla === 'cargando') {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-[#F8FAFC]">
|
||||||
<main className="mx-auto max-w-6xl px-4 py-16">
|
<main className="mx-auto max-w-6xl px-4 py-16">
|
||||||
<p className="text-sm text-neutral-500">Cargando…</p>
|
<p className="text-sm text-[#64748B]">Cargando…</p>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -248,11 +248,11 @@ export default function AltaClientePage() {
|
|||||||
|
|
||||||
if (pantalla === 'entrada') {
|
if (pantalla === 'entrada') {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-[#F8FAFC]">
|
||||||
<header className="border-b border-neutral-200 px-4 py-6">
|
<header className="border-b border-[#E5E7EB] bg-white px-4 py-6">
|
||||||
<div className="mx-auto flex max-w-6xl items-baseline gap-3">
|
<div className="mx-auto flex max-w-6xl items-baseline gap-3">
|
||||||
<span className="text-sm font-bold uppercase tracking-widest">FRASA</span>
|
<span className="text-sm font-bold tracking-widest text-[#111827]">FRASA<span className="text-[#F97316]">.</span></span>
|
||||||
<h1 className="text-2xl font-light tracking-wide">Alta de Clientes</h1>
|
<h1 className="text-2xl font-semibold text-[#111827]">Alta de Clientes</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="mx-auto max-w-6xl px-4 py-16">
|
<main className="mx-auto max-w-6xl px-4 py-16">
|
||||||
@@ -260,20 +260,20 @@ export default function AltaClientePage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setPantalla('afluencia')}
|
onClick={() => setPantalla('afluencia')}
|
||||||
className="border border-neutral-200 bg-white p-8 text-left transition-colors duration-150 hover:border-accent"
|
className="rounded-xl border border-[#E5E7EB] bg-white p-8 text-left shadow-sm transition-all duration-200 ease-in-out hover:border-accent hover:shadow-md"
|
||||||
>
|
>
|
||||||
<h2 className="text-lg font-light tracking-wide text-neutral-900">Alta Afluencia</h2>
|
<h2 className="text-lg font-semibold text-[#111827]">Alta Afluencia</h2>
|
||||||
<p className="mt-2 text-xs font-light text-neutral-500">
|
<p className="mt-2 text-sm text-[#64748B]">
|
||||||
Captura rápida del interesado; entra al embudo de ventas.
|
Captura rápida del interesado; entra al embudo de ventas.
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setPantalla('picker')}
|
onClick={() => setPantalla('picker')}
|
||||||
className="border border-neutral-200 bg-white p-8 text-left transition-colors duration-150 hover:border-accent"
|
className="rounded-xl border border-[#E5E7EB] bg-white p-8 text-left shadow-sm transition-all duration-200 ease-in-out hover:border-accent hover:shadow-md"
|
||||||
>
|
>
|
||||||
<h2 className="text-lg font-light tracking-wide text-neutral-900">Alta de Cliente</h2>
|
<h2 className="text-lg font-semibold text-[#111827]">Alta de Cliente</h2>
|
||||||
<p className="mt-2 text-xs font-light text-neutral-500">
|
<p className="mt-2 text-sm text-[#64748B]">
|
||||||
Formato completo para quien aparta.
|
Formato completo para quien aparta.
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
@@ -285,11 +285,11 @@ export default function AltaClientePage() {
|
|||||||
|
|
||||||
if (pantalla === 'afluencia') {
|
if (pantalla === 'afluencia') {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-[#F8FAFC]">
|
||||||
<header className="border-b border-neutral-200 px-4 py-6">
|
<header className="border-b border-[#E5E7EB] bg-white px-4 py-6">
|
||||||
<div className="mx-auto flex max-w-6xl items-baseline gap-3">
|
<div className="mx-auto flex max-w-6xl items-baseline gap-3">
|
||||||
<span className="text-sm font-bold uppercase tracking-widest">FRASA</span>
|
<span className="text-sm font-bold tracking-widest text-[#111827]">FRASA<span className="text-[#F97316]">.</span></span>
|
||||||
<h1 className="text-2xl font-light tracking-wide">Alta Afluencia</h1>
|
<h1 className="text-2xl font-semibold text-[#111827]">Alta Afluencia</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="mx-auto max-w-6xl px-4 py-16">
|
<main className="mx-auto max-w-6xl px-4 py-16">
|
||||||
@@ -304,26 +304,26 @@ export default function AltaClientePage() {
|
|||||||
|
|
||||||
if (pantalla === 'picker') {
|
if (pantalla === 'picker') {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-[#F8FAFC]">
|
||||||
<header className="border-b border-neutral-200 px-4 py-6">
|
<header className="border-b border-[#E5E7EB] bg-white px-4 py-6">
|
||||||
<div className="mx-auto flex max-w-6xl items-baseline gap-3">
|
<div className="mx-auto flex max-w-6xl items-baseline gap-3">
|
||||||
<span className="text-sm font-bold uppercase tracking-widest">FRASA</span>
|
<span className="text-sm font-bold tracking-widest text-[#111827]">FRASA<span className="text-[#F97316]">.</span></span>
|
||||||
<h1 className="text-2xl font-light tracking-wide">Alta de Clientes</h1>
|
<h1 className="text-2xl font-semibold text-[#111827]">Alta de Clientes</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="mx-auto max-w-6xl px-4 py-16">
|
<main className="mx-auto max-w-6xl px-4 py-16">
|
||||||
{borradores.length > 0 && (
|
{borradores.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<h2 className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h2 className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Tienes {borradores.length} borrador{borradores.length === 1 ? '' : 'es'} guardado
|
Tienes {borradores.length} borrador{borradores.length === 1 ? '' : 'es'} guardado
|
||||||
{borradores.length === 1 ? '' : 's'}
|
{borradores.length === 1 ? '' : 's'}
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="mt-6 divide-y divide-neutral-200 border border-neutral-200 bg-white">
|
<ul className="mt-6 divide-y divide-[#E5E7EB] rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
|
||||||
{borradores.map((b) => (
|
{borradores.map((b) => (
|
||||||
<li key={b.id} className="flex flex-wrap items-center justify-between gap-4 px-4 py-3">
|
<li key={b.id} className="flex flex-wrap items-center justify-between gap-4 px-4 py-3">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<p className="truncate text-sm text-neutral-900">{b.nombre}</p>
|
<p className="truncate text-sm text-[#111827]">{b.nombre}</p>
|
||||||
<p className="mt-1 text-xs font-light text-neutral-500">
|
<p className="mt-1 text-xs text-[#64748B]">
|
||||||
Paso {b.paso + 1} de {PASOS.length} · {formatoFechaHora(b.updatedAt)}
|
Paso {b.paso + 1} de {PASOS.length} · {formatoFechaHora(b.updatedAt)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -331,14 +331,14 @@ export default function AltaClientePage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void continuarBorrador(b.id)}
|
onClick={() => void continuarBorrador(b.id)}
|
||||||
className="border border-accent bg-white px-3 py-1.5 text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:bg-accent hover:text-white"
|
className="rounded-lg border border-accent bg-white px-3 py-1.5 text-xs font-medium text-accent transition-all duration-100 ease-out hover:bg-accent hover:text-white"
|
||||||
>
|
>
|
||||||
Continuar
|
Continuar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void borrarBorrador(b.id)}
|
onClick={() => void borrarBorrador(b.id)}
|
||||||
className="border border-red-300 bg-white px-3 py-1.5 text-xs font-bold uppercase tracking-widest text-red-700 transition-colors duration-150 hover:bg-red-50"
|
className="rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 transition-all duration-100 ease-out hover:bg-red-50"
|
||||||
>
|
>
|
||||||
Eliminar
|
Eliminar
|
||||||
</button>
|
</button>
|
||||||
@@ -352,16 +352,16 @@ export default function AltaClientePage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={empezarNuevo}
|
onClick={empezarNuevo}
|
||||||
className="mt-8 bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-neutral-900"
|
className="mt-8 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"
|
||||||
>
|
>
|
||||||
Empezar nuevo
|
Empezar nuevo
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<section aria-labelledby="titulo-lead" className="mt-12 border-t border-neutral-200 pt-8">
|
<section aria-labelledby="titulo-lead" className="mt-12 border-t border-[#E5E7EB] pt-8">
|
||||||
<h3 id="titulo-lead" className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h3 id="titulo-lead" className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Desde un lead de Monday
|
Desde un lead de Monday
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-2 text-xs font-light text-neutral-500">
|
<p className="mt-2 text-sm text-[#64748B]">
|
||||||
Empieza un alta nueva con el nombre, email y celular del lead ya capturados.
|
Empieza un alta nueva con el nombre, email y celular del lead ya capturados.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
@@ -374,26 +374,26 @@ export default function AltaClientePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-[#F8FAFC]">
|
||||||
<header className="border-b border-neutral-200 px-4 py-6">
|
<header className="border-b border-[#E5E7EB] bg-white px-4 py-6">
|
||||||
<div className="mx-auto flex max-w-6xl flex-wrap items-baseline gap-3">
|
<div className="mx-auto flex max-w-6xl flex-wrap items-baseline gap-3">
|
||||||
<span className="text-sm font-bold uppercase tracking-widest">FRASA</span>
|
<span className="text-sm font-bold tracking-widest text-[#111827]">FRASA<span className="text-[#F97316]">.</span></span>
|
||||||
<h1 className="text-2xl font-light tracking-wide">Alta de Clientes</h1>
|
<h1 className="text-2xl font-semibold text-[#111827]">Alta de Clientes</h1>
|
||||||
<div className="ml-auto flex items-center gap-3">
|
<div className="ml-auto flex items-center gap-3">
|
||||||
{avisoBorrador && (
|
{avisoBorrador && (
|
||||||
<span role="status" className="text-xs font-light text-neutral-500">
|
<span role="status" className="text-xs text-[#64748B]">
|
||||||
{avisoBorrador}
|
{avisoBorrador}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{avisoProgreso && (
|
{avisoProgreso && (
|
||||||
<span role="status" className="text-xs font-light text-amber-700">
|
<span role="status" className="text-xs text-amber-700">
|
||||||
{avisoProgreso}
|
{avisoProgreso}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void guardar()}
|
onClick={() => void guardar()}
|
||||||
className="border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="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]"
|
||||||
>
|
>
|
||||||
Guardar borrador
|
Guardar borrador
|
||||||
</button>
|
</button>
|
||||||
@@ -405,7 +405,7 @@ export default function AltaClientePage() {
|
|||||||
<Stepper pasos={[...PASOS]} pasoActual={paso} onIrAPaso={irAPaso} />
|
<Stepper pasos={[...PASOS]} pasoActual={paso} onIrAPaso={irAPaso} />
|
||||||
|
|
||||||
{errorCarga && (
|
{errorCarga && (
|
||||||
<p role="alert" className="mt-8 border border-amber-300 bg-amber-50 px-4 py-3 text-xs text-amber-700">
|
<p role="alert" className="mt-8 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-700">
|
||||||
{errorCarga}
|
{errorCarga}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -481,7 +481,7 @@ export default function AltaClientePage() {
|
|||||||
{cargoDeBorrador && (
|
{cargoDeBorrador && (
|
||||||
<p
|
<p
|
||||||
role="note"
|
role="note"
|
||||||
className="mb-8 border border-amber-300 bg-amber-50 px-4 py-3 text-xs text-amber-700"
|
className="mb-8 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-700"
|
||||||
>
|
>
|
||||||
Retomaste un borrador: los archivos de los documentos no se guardan en el borrador,
|
Retomaste un borrador: los archivos de los documentos no se guardan en el borrador,
|
||||||
vuelve a adjuntarlos.
|
vuelve a adjuntarlos.
|
||||||
@@ -507,14 +507,14 @@ export default function AltaClientePage() {
|
|||||||
{paso === 7 && (
|
{paso === 7 && (
|
||||||
<>
|
<>
|
||||||
{registro && (
|
{registro && (
|
||||||
<p className="mb-8 border border-accent bg-white px-4 py-3 text-xs font-bold uppercase tracking-widest text-accent">
|
<p className="mb-8 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}
|
Alta registrada en el servidor: {registro}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{sinConexion && (
|
{sinConexion && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="mb-8 border border-amber-300 bg-amber-50 px-4 py-3 text-xs font-bold uppercase tracking-widest text-amber-700"
|
className="mb-8 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-xs text-amber-700"
|
||||||
>
|
>
|
||||||
Sin conexión al servidor — el expediente se generó solo localmente
|
Sin conexión al servidor — el expediente se generó solo localmente
|
||||||
</p>
|
</p>
|
||||||
@@ -522,32 +522,32 @@ export default function AltaClientePage() {
|
|||||||
{errorValidacion && (
|
{errorValidacion && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="mb-8 border border-red-300 bg-red-50 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="mb-8 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-xs text-red-700"
|
||||||
>
|
>
|
||||||
El servidor rechazó el alta: {errorValidacion}
|
El servidor rechazó el alta: {errorValidacion}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{registro && (
|
{registro && (
|
||||||
<div className="mb-8 border border-neutral-200 bg-white px-4 py-4">
|
<div className="mb-8 rounded-xl border border-[#E5E7EB] bg-white px-4 py-4 shadow-sm">
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Firma en remoto
|
Firma en remoto
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-xs font-light text-neutral-500">
|
<p className="mt-2 text-sm text-[#64748B]">
|
||||||
Genera una liga pública (vigencia 72 h) para que el cliente firme y suba sus
|
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.
|
documentos desde su celular, sin usuario ni contraseña.
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void generarLiga()}
|
onClick={() => void generarLiga()}
|
||||||
className="mt-4 border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="mt-4 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]"
|
||||||
>
|
>
|
||||||
Generar liga para firmar en remoto
|
Generar liga para firmar en remoto
|
||||||
</button>
|
</button>
|
||||||
{ligaUrl && (
|
{ligaUrl && (
|
||||||
<p className="mt-4 break-all text-xs text-neutral-900">
|
<p className="mt-4 break-all text-xs text-[#111827]">
|
||||||
{ligaUrl}
|
{ligaUrl}
|
||||||
{ligaCopiada && (
|
{ligaCopiada && (
|
||||||
<span role="status" className="ml-2 font-bold uppercase tracking-widest text-accent">
|
<span role="status" className="ml-2 font-medium text-accent">
|
||||||
Liga y mensaje de WhatsApp copiados
|
Liga y mensaje de WhatsApp copiados
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -82,10 +82,10 @@ export default function FirmaRemotaPage() {
|
|||||||
|
|
||||||
if (errorCarga) {
|
if (errorCarga) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white text-neutral-900">
|
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
|
||||||
<main className="mx-auto max-w-xl px-4 py-24 text-center">
|
<main className="mx-auto max-w-xl px-4 py-24 text-center">
|
||||||
<span className="text-sm font-bold uppercase tracking-widest">FRASA</span>
|
<span className="text-sm font-bold tracking-widest text-[#111827]">FRASA<span className="text-[#F97316]">.</span></span>
|
||||||
<p role="alert" className="mt-8 border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-700">
|
<p role="alert" className="mt-8 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">
|
||||||
{errorCarga}
|
{errorCarga}
|
||||||
</p>
|
</p>
|
||||||
</main>
|
</main>
|
||||||
@@ -95,29 +95,29 @@ export default function FirmaRemotaPage() {
|
|||||||
|
|
||||||
if (!cliente) {
|
if (!cliente) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-[#F8FAFC]">
|
||||||
<main className="mx-auto max-w-xl px-4 py-24">
|
<main className="mx-auto max-w-xl px-4 py-24">
|
||||||
<p className="text-sm text-neutral-500">Cargando…</p>
|
<p className="text-sm text-[#64748B]">Cargando…</p>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white text-neutral-900">
|
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
|
||||||
<header className="border-b border-neutral-200 px-4 py-6">
|
<header className="border-b border-[#E5E7EB] bg-white px-4 py-6">
|
||||||
<div className="mx-auto flex max-w-xl items-baseline gap-3">
|
<div className="mx-auto flex max-w-xl items-baseline gap-3">
|
||||||
<span className="text-sm font-bold uppercase tracking-widest">FRASA</span>
|
<span className="text-sm font-bold tracking-widest text-[#111827]">FRASA<span className="text-[#F97316]">.</span></span>
|
||||||
<h1 className="text-2xl font-light tracking-wide">Expediente digital</h1>
|
<h1 className="text-2xl font-semibold text-[#111827]">Expediente digital</h1>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="mx-auto max-w-xl space-y-12 px-4 py-12">
|
<main className="mx-auto max-w-xl space-y-12 px-4 py-12">
|
||||||
<section>
|
<section>
|
||||||
<h2 className="text-lg font-light">
|
<h2 className="text-lg font-semibold text-[#111827]">
|
||||||
Hola, <span className="font-medium">{cliente.nombre}</span>
|
Hola, <span className="font-semibold">{cliente.nombre}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-sm font-light text-neutral-500">
|
<p className="mt-2 text-sm text-[#64748B]">
|
||||||
Proyecto {cliente.proyecto}
|
Proyecto {cliente.proyecto}
|
||||||
{cliente.unidad ? ` · Unidad ${cliente.unidad}` : ''}. Firma y sube los documentos de tu
|
{cliente.unidad ? ` · Unidad ${cliente.unidad}` : ''}. Firma y sube los documentos de tu
|
||||||
expediente desde aquí.
|
expediente desde aquí.
|
||||||
@@ -125,11 +125,11 @@ export default function FirmaRemotaPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section aria-labelledby="firma-titulo">
|
<section aria-labelledby="firma-titulo">
|
||||||
<h3 id="firma-titulo" className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h3 id="firma-titulo" className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Tu firma
|
Tu firma
|
||||||
</h3>
|
</h3>
|
||||||
{firmaOk ? (
|
{firmaOk ? (
|
||||||
<p role="status" className="mt-4 border border-accent bg-white px-4 py-3 text-xs font-bold uppercase tracking-widest text-accent">
|
<p role="status" className="mt-4 rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-700">
|
||||||
Firma registrada correctamente
|
Firma registrada correctamente
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -140,7 +140,7 @@ export default function FirmaRemotaPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={enviandoFirma}
|
disabled={enviandoFirma}
|
||||||
onClick={() => void enviarFirma()}
|
onClick={() => void enviarFirma()}
|
||||||
className="mt-4 bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-neutral-900 disabled:opacity-40"
|
className="mt-4 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:opacity-40"
|
||||||
>
|
>
|
||||||
{enviandoFirma ? 'Enviando…' : 'Enviar firma'}
|
{enviandoFirma ? 'Enviando…' : 'Enviar firma'}
|
||||||
</button>
|
</button>
|
||||||
@@ -149,22 +149,22 @@ export default function FirmaRemotaPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section aria-labelledby="docs-titulo">
|
<section aria-labelledby="docs-titulo">
|
||||||
<h3 id="docs-titulo" className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h3 id="docs-titulo" className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Tus documentos
|
Tus documentos
|
||||||
</h3>
|
</h3>
|
||||||
<ul className="mt-4 space-y-4">
|
<ul className="mt-4 space-y-4">
|
||||||
{SLOTS_DOCUMENTOS.map(({ slot, etiqueta }) => {
|
{SLOTS_DOCUMENTOS.map(({ slot, etiqueta }) => {
|
||||||
const subido = cliente.documentos.find((d) => d.tipo === slot)
|
const subido = cliente.documentos.find((d) => d.tipo === slot)
|
||||||
return (
|
return (
|
||||||
<li key={slot} className="border border-neutral-200 bg-white px-4 py-3">
|
<li key={slot} className="rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm">
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">{etiqueta}</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">{etiqueta}</p>
|
||||||
{subido ? (
|
{subido ? (
|
||||||
<p role="status" className="mt-2 text-sm text-neutral-900">
|
<p role="status" className="mt-2 text-sm text-[#111827]">
|
||||||
✓ {subido.nombreArchivo}
|
✓ {subido.nombreArchivo}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<label className="inline-block cursor-pointer border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white">
|
<label className="inline-block cursor-pointer 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]">
|
||||||
{subiendoSlot === slot ? 'Subiendo…' : 'Seleccionar archivo'}
|
{subiendoSlot === slot ? 'Subiendo…' : 'Seleccionar archivo'}
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
@@ -178,7 +178,7 @@ export default function FirmaRemotaPage() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<p className="mt-2 text-xs font-light text-neutral-500">PDF/JPG/PNG · máx 10 MB</p>
|
<p className="mt-2 text-xs text-[#64748B]">PDF/JPG/PNG · máx 10 MB</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{erroresDoc[slot] && (
|
{erroresDoc[slot] && (
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white text-neutral-900">
|
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
|
||||||
<div className="border-b border-neutral-200">
|
<div className="border-b border-[#E5E7EB] bg-white">
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="text-xs font-medium text-[#64748B]">
|
||||||
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setModalPassword(true)}
|
onClick={() => setModalPassword(true)}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:text-neutral-900"
|
className="text-xs font-medium text-[#64748B] transition-colors duration-100 ease-out hover:text-[#111827]"
|
||||||
>
|
>
|
||||||
Contraseña
|
Contraseña
|
||||||
</button>
|
</button>
|
||||||
@@ -37,7 +37,7 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
logout()
|
logout()
|
||||||
setSesion(null)
|
setSesion(null)
|
||||||
}}
|
}}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:text-accent-hover"
|
className="text-xs font-medium text-accent transition-colors duration-100 ease-out hover:text-accent-hover"
|
||||||
>
|
>
|
||||||
Salir
|
Salir
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import type { StatusPago } from '../types'
|
import type { StatusPago } from '../types'
|
||||||
|
|
||||||
const CLASES: Record<StatusPago, string> = {
|
const CLASES: Record<StatusPago, string> = {
|
||||||
PENDIENTE: 'border-neutral-300 text-neutral-500',
|
PENDIENTE: 'bg-gray-100 text-gray-600',
|
||||||
PAGADO: 'border-green-300 text-green-700',
|
PAGADO: 'bg-green-50 text-green-700',
|
||||||
VENCIDO: 'border-red-300 text-red-700',
|
VENCIDO: 'bg-red-50 text-red-700',
|
||||||
PARCIAL: 'border-amber-300 text-amber-700',
|
PARCIAL: 'bg-amber-50 text-amber-700',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function BadgeStatus({ status }: { status: StatusPago }) {
|
export default function BadgeStatus({ status }: { status: StatusPago }) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-block border bg-white px-2 py-0.5 text-xs font-bold uppercase tracking-wider ${CLASES[status]}`}
|
className={`inline-block rounded-full px-2.5 py-0.5 text-xs font-medium ${CLASES[status]}`}
|
||||||
>
|
>
|
||||||
{status}
|
{status}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { validarConfirmacion, validarPasswordNueva } from '../lib/password'
|
|||||||
import { cambiarPassword } from '../services/auth'
|
import { cambiarPassword } from '../services/auth'
|
||||||
|
|
||||||
const CLASE_INPUT =
|
const CLASE_INPUT =
|
||||||
'w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none'
|
'w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
|
||||||
const CLASE_LABEL = 'mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500'
|
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
||||||
@@ -45,19 +45,19 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Cambiar contraseña"
|
aria-label="Cambiar contraseña"
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-sm border border-neutral-200 bg-white p-6">
|
<div className="w-full max-w-sm rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-xl">
|
||||||
<h2 className="mb-6 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h2 className="mb-6 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Cambiar contraseña
|
Cambiar contraseña
|
||||||
</h2>
|
</h2>
|
||||||
{exito ? (
|
{exito ? (
|
||||||
<>
|
<>
|
||||||
<p className="border border-accent px-4 py-3 text-xs font-bold uppercase tracking-widest text-accent">
|
<p className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-700">
|
||||||
Contraseña actualizada
|
Contraseña actualizada
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCerrar}
|
onClick={onCerrar}
|
||||||
className="mt-6 w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover"
|
className="mt-6 w-full 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"
|
||||||
>
|
>
|
||||||
Cerrar
|
Cerrar
|
||||||
</button>
|
</button>
|
||||||
@@ -111,7 +111,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@@ -120,14 +120,14 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="flex-1 bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="flex-1 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:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Guardando…' : 'Guardar'}
|
{cargando ? 'Guardando…' : 'Guardar'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCerrar}
|
onClick={onCerrar}
|
||||||
className="border border-neutral-300 px-5 py-3 text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:border-neutral-500"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#374151] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import type { ReactNode } from 'react'
|
|||||||
|
|
||||||
export default function KpiCard({ titulo, valor, detalle }: { titulo: string; valor: ReactNode; detalle?: string }) {
|
export default function KpiCard({ titulo, valor, detalle }: { titulo: string; valor: ReactNode; detalle?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="border border-neutral-200 bg-white p-6">
|
<div className="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm transition-shadow duration-200 ease-in-out hover:shadow-md">
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">{titulo}</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">{titulo}</p>
|
||||||
<p className="mt-2 text-3xl font-light text-neutral-900">{valor}</p>
|
<p className="mt-2 text-3xl font-light text-[#111827]">{valor}</p>
|
||||||
{detalle && <p className="mt-2 text-xs text-neutral-500">{detalle}</p>}
|
{detalle && <p className="mt-2 text-xs text-[#64748B]">{detalle}</p>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,36 +3,37 @@ import { Link } from 'react-router-dom'
|
|||||||
|
|
||||||
export default function Layout({ children }: { children: ReactNode }) {
|
export default function Layout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white text-neutral-900">
|
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
|
||||||
<header className="border-b border-neutral-200">
|
<header className="border-b border-[#E5E7EB] bg-white">
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-4">
|
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-4">
|
||||||
<Link to="/" className="text-sm tracking-widest">
|
<Link to="/" className="text-sm tracking-widest">
|
||||||
<span className="font-bold">FRASA</span>{' '}
|
<span className="font-bold">FRASA</span>
|
||||||
<span className="font-light text-neutral-500">COBRANZA</span>
|
<span className="text-[#F97316]">.</span>{' '}
|
||||||
|
<span className="font-light text-[#64748B]">COBRANZA</span>
|
||||||
</Link>
|
</Link>
|
||||||
<nav className="flex items-center gap-6 text-sm">
|
<nav className="flex items-center gap-6 text-sm">
|
||||||
<Link
|
<Link
|
||||||
to="/"
|
to="/"
|
||||||
className="text-neutral-500 transition-colors duration-150 hover:text-neutral-900"
|
className="text-[#64748B] transition-colors duration-150 hover:text-[#111827]"
|
||||||
>
|
>
|
||||||
Cobranza
|
Cobranza
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/hoy"
|
to="/hoy"
|
||||||
className="border border-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:bg-accent hover:text-white"
|
className="rounded-lg border border-accent px-5 py-2.5 text-sm font-medium text-accent transition-all duration-100 ease-out hover:bg-accent hover:text-white"
|
||||||
>
|
>
|
||||||
Cobranza del día
|
Cobranza del día
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
to="/nuevo"
|
to="/nuevo"
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:opacity-90"
|
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"
|
||||||
>
|
>
|
||||||
Nuevo plan
|
Nuevo plan
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className="mx-auto max-w-6xl px-4 py-16">{children}</main>
|
<main className="mx-auto max-w-6xl px-4 py-10">{children}</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-white px-4">
|
<div className="flex min-h-screen items-center justify-center bg-[#F8FAFC] px-4">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm">
|
||||||
<div className="mb-10 text-center">
|
<div className="mb-10 text-center">
|
||||||
<p className="text-lg font-bold tracking-widest">FRASA</p>
|
<p className="text-lg font-bold tracking-widest">
|
||||||
<p className="mt-2 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
FRASA<span className="text-[#F97316]">.</span>
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Acceso al ecosistema
|
Acceso al ecosistema
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +36,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="login-usuario"
|
htmlFor="login-usuario"
|
||||||
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Usuario
|
Usuario
|
||||||
</label>
|
</label>
|
||||||
@@ -46,13 +48,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="login-password"
|
htmlFor="login-password"
|
||||||
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Contraseña
|
Contraseña
|
||||||
</label>
|
</label>
|
||||||
@@ -63,13 +65,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@@ -77,7 +79,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="w-full 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:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Entrando…' : 'Entrar'}
|
{cargando ? 'Entrando…' : 'Entrar'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ interface Props {
|
|||||||
onGuardar: (datos: AbonoForm) => void
|
onGuardar: (datos: AbonoForm) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const claseLabel = 'block text-xs font-bold uppercase tracking-widest text-neutral-500'
|
const claseLabel = 'block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
/** Modal para registrar un abono (total o parcial) a un pago del plan */
|
/** Modal para registrar un abono (total o parcial) a un pago del plan */
|
||||||
export default function ModalAbono({ pago, onCancelar, onGuardar }: Props) {
|
export default function ModalAbono({ pago, onCancelar, onGuardar }: Props) {
|
||||||
@@ -62,13 +62,13 @@ export default function ModalAbono({ pago, onCancelar, onGuardar }: Props) {
|
|||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="titulo-abono"
|
aria-labelledby="titulo-abono"
|
||||||
className="w-full max-w-md border border-neutral-900 bg-white p-8"
|
className="w-full max-w-md rounded-2xl border border-[#E5E7EB] bg-white p-8 shadow-xl"
|
||||||
>
|
>
|
||||||
<h2 id="titulo-abono" className="text-xl font-light text-neutral-900">
|
<h2 id="titulo-abono" className="text-xl font-light text-[#111827]">
|
||||||
Registrar abono — Pago #{pago.numPago}
|
Registrar abono — Pago #{pago.numPago}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-sm text-neutral-500">
|
<p className="mt-2 text-sm text-[#64748B]">
|
||||||
Restante: <span className="font-bold text-neutral-900">{formatoMoneda(restante)}</span>
|
Restante: <span className="font-bold text-[#111827]">{formatoMoneda(restante)}</span>
|
||||||
</p>
|
</p>
|
||||||
<form
|
<form
|
||||||
className="mt-8 space-y-6"
|
className="mt-8 space-y-6"
|
||||||
@@ -84,7 +84,7 @@ export default function ModalAbono({ pago, onCancelar, onGuardar }: Props) {
|
|||||||
id="abono-monto"
|
id="abono-monto"
|
||||||
type="number"
|
type="number"
|
||||||
step="0.01"
|
step="0.01"
|
||||||
className={`mt-1 w-full border bg-white px-3 py-2.5 text-sm focus:border-neutral-900 focus:outline-none ${errors.monto ? 'border-red-400' : 'border-neutral-300'}`}
|
className={`mt-1 w-full rounded-lg border bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20 ${errors.monto ? 'border-red-400' : 'border-[#D1D5DB]'}`}
|
||||||
{...regMonto}
|
{...regMonto}
|
||||||
ref={(el) => {
|
ref={(el) => {
|
||||||
regMonto.ref(el)
|
regMonto.ref(el)
|
||||||
@@ -100,7 +100,7 @@ export default function ModalAbono({ pago, onCancelar, onGuardar }: Props) {
|
|||||||
<input
|
<input
|
||||||
id="abono-fecha"
|
id="abono-fecha"
|
||||||
type="date"
|
type="date"
|
||||||
className={`mt-1 w-full border bg-white px-3 py-2.5 text-sm focus:border-neutral-900 focus:outline-none ${errors.fechaPago ? 'border-red-400' : 'border-neutral-300'}`}
|
className={`mt-1 w-full rounded-lg border bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20 ${errors.fechaPago ? 'border-red-400' : 'border-[#D1D5DB]'}`}
|
||||||
{...register('fechaPago')}
|
{...register('fechaPago')}
|
||||||
/>
|
/>
|
||||||
{errors.fechaPago && <p className="mt-1 text-xs text-red-700">{errors.fechaPago.message}</p>}
|
{errors.fechaPago && <p className="mt-1 text-xs text-red-700">{errors.fechaPago.message}</p>}
|
||||||
@@ -112,7 +112,7 @@ export default function ModalAbono({ pago, onCancelar, onGuardar }: Props) {
|
|||||||
<input
|
<input
|
||||||
id="abono-referencia"
|
id="abono-referencia"
|
||||||
type="text"
|
type="text"
|
||||||
className={`mt-1 w-full border bg-white px-3 py-2.5 text-sm focus:border-neutral-900 focus:outline-none ${errors.referencia ? 'border-red-400' : 'border-neutral-300'}`}
|
className={`mt-1 w-full rounded-lg border bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20 ${errors.referencia ? 'border-red-400' : 'border-[#D1D5DB]'}`}
|
||||||
placeholder="SPEI-000123"
|
placeholder="SPEI-000123"
|
||||||
{...register('referencia')}
|
{...register('referencia')}
|
||||||
/>
|
/>
|
||||||
@@ -123,13 +123,13 @@ export default function ModalAbono({ pago, onCancelar, onGuardar }: Props) {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onCancelar}
|
onClick={onCancelar}
|
||||||
aria-label="Cancelar y cerrar el registro de abono"
|
aria-label="Cancelar y cerrar el registro de abono"
|
||||||
className="border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#374151] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:opacity-90 disabled:opacity-40"
|
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:opacity-40"
|
||||||
>
|
>
|
||||||
Guardar abono
|
Guardar abono
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ const ETIQUETA: Record<Semaforo, string> = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CLASE_DOT: Record<Semaforo, string> = {
|
const CLASE_DOT: Record<Semaforo, string> = {
|
||||||
VERDE: 'border-green-300 bg-white text-green-700',
|
VERDE: 'bg-green-50 text-green-700',
|
||||||
AMBAR: 'border-amber-300 bg-white text-amber-700',
|
AMBAR: 'bg-amber-50 text-amber-700',
|
||||||
ROJO: 'border-red-300 bg-white text-red-700',
|
ROJO: 'bg-red-50 text-red-700',
|
||||||
GRIS: 'border-neutral-300 bg-white text-neutral-500',
|
GRIS: 'bg-gray-100 text-gray-600',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,7 +37,7 @@ export function semaforoPlan(plan: PlanPago, hoy: Date = new Date()): Semaforo {
|
|||||||
export default function SemaforoDot({ valor }: { valor: Semaforo }) {
|
export default function SemaforoDot({ valor }: { valor: Semaforo }) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={`inline-block border px-2 py-0.5 text-xs font-bold uppercase tracking-wider ${CLASE_DOT[valor]}`}
|
className={`inline-block rounded-full px-2.5 py-0.5 text-xs font-medium ${CLASE_DOT[valor]}`}
|
||||||
>
|
>
|
||||||
{ETIQUETA[valor]}
|
{ETIQUETA[valor]}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2,22 +2,17 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
--color-accent: #1B4332;
|
--color-accent: #2563EB;
|
||||||
--radius-sm: 0;
|
--color-accent-hover: #1D4ED8;
|
||||||
--radius-md: 0;
|
|
||||||
--radius-lg: 0;
|
|
||||||
--radius-xl: 0;
|
|
||||||
--radius-2xl: 0;
|
|
||||||
--radius-3xl: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply bg-white text-neutral-900 antialiased;
|
@apply bg-[#F8FAFC] text-[#111827] antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
*:focus-visible {
|
*:focus-visible {
|
||||||
outline: 2px solid #1B4332;
|
outline: none;
|
||||||
outline-offset: 2px;
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export function crearDocumento(plan: PlanPago, hoy: Date = new Date()): Document
|
|||||||
formatoMoneda(plan.mensualidad),
|
formatoMoneda(plan.mensualidad),
|
||||||
String(plan.numPagos),
|
String(plan.numPagos),
|
||||||
]],
|
]],
|
||||||
headStyles: { fillColor: [4, 120, 87] },
|
headStyles: { fillColor: [37, 99, 235] },
|
||||||
})
|
})
|
||||||
y = finalY(doc) + 6
|
y = finalY(doc) + 6
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export function crearDocumento(plan: PlanPago, hoy: Date = new Date()): Document
|
|||||||
.map((a) => `${a.referencia} (${formatoFechaCorta(a.fechaPago)})`)
|
.map((a) => `${a.referencia} (${formatoFechaCorta(a.fechaPago)})`)
|
||||||
.join('\n') || '—',
|
.join('\n') || '—',
|
||||||
]),
|
]),
|
||||||
headStyles: { fillColor: [4, 120, 87] },
|
headStyles: { fillColor: [37, 99, 235] },
|
||||||
})
|
})
|
||||||
y = finalY(doc) + 6
|
y = finalY(doc) + 6
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import KpiCard from '../components/KpiCard'
|
|||||||
import SemaforoDot, { semaforoPlan } from '../components/Semaforo'
|
import SemaforoDot, { semaforoPlan } from '../components/Semaforo'
|
||||||
|
|
||||||
const MESES_CORTOS = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic']
|
const MESES_CORTOS = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic']
|
||||||
const COLORES_PIE = ['#1B4332', '#d4d4d4']
|
const COLORES_PIE = ['#2563EB', '#CBD5E1']
|
||||||
|
|
||||||
type ColumnaOrden = 'saldo' | 'vencimiento' | 'atraso' | 'avance'
|
type ColumnaOrden = 'saldo' | 'vencimiento' | 'atraso' | 'avance'
|
||||||
const COLUMNAS_ORDEN: ColumnaOrden[] = ['saldo', 'vencimiento', 'atraso', 'avance']
|
const COLUMNAS_ORDEN: ColumnaOrden[] = ['saldo', 'vencimiento', 'atraso', 'avance']
|
||||||
@@ -160,9 +160,9 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
if (!resultado) {
|
if (!resultado) {
|
||||||
return errorCarga ? (
|
return errorCarga ? (
|
||||||
<div className="border border-red-300 bg-white px-4 py-3 text-sm text-red-700">{errorCarga}</div>
|
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{errorCarga}</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-neutral-400">Cargando...</p>
|
<p className="text-sm text-[#64748B]">Cargando...</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,12 +200,12 @@ export default function DashboardPage() {
|
|||||||
})()
|
})()
|
||||||
|
|
||||||
const encabezadoOrdenable = (columna: ColumnaOrden, titulo: string, alineadoDerecha = false) => (
|
const encabezadoOrdenable = (columna: ColumnaOrden, titulo: string, alineadoDerecha = false) => (
|
||||||
<th scope="col" className={`py-3 pr-4 ${alineadoDerecha ? 'text-right' : ''}`}>
|
<th scope="col" className={`px-4 py-3 ${alineadoDerecha ? 'text-right' : ''}`}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => alternarOrden(columna)}
|
onClick={() => alternarOrden(columna)}
|
||||||
aria-label={`Ordenar por ${titulo}`}
|
aria-label={`Ordenar por ${titulo}`}
|
||||||
className="inline-flex items-center gap-1 font-bold uppercase tracking-widest transition-colors duration-150 hover:text-neutral-900"
|
className="inline-flex items-center gap-1 font-semibold uppercase tracking-wider transition-colors duration-100 ease-out hover:text-[#111827]"
|
||||||
>
|
>
|
||||||
{titulo}
|
{titulo}
|
||||||
{orden === columna && <span aria-hidden="true">{dir === 'asc' ? '↑' : '↓'}</span>}
|
{orden === columna && <span aria-hidden="true">{dir === 'asc' ? '↑' : '↓'}</span>}
|
||||||
@@ -214,12 +214,12 @@ export default function DashboardPage() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16">
|
<div className="space-y-8">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-3xl font-light text-neutral-900">Módulo Cobranza — Planes de enganche</h1>
|
<h1 className="text-3xl font-light text-[#111827]">Módulo Cobranza — Planes de enganche</h1>
|
||||||
<Link
|
<Link
|
||||||
to="/nuevo"
|
to="/nuevo"
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:opacity-90"
|
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"
|
||||||
>
|
>
|
||||||
Nuevo plan
|
Nuevo plan
|
||||||
</Link>
|
</Link>
|
||||||
@@ -248,8 +248,8 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
{/* Gráficas */}
|
{/* Gráficas */}
|
||||||
<div className="grid gap-6 lg:grid-cols-2">
|
<div className="grid gap-6 lg:grid-cols-2">
|
||||||
<div className="border border-neutral-200 bg-white p-6">
|
<div className="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||||
<h2 className="mb-6 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h2 className="mb-6 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Al corriente vs vencidos
|
Al corriente vs vencidos
|
||||||
</h2>
|
</h2>
|
||||||
<ResponsiveContainer width="100%" height={240}>
|
<ResponsiveContainer width="100%" height={240}>
|
||||||
@@ -272,21 +272,21 @@ export default function DashboardPage() {
|
|||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
<div className="border border-neutral-200 bg-white p-6">
|
<div className="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||||
<h2 className="mb-6 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h2 className="mb-6 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Cobrado por mes (últimos 6 meses)
|
Cobrado por mes (últimos 6 meses)
|
||||||
</h2>
|
</h2>
|
||||||
<ResponsiveContainer width="100%" height={240}>
|
<ResponsiveContainer width="100%" height={240}>
|
||||||
<BarChart data={datosBarras}>
|
<BarChart data={datosBarras}>
|
||||||
<CartesianGrid stroke="#e5e5e5" strokeDasharray="3 3" vertical={false} />
|
<CartesianGrid stroke="#E5E7EB" strokeDasharray="3 3" vertical={false} />
|
||||||
<XAxis dataKey="mes" tickLine={false} axisLine={{ stroke: '#e5e5e5' }} />
|
<XAxis dataKey="mes" tickLine={false} axisLine={{ stroke: '#E5E7EB' }} />
|
||||||
<YAxis
|
<YAxis
|
||||||
tickLine={false}
|
tickLine={false}
|
||||||
axisLine={false}
|
axisLine={false}
|
||||||
tickFormatter={(v: number) => `$${(v / 1000).toLocaleString('es-MX')}k`}
|
tickFormatter={(v: number) => `$${(v / 1000).toLocaleString('es-MX')}k`}
|
||||||
/>
|
/>
|
||||||
<Tooltip formatter={(v) => formatoMoneda(Number(v))} cursor={{ fill: '#fafafa' }} />
|
<Tooltip formatter={(v) => formatoMoneda(Number(v))} cursor={{ fill: '#F8FAFC' }} />
|
||||||
<Bar dataKey="cobrado" fill="#1B4332" name="Cobrado" />
|
<Bar dataKey="cobrado" fill="#2563EB" name="Cobrado" radius={[6, 6, 0, 0]} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
@@ -295,13 +295,13 @@ export default function DashboardPage() {
|
|||||||
{/* Filtros */}
|
{/* Filtros */}
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
{/* Toggle Mis planes / Todos (?mios=1 filtra en el API por el vendedor del token) */}
|
{/* Toggle Mis planes / Todos (?mios=1 filtra en el API por el vendedor del token) */}
|
||||||
<div className="flex border border-neutral-300" role="group" aria-label="Alcance de planes">
|
<div className="flex rounded-lg border border-[#D1D5DB] bg-white" role="group" aria-label="Alcance de planes">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => actualizarParams({ vista: mios ? null : 'mios' })}
|
onClick={() => actualizarParams({ vista: mios ? null : 'mios' })}
|
||||||
aria-pressed={mios}
|
aria-pressed={mios}
|
||||||
className={`px-4 py-2.5 text-xs font-bold uppercase tracking-widest transition-colors duration-150 ${
|
className={`rounded-l-lg px-4 py-2.5 text-sm font-medium transition-all duration-100 ease-out ${
|
||||||
mios ? 'bg-accent text-white' : 'bg-white text-neutral-600 hover:text-neutral-900'
|
mios ? 'bg-accent text-white shadow-sm' : 'bg-white text-[#64748B] hover:text-[#111827]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Mis planes
|
Mis planes
|
||||||
@@ -310,8 +310,8 @@ export default function DashboardPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => actualizarParams({ vista: mios ? 'todos' : null })}
|
onClick={() => actualizarParams({ vista: mios ? 'todos' : null })}
|
||||||
aria-pressed={!mios}
|
aria-pressed={!mios}
|
||||||
className={`px-4 py-2.5 text-xs font-bold uppercase tracking-widest transition-colors duration-150 ${
|
className={`rounded-r-lg px-4 py-2.5 text-sm font-medium transition-all duration-100 ease-out ${
|
||||||
mios ? 'bg-white text-neutral-600 hover:text-neutral-900' : 'bg-accent text-white'
|
mios ? 'bg-white text-[#64748B] hover:text-[#111827]' : 'bg-accent text-white shadow-sm'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Todos
|
Todos
|
||||||
@@ -326,7 +326,7 @@ export default function DashboardPage() {
|
|||||||
value={busqueda}
|
value={busqueda}
|
||||||
onChange={(e) => actualizarParams({ q: e.target.value })}
|
onChange={(e) => actualizarParams({ q: e.target.value })}
|
||||||
placeholder="Buscar por cliente o unidad…"
|
placeholder="Buscar por cliente o unidad…"
|
||||||
className="w-64 border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none"
|
className="w-64 rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
<label htmlFor="filtro-status" className="sr-only">
|
<label htmlFor="filtro-status" className="sr-only">
|
||||||
Filtrar por status
|
Filtrar por status
|
||||||
@@ -335,7 +335,7 @@ export default function DashboardPage() {
|
|||||||
id="filtro-status"
|
id="filtro-status"
|
||||||
value={filtroStatus}
|
value={filtroStatus}
|
||||||
onChange={(e) => actualizarParams({ status: e.target.value === 'TODOS' ? null : e.target.value })}
|
onChange={(e) => actualizarParams({ status: e.target.value === 'TODOS' ? null : e.target.value })}
|
||||||
className="border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
>
|
>
|
||||||
<option value="TODOS">Todos los status</option>
|
<option value="TODOS">Todos los status</option>
|
||||||
<option value="ACTIVO">ACTIVO</option>
|
<option value="ACTIVO">ACTIVO</option>
|
||||||
@@ -349,7 +349,7 @@ export default function DashboardPage() {
|
|||||||
id="filtro-proyecto"
|
id="filtro-proyecto"
|
||||||
value={filtroProyecto}
|
value={filtroProyecto}
|
||||||
onChange={(e) => actualizarParams({ proyecto: e.target.value === 'TODOS' ? null : e.target.value })}
|
onChange={(e) => actualizarParams({ proyecto: e.target.value === 'TODOS' ? null : e.target.value })}
|
||||||
className="border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
>
|
>
|
||||||
<option value="TODOS">Todos los proyectos</option>
|
<option value="TODOS">Todos los proyectos</option>
|
||||||
{proyectos.map((p) => (
|
{proyectos.map((p) => (
|
||||||
@@ -361,49 +361,49 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabla de planes */}
|
{/* Tabla de planes */}
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-neutral-900 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<tr className="border-b border-[#E5E7EB] bg-[#F8FAFC] text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
<th scope="col" className="py-3 pr-4">Cliente</th>
|
<th scope="col" className="px-4 py-3">Cliente</th>
|
||||||
<th scope="col" className="py-3 pr-4">Unidad</th>
|
<th scope="col" className="px-4 py-3">Unidad</th>
|
||||||
<th scope="col" className="py-3 pr-4">Proyecto</th>
|
<th scope="col" className="px-4 py-3">Proyecto</th>
|
||||||
{encabezadoOrdenable('avance', '% pagado')}
|
{encabezadoOrdenable('avance', '% pagado')}
|
||||||
{encabezadoOrdenable('vencimiento', 'Próx. vencimiento')}
|
{encabezadoOrdenable('vencimiento', 'Próx. vencimiento')}
|
||||||
{encabezadoOrdenable('saldo', 'Saldo', true)}
|
{encabezadoOrdenable('saldo', 'Saldo', true)}
|
||||||
{encabezadoOrdenable('atraso', 'Días atraso', true)}
|
{encabezadoOrdenable('atraso', 'Días atraso', true)}
|
||||||
<th scope="col" className="py-3">Estado</th>
|
<th scope="col" className="px-4 py-3">Estado</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody className="divide-y divide-[#E5E7EB]">
|
||||||
{ordenados.map((d) => (
|
{ordenados.map((d) => (
|
||||||
<tr key={d.plan.id} className="border-b border-neutral-200 transition-colors duration-150 hover:bg-neutral-50">
|
<tr key={d.plan.id} className="transition-colors duration-100 ease-out hover:bg-[#F8FAFC]">
|
||||||
<td className="py-3 pr-4">
|
<td className="px-4 py-3">
|
||||||
<Link to={`/planes/${d.plan.id}`} className="font-medium text-accent hover:underline">
|
<Link to={`/planes/${d.plan.id}`} className="font-medium text-accent hover:underline">
|
||||||
{d.plan.clienteNombre}
|
{d.plan.clienteNombre}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4">{d.plan.unidadCodigo}</td>
|
<td className="px-4 py-3">{d.plan.unidadCodigo}</td>
|
||||||
<td className="py-3 pr-4">{d.plan.proyecto}</td>
|
<td className="px-4 py-3">{d.plan.proyecto}</td>
|
||||||
<td className="py-3 pr-4">
|
<td className="px-4 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="h-1 w-24 bg-neutral-200">
|
<div className="h-1.5 w-24 rounded-full bg-[#E5E7EB]">
|
||||||
<div
|
<div
|
||||||
className="h-full bg-accent"
|
className="h-full rounded-full bg-accent"
|
||||||
style={{ width: `${Math.min(100, d.avance * 100)}%` }}
|
style={{ width: `${Math.min(100, d.avance * 100)}%` }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs text-neutral-500">{formatoPorcentaje(d.avance)}</span>
|
<span className="text-xs text-[#64748B]">{formatoPorcentaje(d.avance)}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4">
|
<td className="px-4 py-3">
|
||||||
{d.proximo ? formatoFechaCorta(d.proximo.fechaVencimiento) : '—'}
|
{d.proximo ? formatoFechaCorta(d.proximo.fechaVencimiento) : '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 text-right font-medium">{formatoMoneda(d.saldo)}</td>
|
<td className="px-4 py-3 text-right font-medium">{formatoMoneda(d.saldo)}</td>
|
||||||
<td className="py-3 pr-4 text-right">
|
<td className="px-4 py-3 text-right">
|
||||||
{d.atraso > 0 ? <span className="text-red-700">{d.atraso}</span> : '0'}
|
{d.atraso > 0 ? <span className="text-red-700">{d.atraso}</span> : '0'}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3">
|
<td className="px-4 py-3">
|
||||||
<SemaforoDot valor={d.semaforo} />
|
<SemaforoDot valor={d.semaforo} />
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|||||||
@@ -36,17 +36,17 @@ export default function HoyPage() {
|
|||||||
|
|
||||||
if (!resultado) {
|
if (!resultado) {
|
||||||
return errorCarga ? (
|
return errorCarga ? (
|
||||||
<div className="border border-red-300 bg-white px-4 py-3 text-sm text-red-700">{errorCarga}</div>
|
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{errorCarga}</div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-neutral-400">Cargando...</p>
|
<p className="text-sm text-[#64748B]">Cargando...</p>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16">
|
<div className="space-y-8">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-3xl font-light text-neutral-900">Cobranza del día</h1>
|
<h1 className="text-3xl font-light text-[#111827]">Cobranza del día</h1>
|
||||||
<p className="mt-1 text-sm text-neutral-500">
|
<p className="mt-1 text-sm text-[#64748B]">
|
||||||
Pagos vencidos y vencimientos de los próximos 7 días, en todos los planes activos.
|
Pagos vencidos y vencimientos de los próximos 7 días, en todos los planes activos.
|
||||||
</p>
|
</p>
|
||||||
{resultado.origen === 'local' && (
|
{resultado.origen === 'local' && (
|
||||||
@@ -62,29 +62,29 @@ export default function HoyPage() {
|
|||||||
<KpiCard titulo="Monto por cobrar" valor={formatoMoneda(totalPorCobrar)} />
|
<KpiCard titulo="Monto por cobrar" valor={formatoMoneda(totalPorCobrar)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-neutral-900 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<tr className="border-b border-[#E5E7EB] bg-[#F8FAFC] text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
<th scope="col" className="py-3 pr-4">Cliente</th>
|
<th scope="col" className="px-4 py-3">Cliente</th>
|
||||||
<th scope="col" className="py-3 pr-4">Teléfono</th>
|
<th scope="col" className="px-4 py-3">Teléfono</th>
|
||||||
<th scope="col" className="py-3 pr-4">Unidad</th>
|
<th scope="col" className="px-4 py-3">Unidad</th>
|
||||||
<th scope="col" className="py-3 pr-4"># pago</th>
|
<th scope="col" className="px-4 py-3"># pago</th>
|
||||||
<th scope="col" className="py-3 pr-4">Vencimiento</th>
|
<th scope="col" className="px-4 py-3">Vencimiento</th>
|
||||||
<th scope="col" className="py-3 pr-4 text-right">Monto</th>
|
<th scope="col" className="px-4 py-3 text-right">Monto</th>
|
||||||
<th scope="col" className="py-3 pr-4 text-right">Restante</th>
|
<th scope="col" className="px-4 py-3 text-right">Restante</th>
|
||||||
<th scope="col" className="py-3 pr-4 text-right">Días atraso</th>
|
<th scope="col" className="px-4 py-3 text-right">Días atraso</th>
|
||||||
<th scope="col" className="py-3">Plan</th>
|
<th scope="col" className="px-4 py-3">Plan</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody className="divide-y divide-[#E5E7EB]">
|
||||||
{items.map(({ plan, pago, atraso }) => (
|
{items.map(({ plan, pago, atraso }) => (
|
||||||
<tr
|
<tr
|
||||||
key={`${plan.id}-${pago.id}`}
|
key={`${plan.id}-${pago.id}`}
|
||||||
className="border-b border-neutral-200 transition-colors duration-150 hover:bg-neutral-50"
|
className="transition-colors duration-100 ease-out hover:bg-[#F8FAFC]"
|
||||||
>
|
>
|
||||||
<td className="py-3 pr-4 font-medium">{plan.clienteNombre}</td>
|
<td className="px-4 py-3 font-medium">{plan.clienteNombre}</td>
|
||||||
<td className="py-3 pr-4">
|
<td className="px-4 py-3">
|
||||||
{plan.clienteTelefono ? (
|
{plan.clienteTelefono ? (
|
||||||
<a href={`tel:${plan.clienteTelefono.replace(/\s/g, '')}`} className="text-accent hover:underline">
|
<a href={`tel:${plan.clienteTelefono.replace(/\s/g, '')}`} className="text-accent hover:underline">
|
||||||
{plan.clienteTelefono}
|
{plan.clienteTelefono}
|
||||||
@@ -93,14 +93,14 @@ export default function HoyPage() {
|
|||||||
<span className="text-neutral-400">—</span>
|
<span className="text-neutral-400">—</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4">{plan.unidadCodigo}</td>
|
<td className="px-4 py-3">{plan.unidadCodigo}</td>
|
||||||
<td className="py-3 pr-4">{pago.numPago}</td>
|
<td className="px-4 py-3">{pago.numPago}</td>
|
||||||
<td className="py-3 pr-4">{formatoFechaCorta(pago.fechaVencimiento)}</td>
|
<td className="px-4 py-3">{formatoFechaCorta(pago.fechaVencimiento)}</td>
|
||||||
<td className="py-3 pr-4 text-right">{formatoMoneda(pago.monto)}</td>
|
<td className="px-4 py-3 text-right">{formatoMoneda(pago.monto)}</td>
|
||||||
<td className="py-3 pr-4 text-right font-medium">
|
<td className="px-4 py-3 text-right font-medium">
|
||||||
{formatoMoneda(round2(pago.monto - pago.montoPagado))}
|
{formatoMoneda(round2(pago.monto - pago.montoPagado))}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4 text-right">
|
<td className="px-4 py-3 text-right">
|
||||||
{atraso > 0 ? (
|
{atraso > 0 ? (
|
||||||
<span className="font-medium text-red-700">{atraso}</span>
|
<span className="font-medium text-red-700">{atraso}</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -109,7 +109,7 @@ export default function HoyPage() {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3">
|
<td className="px-4 py-3">
|
||||||
<Link to={`/planes/${plan.id}`} className="text-accent hover:underline">
|
<Link to={`/planes/${plan.id}`} className="text-accent hover:underline">
|
||||||
Ver plan
|
Ver plan
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -37,9 +37,9 @@ const esquema = z.object({
|
|||||||
type NuevoPlanForm = z.infer<typeof esquema>
|
type NuevoPlanForm = z.infer<typeof esquema>
|
||||||
|
|
||||||
const claseInput =
|
const claseInput =
|
||||||
'mt-1 w-full border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none'
|
'mt-1 w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
|
||||||
const claseError = 'mt-1 text-xs text-red-700'
|
const claseError = 'mt-1 text-xs text-red-700'
|
||||||
const claseLabel = 'block text-xs font-bold uppercase tracking-widest text-neutral-500'
|
const claseLabel = 'block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
export default function NuevoPlanPage() {
|
export default function NuevoPlanPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@@ -88,11 +88,11 @@ export default function NuevoPlanPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-3xl space-y-16">
|
<div className="mx-auto max-w-3xl space-y-8">
|
||||||
<h1 className="text-3xl font-light text-neutral-900">Nuevo plan de enganche</h1>
|
<h1 className="text-3xl font-light text-[#111827]">Nuevo plan de enganche</h1>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
className="border border-neutral-200 bg-white p-8"
|
className="rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
void handleSubmit(guardar)(e)
|
void handleSubmit(guardar)(e)
|
||||||
}}
|
}}
|
||||||
@@ -176,14 +176,14 @@ export default function NuevoPlanPage() {
|
|||||||
|
|
||||||
{/* Vista previa del calendario */}
|
{/* Vista previa del calendario */}
|
||||||
{vistaPrevia && (
|
{vistaPrevia && (
|
||||||
<div className="mt-8 border border-neutral-200 bg-white p-6">
|
<div className="mt-8 rounded-xl border border-[#E5E7EB] bg-[#F8FAFC] p-6">
|
||||||
<h2 className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h2 className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Vista previa — {vistaPrevia.pagos.length} pagos de {formatoMoneda(vistaPrevia.mensualidad)}
|
Vista previa — {vistaPrevia.pagos.length} pagos de {formatoMoneda(vistaPrevia.mensualidad)}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-4 max-h-48 overflow-y-auto">
|
<div className="mt-4 max-h-48 overflow-y-auto">
|
||||||
<table className="w-full text-left text-xs">
|
<table className="w-full text-left text-xs">
|
||||||
<thead className="text-neutral-500">
|
<thead className="text-[#64748B]">
|
||||||
<tr className="border-b border-neutral-900">
|
<tr className="border-b border-[#D1D5DB]">
|
||||||
<th scope="col" className="py-1">#</th>
|
<th scope="col" className="py-1">#</th>
|
||||||
<th scope="col" className="py-1">Vencimiento</th>
|
<th scope="col" className="py-1">Vencimiento</th>
|
||||||
<th scope="col" className="py-1 text-right">Monto</th>
|
<th scope="col" className="py-1 text-right">Monto</th>
|
||||||
@@ -191,7 +191,7 @@ export default function NuevoPlanPage() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{vistaPrevia.pagos.map((p) => (
|
{vistaPrevia.pagos.map((p) => (
|
||||||
<tr key={p.numPago} className="border-b border-neutral-200">
|
<tr key={p.numPago} className="border-b border-[#E5E7EB]">
|
||||||
<td className="py-1">{p.numPago}</td>
|
<td className="py-1">{p.numPago}</td>
|
||||||
<td className="py-1">{formatoFechaCorta(p.fechaVencimiento)}</td>
|
<td className="py-1">{formatoFechaCorta(p.fechaVencimiento)}</td>
|
||||||
<td className="py-1 text-right">{formatoMoneda(p.monto)}</td>
|
<td className="py-1 text-right">{formatoMoneda(p.monto)}</td>
|
||||||
@@ -208,14 +208,14 @@ export default function NuevoPlanPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void navigate('/')}
|
onClick={() => void navigate('/')}
|
||||||
className="border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#374151] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={guardando}
|
disabled={guardando}
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:opacity-90 disabled:opacity-40"
|
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:opacity-40"
|
||||||
>
|
>
|
||||||
{guardando ? 'Guardando…' : 'Guardar plan'}
|
{guardando ? 'Guardando…' : 'Guardar plan'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -63,17 +63,17 @@ export default function PlanDetallePage() {
|
|||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
if (cargando) {
|
if (cargando) {
|
||||||
return <p className="text-sm text-neutral-400">Cargando...</p>
|
return <p className="text-sm text-[#64748B]">Cargando...</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorCarga) {
|
if (errorCarga) {
|
||||||
return <div className="border border-red-300 bg-white px-4 py-3 text-sm text-red-700">{errorCarga}</div>
|
return <div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{errorCarga}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!plan) {
|
if (!plan) {
|
||||||
return (
|
return (
|
||||||
<div className="border border-neutral-200 bg-white p-16 text-center">
|
<div className="rounded-xl border border-[#E5E7EB] bg-white p-16 text-center shadow-sm">
|
||||||
<p className="text-neutral-500">Plan no encontrado.</p>
|
<p className="text-[#64748B]">Plan no encontrado.</p>
|
||||||
<Link to="/" className="mt-4 inline-block text-accent hover:underline">
|
<Link to="/" className="mt-4 inline-block text-accent hover:underline">
|
||||||
Volver al dashboard
|
Volver al dashboard
|
||||||
</Link>
|
</Link>
|
||||||
@@ -134,19 +134,19 @@ export default function PlanDetallePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16">
|
<div className="space-y-8">
|
||||||
<div className="flex flex-wrap items-start justify-between gap-6">
|
<div className="flex flex-wrap items-start justify-between gap-6">
|
||||||
<div>
|
<div>
|
||||||
<Link to="/" className="text-sm text-accent hover:underline">
|
<Link to="/" className="text-sm text-accent hover:underline">
|
||||||
← Volver al dashboard
|
← Volver al dashboard
|
||||||
</Link>
|
</Link>
|
||||||
<h1 className="mt-2 text-3xl font-light text-neutral-900">{plan.clienteNombre}</h1>
|
<h1 className="mt-2 text-3xl font-light text-[#111827]">{plan.clienteNombre}</h1>
|
||||||
<p className="mt-1 text-sm text-neutral-500">
|
<p className="mt-1 text-sm text-[#64748B]">
|
||||||
{plan.unidadCodigo} · {plan.proyecto} · Venta {plan.ventaRef || '—'} ·{' '}
|
{plan.unidadCodigo} · {plan.proyecto} · Venta {plan.ventaRef || '—'} ·{' '}
|
||||||
<span className="font-bold text-neutral-900">{status}</span>
|
<span className="font-semibold text-[#111827]">{status}</span>
|
||||||
{atraso > 0 && <span className="text-red-700"> · {atraso} días de atraso</span>}
|
{atraso > 0 && <span className="text-red-700"> · {atraso} días de atraso</span>}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-sm text-neutral-500">
|
<p className="mt-1 text-sm text-[#64748B]">
|
||||||
Tel. {plan.clienteTelefono || '—'}
|
Tel. {plan.clienteTelefono || '—'}
|
||||||
{tasaMora > 0 && <span> · Mora {tasaMora}% mensual</span>}
|
{tasaMora > 0 && <span> · Mora {tasaMora}% mensual</span>}
|
||||||
</p>
|
</p>
|
||||||
@@ -154,14 +154,14 @@ export default function PlanDetallePage() {
|
|||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => descargar(plan)}
|
onClick={() => descargar(plan)}
|
||||||
className="border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#374151] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Estado de cuenta PDF
|
Estado de cuenta PDF
|
||||||
</button>
|
</button>
|
||||||
{status === 'ACTIVO' && saldo > 0 && (
|
{status === 'ACTIVO' && saldo > 0 && (
|
||||||
<button
|
<button
|
||||||
onClick={cancelar}
|
onClick={cancelar}
|
||||||
className="border border-red-300 bg-white px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-red-700 transition-colors duration-150 hover:bg-red-700 hover:text-white"
|
className="rounded-lg border border-red-600 bg-white px-5 py-2.5 text-sm font-medium text-red-600 transition-all duration-100 ease-out hover:bg-red-600 hover:text-white"
|
||||||
>
|
>
|
||||||
Cancelar plan
|
Cancelar plan
|
||||||
</button>
|
</button>
|
||||||
@@ -176,7 +176,7 @@ export default function PlanDetallePage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="border border-red-300 bg-white px-4 py-3 text-sm text-red-700">{error}</div>
|
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error}</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* KPIs del plan */}
|
{/* KPIs del plan */}
|
||||||
@@ -196,22 +196,22 @@ export default function PlanDetallePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Tabla de pagos */}
|
{/* Tabla de pagos */}
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
|
||||||
<table className="w-full text-left text-sm">
|
<table className="w-full text-left text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b border-neutral-900 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<tr className="border-b border-[#E5E7EB] bg-[#F8FAFC] text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
<th scope="col" className="py-3 pr-4">#</th>
|
<th scope="col" className="px-4 py-3">#</th>
|
||||||
<th scope="col" className="py-3 pr-4">Vencimiento</th>
|
<th scope="col" className="px-4 py-3">Vencimiento</th>
|
||||||
<th scope="col" className="py-3 pr-4 text-right">Monto</th>
|
<th scope="col" className="px-4 py-3 text-right">Monto</th>
|
||||||
<th scope="col" className="py-3 pr-4 text-right">Pagado</th>
|
<th scope="col" className="px-4 py-3 text-right">Pagado</th>
|
||||||
<th scope="col" className="py-3 pr-4 text-right">Restante</th>
|
<th scope="col" className="px-4 py-3 text-right">Restante</th>
|
||||||
{tasaMora > 0 && <th scope="col" className="py-3 pr-4 text-right">Mora</th>}
|
{tasaMora > 0 && <th scope="col" className="px-4 py-3 text-right">Mora</th>}
|
||||||
<th scope="col" className="py-3 pr-4">Status</th>
|
<th scope="col" className="px-4 py-3">Status</th>
|
||||||
<th scope="col" className="py-3 pr-4">Abonos</th>
|
<th scope="col" className="px-4 py-3">Abonos</th>
|
||||||
<th scope="col" className="py-3" />
|
<th scope="col" className="px-4 py-3" />
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody className="divide-y divide-[#E5E7EB]">
|
||||||
{plan.pagos.map((pago) => {
|
{plan.pagos.map((pago) => {
|
||||||
const st = statusPago(pago, hoy)
|
const st = statusPago(pago, hoy)
|
||||||
const restante = round2(pago.monto - pago.montoPagado)
|
const restante = round2(pago.monto - pago.montoPagado)
|
||||||
@@ -219,14 +219,14 @@ export default function PlanDetallePage() {
|
|||||||
const expandido = expandidos.has(pago.id)
|
const expandido = expandidos.has(pago.id)
|
||||||
return (
|
return (
|
||||||
<Fragment key={pago.id}>
|
<Fragment key={pago.id}>
|
||||||
<tr className="border-b border-neutral-200 transition-colors duration-150 hover:bg-neutral-50">
|
<tr className="transition-colors duration-100 ease-out hover:bg-[#F8FAFC]">
|
||||||
<td className="py-3 pr-4 font-medium">{pago.numPago}</td>
|
<td className="px-4 py-3 font-medium">{pago.numPago}</td>
|
||||||
<td className="py-3 pr-4">{formatoFechaCorta(pago.fechaVencimiento)}</td>
|
<td className="px-4 py-3">{formatoFechaCorta(pago.fechaVencimiento)}</td>
|
||||||
<td className="py-3 pr-4 text-right">{formatoMoneda(pago.monto)}</td>
|
<td className="px-4 py-3 text-right">{formatoMoneda(pago.monto)}</td>
|
||||||
<td className="py-3 pr-4 text-right">{formatoMoneda(pago.montoPagado)}</td>
|
<td className="px-4 py-3 text-right">{formatoMoneda(pago.montoPagado)}</td>
|
||||||
<td className="py-3 pr-4 text-right font-medium">{formatoMoneda(restante)}</td>
|
<td className="px-4 py-3 text-right font-medium">{formatoMoneda(restante)}</td>
|
||||||
{tasaMora > 0 && (
|
{tasaMora > 0 && (
|
||||||
<td className="py-3 pr-4 text-right">
|
<td className="px-4 py-3 text-right">
|
||||||
{moraPago > 0 ? (
|
{moraPago > 0 ? (
|
||||||
<span className="text-red-700">{formatoMoneda(moraPago)}</span>
|
<span className="text-red-700">{formatoMoneda(moraPago)}</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -234,10 +234,10 @@ export default function PlanDetallePage() {
|
|||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
<td className="py-3 pr-4">
|
<td className="px-4 py-3">
|
||||||
<BadgeStatus status={st} />
|
<BadgeStatus status={st} />
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 pr-4">
|
<td className="px-4 py-3">
|
||||||
{pago.aplicaciones.length > 0 ? (
|
{pago.aplicaciones.length > 0 ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleExpandido(pago.id)}
|
onClick={() => toggleExpandido(pago.id)}
|
||||||
@@ -249,11 +249,11 @@ export default function PlanDetallePage() {
|
|||||||
<span className="text-neutral-400">—</span>
|
<span className="text-neutral-400">—</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 text-right">
|
<td className="px-4 py-3 text-right">
|
||||||
{st !== 'PAGADO' && status === 'ACTIVO' && (
|
{st !== 'PAGADO' && status === 'ACTIVO' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setPagoAbono(pago)}
|
onClick={() => setPagoAbono(pago)}
|
||||||
className="bg-accent px-4 py-2 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:opacity-90"
|
className="rounded-lg bg-accent px-4 py-2 text-xs font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover"
|
||||||
>
|
>
|
||||||
Registrar abono
|
Registrar abono
|
||||||
</button>
|
</button>
|
||||||
@@ -261,11 +261,11 @@ export default function PlanDetallePage() {
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{expandido && (
|
{expandido && (
|
||||||
<tr className="border-b border-neutral-200 bg-neutral-50">
|
<tr className="bg-[#F8FAFC]">
|
||||||
<td colSpan={tasaMora > 0 ? 9 : 8} className="px-4 py-4">
|
<td colSpan={tasaMora > 0 ? 9 : 8} className="px-4 py-4">
|
||||||
<table className="w-full text-xs">
|
<table className="w-full text-xs">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="text-left font-bold uppercase tracking-widest text-neutral-500">
|
<tr className="text-left font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
<th scope="col" className="py-1">Fecha</th>
|
<th scope="col" className="py-1">Fecha</th>
|
||||||
<th scope="col" className="py-1 text-right">Monto</th>
|
<th scope="col" className="py-1 text-right">Monto</th>
|
||||||
<th scope="col" className="py-1">Referencia</th>
|
<th scope="col" className="py-1">Referencia</th>
|
||||||
@@ -274,7 +274,7 @@ export default function PlanDetallePage() {
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{pago.aplicaciones.map((a) => (
|
{pago.aplicaciones.map((a) => (
|
||||||
<tr key={a.id} className="border-t border-neutral-200">
|
<tr key={a.id} className="border-t border-[#E5E7EB]">
|
||||||
<td className="py-1">{formatoFechaCorta(a.fechaPago)}</td>
|
<td className="py-1">{formatoFechaCorta(a.fechaPago)}</td>
|
||||||
<td className="py-1 text-right">{formatoMoneda(a.monto)}</td>
|
<td className="py-1 text-right">{formatoMoneda(a.monto)}</td>
|
||||||
<td className="py-1">{a.referencia}</td>
|
<td className="py-1">{a.referencia}</td>
|
||||||
@@ -302,19 +302,19 @@ export default function PlanDetallePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bitácora del plan */}
|
{/* Bitácora del plan */}
|
||||||
<section className="border border-neutral-200 bg-white p-8" aria-labelledby="titulo-bitacora">
|
<section className="rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm" aria-labelledby="titulo-bitacora">
|
||||||
<h2 id="titulo-bitacora" className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h2 id="titulo-bitacora" className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Bitácora
|
Bitácora
|
||||||
</h2>
|
</h2>
|
||||||
{(plan.notas ?? []).length === 0 ? (
|
{(plan.notas ?? []).length === 0 ? (
|
||||||
<p className="mt-6 text-sm text-neutral-500">Sin notas registradas.</p>
|
<p className="mt-6 text-sm text-[#64748B]">Sin notas registradas.</p>
|
||||||
) : (
|
) : (
|
||||||
<ul className="mt-6 space-y-4">
|
<ul className="mt-6 space-y-4">
|
||||||
{[...(plan.notas ?? [])]
|
{[...(plan.notas ?? [])]
|
||||||
.sort((a, b) => b.fecha.localeCompare(a.fecha))
|
.sort((a, b) => b.fecha.localeCompare(a.fecha))
|
||||||
.map((nota) => (
|
.map((nota) => (
|
||||||
<li key={nota.id} className="border-l-2 border-neutral-200 pl-4">
|
<li key={nota.id} className="border-l-2 border-[#E5E7EB] pl-4">
|
||||||
<p className="text-xs text-neutral-500">
|
<p className="text-xs text-[#64748B]">
|
||||||
{new Date(nota.fecha).toLocaleString('es-MX', {
|
{new Date(nota.fecha).toLocaleString('es-MX', {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
month: '2-digit',
|
month: '2-digit',
|
||||||
@@ -323,7 +323,7 @@ export default function PlanDetallePage() {
|
|||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-sm text-neutral-900">{nota.texto}</p>
|
<p className="mt-1 text-sm text-[#111827]">{nota.texto}</p>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -337,7 +337,7 @@ export default function PlanDetallePage() {
|
|||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
htmlFor="nota-texto"
|
htmlFor="nota-texto"
|
||||||
className="block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Nueva nota
|
Nueva nota
|
||||||
</label>
|
</label>
|
||||||
@@ -347,14 +347,14 @@ export default function PlanDetallePage() {
|
|||||||
onChange={(e) => setNotaTexto(e.target.value)}
|
onChange={(e) => setNotaTexto(e.target.value)}
|
||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Gestión realizada, acuerdos con el cliente…"
|
placeholder="Gestión realizada, acuerdos con el cliente…"
|
||||||
className="mt-1 w-full border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none"
|
className="mt-1 w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
{errorNota && <p className="mt-1 text-xs text-red-700">{errorNota}</p>}
|
{errorNota && <p className="mt-1 text-xs text-red-700">{errorNota}</p>}
|
||||||
<div className="mt-4 flex justify-end">
|
<div className="mt-4 flex justify-end">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={notaTexto.trim().length < 3}
|
disabled={notaTexto.trim().length < 3}
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:opacity-90 disabled:opacity-40"
|
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:opacity-40"
|
||||||
>
|
>
|
||||||
Agregar nota
|
Agregar nota
|
||||||
</button>
|
</button>
|
||||||
@@ -365,7 +365,7 @@ export default function PlanDetallePage() {
|
|||||||
{status === 'CANCELADO' && (
|
{status === 'CANCELADO' && (
|
||||||
<button
|
<button
|
||||||
onClick={() => void navigate('/')}
|
onClick={() => void navigate('/')}
|
||||||
className="border border-neutral-900 bg-transparent px-5 py-2.5 text-xs font-bold uppercase tracking-widest text-neutral-900 transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#374151] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Volver
|
Volver
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#1B4332" />
|
<meta name="theme-color" content="#2563EB" />
|
||||||
<link rel="manifest" href="/quote/manifest.webmanifest" />
|
<link rel="manifest" href="/quote/manifest.webmanifest" />
|
||||||
<link rel="apple-touch-icon" href="/quote/icons/icon-192.png" />
|
<link rel="apple-touch-icon" href="/quote/icons/icon-192.png" />
|
||||||
<title>FRASA Desarrollos — Cotizador</title>
|
<title>FRASA Desarrollos — Cotizador</title>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"scope": "/quote/",
|
"scope": "/quote/",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
"theme_color": "#1B4332",
|
"theme_color": "#2563EB",
|
||||||
"icons": [
|
"icons": [
|
||||||
{ "src": "/quote/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
{ "src": "/quote/icons/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||||||
{ "src": "/quote/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
{ "src": "/quote/icons/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white text-neutral-900">
|
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
|
||||||
<div className="border-b border-neutral-200">
|
<div className="border-b border-[#E5E7EB] bg-white">
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="text-xs font-medium text-[#64748B]">
|
||||||
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setModalPassword(true)}
|
onClick={() => setModalPassword(true)}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:text-neutral-900"
|
className="text-xs font-medium text-[#64748B] transition-colors duration-100 hover:text-[#111827]"
|
||||||
>
|
>
|
||||||
Contraseña
|
Contraseña
|
||||||
</button>
|
</button>
|
||||||
@@ -37,7 +37,7 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
logout()
|
logout()
|
||||||
setSesion(null)
|
setSesion(null)
|
||||||
}}
|
}}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:text-accent-hover"
|
className="text-xs font-medium text-accent transition-colors duration-100 hover:text-accent-hover"
|
||||||
>
|
>
|
||||||
Salir
|
Salir
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { validarConfirmacion, validarPasswordNueva } from '../lib/password'
|
|||||||
import { cambiarPassword } from '../services/auth'
|
import { cambiarPassword } from '../services/auth'
|
||||||
|
|
||||||
const CLASE_INPUT =
|
const CLASE_INPUT =
|
||||||
'w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none'
|
'w-full 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'
|
||||||
const CLASE_LABEL = 'mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500'
|
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
||||||
@@ -45,19 +45,19 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Cambiar contraseña"
|
aria-label="Cambiar contraseña"
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-sm border border-neutral-200 bg-white p-6">
|
<div className="w-full max-w-sm rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-xl">
|
||||||
<h2 className="mb-6 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h2 className="mb-6 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Cambiar contraseña
|
Cambiar contraseña
|
||||||
</h2>
|
</h2>
|
||||||
{exito ? (
|
{exito ? (
|
||||||
<>
|
<>
|
||||||
<p className="border border-accent px-4 py-3 text-xs font-bold uppercase tracking-widest text-accent">
|
<p className="rounded-lg bg-green-50 px-4 py-3 text-xs font-medium text-green-700">
|
||||||
Contraseña actualizada
|
Contraseña actualizada
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCerrar}
|
onClick={onCerrar}
|
||||||
className="mt-6 w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover"
|
className="mt-6 w-full 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"
|
||||||
>
|
>
|
||||||
Cerrar
|
Cerrar
|
||||||
</button>
|
</button>
|
||||||
@@ -92,7 +92,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
required
|
required
|
||||||
className={CLASE_INPUT}
|
className={CLASE_INPUT}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 text-xs text-neutral-400">Mínimo 8 caracteres y un número</p>
|
<p className="mt-2 text-xs text-[#64748B]">Mínimo 8 caracteres y un número</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="password-confirmar" className={CLASE_LABEL}>
|
<label htmlFor="password-confirmar" className={CLASE_LABEL}>
|
||||||
@@ -111,7 +111,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="rounded-lg bg-red-50 px-4 py-3 text-xs font-medium text-red-700"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@@ -120,14 +120,14 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="flex-1 bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="flex-1 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:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Guardando…' : 'Guardar'}
|
{cargando ? 'Guardando…' : 'Guardar'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCerrar}
|
onClick={onCerrar}
|
||||||
className="border border-neutral-300 px-5 py-3 text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:border-neutral-500"
|
className="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]"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export function ComparadorPropuestas({ precioLista, apartado, propuestas, titulo
|
|||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<h2
|
<h2
|
||||||
id="comparador-titulo"
|
id="comparador-titulo"
|
||||||
className="text-xs font-bold tracking-widest text-neutral-500 uppercase"
|
className="text-xs font-semibold uppercase tracking-wider text-[#64748B]"
|
||||||
>
|
>
|
||||||
Comparar propuestas
|
Comparar propuestas
|
||||||
</h2>
|
</h2>
|
||||||
@@ -85,18 +85,18 @@ export function ComparadorPropuestas({ precioLista, apartado, propuestas, titulo
|
|||||||
aria-expanded={abierto}
|
aria-expanded={abierto}
|
||||||
aria-controls="comparador-contenido"
|
aria-controls="comparador-contenido"
|
||||||
onClick={() => setAbierto((v) => !v)}
|
onClick={() => setAbierto((v) => !v)}
|
||||||
className="border border-neutral-900 bg-white px-5 py-2.5 text-xs font-bold tracking-widest text-neutral-900 uppercase transition-colors duration-150 hover:bg-neutral-100"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-4 py-2 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
{abierto ? 'Ocultar comparación' : 'Comparar propuestas'}
|
{abierto ? 'Ocultar comparación' : 'Comparar propuestas'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{abierto && (
|
{abierto && (
|
||||||
<div id="comparador-contenido" className="mt-6 border border-neutral-200 bg-white p-6">
|
<div id="comparador-contenido" className="mt-6 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full min-w-[720px] border-collapse text-sm">
|
<table className="w-full min-w-[720px] border-collapse text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr className="bg-[#F8FAFC]">
|
||||||
<th scope="col" className="w-48 p-3 text-left align-bottom">
|
<th scope="col" className="w-48 p-3 text-left align-bottom">
|
||||||
<span className="sr-only">Concepto</span>
|
<span className="sr-only">Concepto</span>
|
||||||
</th>
|
</th>
|
||||||
@@ -106,11 +106,11 @@ export function ComparadorPropuestas({ precioLista, apartado, propuestas, titulo
|
|||||||
scope="col"
|
scope="col"
|
||||||
className={`p-3 text-right align-bottom ${bordeColumna(i, 'inicio')}`}
|
className={`p-3 text-right align-bottom ${bordeColumna(i, 'inicio')}`}
|
||||||
>
|
>
|
||||||
<span className="block text-xs font-bold tracking-widest text-neutral-900 uppercase">
|
<span className="block text-xs font-semibold uppercase tracking-wider text-[#111827]">
|
||||||
{titulos[i] ?? p.tipo}
|
{titulos[i] ?? p.tipo}
|
||||||
</span>
|
</span>
|
||||||
{i === ganador && (
|
{i === ganador && (
|
||||||
<span className="mt-2 inline-block border border-accent px-2 py-0.5 text-xs font-bold tracking-widest text-accent uppercase">
|
<span className="mt-2 inline-block rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-blue-700">
|
||||||
Menor total
|
Menor total
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -120,20 +120,20 @@ export function ComparadorPropuestas({ precioLista, apartado, propuestas, titulo
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filas.map((fila, indiceFila) => (
|
{filas.map((fila, indiceFila) => (
|
||||||
<tr key={fila.etiqueta}>
|
<tr key={fila.etiqueta} className="hover:bg-[#F8FAFC]">
|
||||||
<th
|
<th
|
||||||
scope="row"
|
scope="row"
|
||||||
className="border-t border-neutral-200 p-3 text-left text-xs font-bold tracking-widest text-neutral-500 uppercase"
|
className="border-t border-[#E5E7EB] p-3 text-left text-xs font-medium text-[#64748B]"
|
||||||
>
|
>
|
||||||
{fila.etiqueta}
|
{fila.etiqueta}
|
||||||
</th>
|
</th>
|
||||||
{propuestas.map((p, i) => (
|
{propuestas.map((p, i) => (
|
||||||
<td
|
<td
|
||||||
key={p.tipo}
|
key={p.tipo}
|
||||||
className={`border-t border-neutral-200 p-3 text-right whitespace-nowrap ${
|
className={`border-t border-[#E5E7EB] p-3 text-right whitespace-nowrap ${
|
||||||
fila.destacada
|
fila.destacada
|
||||||
? `font-bold ${i === ganador ? 'text-accent' : 'text-neutral-900'}`
|
? `font-semibold ${i === ganador ? 'text-accent' : 'text-[#111827]'}`
|
||||||
: 'font-light text-neutral-900'
|
: 'text-[#111827]'
|
||||||
} ${bordeColumna(
|
} ${bordeColumna(
|
||||||
i,
|
i,
|
||||||
indiceFila === filas.length - 1 ? 'fin' : 'media',
|
indiceFila === filas.length - 1 ? 'fin' : 'media',
|
||||||
@@ -147,7 +147,7 @@ export function ComparadorPropuestas({ precioLista, apartado, propuestas, titulo
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-6 text-xs text-neutral-400">
|
<p className="mt-6 text-xs text-[#64748B]">
|
||||||
El enganche ya incluye la deducción del apartado. El total pagado suma apartado +
|
El enganche ya incluye la deducción del apartado. El total pagado suma apartado +
|
||||||
enganche + mensualidades + pago final.
|
enganche + mensualidades + pago final.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -71,14 +71,14 @@ export function ComparadorUnidades({ unidades, tipo, apartado, acabados }: Props
|
|||||||
|
|
||||||
const selector = (id: string, valor: string, onChange: (v: string) => void, etiqueta: string) => (
|
const selector = (id: string, valor: string, onChange: (v: string) => void, etiqueta: string) => (
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor={id} className="mb-2 block text-xs font-bold tracking-widest text-neutral-500 uppercase">
|
<label htmlFor={id} className="mb-2 block text-xs font-medium text-[#4B5563]">
|
||||||
{etiqueta}
|
{etiqueta}
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id={id}
|
id={id}
|
||||||
value={valor}
|
value={valor}
|
||||||
onChange={(e) => onChange(e.target.value)}
|
onChange={(e) => onChange(e.target.value)}
|
||||||
className="w-full border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none"
|
className="w-full 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"
|
||||||
>
|
>
|
||||||
<option value="">Selecciona una unidad…</option>
|
<option value="">Selecciona una unidad…</option>
|
||||||
{LISTA_DESARROLLOS.map((dev) => {
|
{LISTA_DESARROLLOS.map((dev) => {
|
||||||
@@ -103,7 +103,7 @@ export function ComparadorUnidades({ unidades, tipo, apartado, acabados }: Props
|
|||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<h2
|
<h2
|
||||||
id="comparador-unidades-titulo"
|
id="comparador-unidades-titulo"
|
||||||
className="text-xs font-bold tracking-widest text-neutral-500 uppercase"
|
className="text-xs font-semibold uppercase tracking-wider text-[#64748B]"
|
||||||
>
|
>
|
||||||
Comparar unidades
|
Comparar unidades
|
||||||
</h2>
|
</h2>
|
||||||
@@ -112,28 +112,28 @@ export function ComparadorUnidades({ unidades, tipo, apartado, acabados }: Props
|
|||||||
aria-expanded={abierto}
|
aria-expanded={abierto}
|
||||||
aria-controls="comparador-unidades-contenido"
|
aria-controls="comparador-unidades-contenido"
|
||||||
onClick={() => setAbierto((v) => !v)}
|
onClick={() => setAbierto((v) => !v)}
|
||||||
className="border border-neutral-900 bg-white px-5 py-2.5 text-xs font-bold tracking-widest text-neutral-900 uppercase transition-colors duration-150 hover:bg-neutral-100"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-4 py-2 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
{abierto ? 'Ocultar comparación' : 'Comparar 2 unidades'}
|
{abierto ? 'Ocultar comparación' : 'Comparar 2 unidades'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{abierto && (
|
{abierto && (
|
||||||
<div id="comparador-unidades-contenido" className="mt-6 border border-neutral-200 bg-white p-6">
|
<div id="comparador-unidades-contenido" className="mt-6 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
{selector('comparador-a', codigoA, setCodigoA, 'Unidad A')}
|
{selector('comparador-a', codigoA, setCodigoA, 'Unidad A')}
|
||||||
{selector('comparador-b', codigoB, setCodigoB, 'Unidad B')}
|
{selector('comparador-b', codigoB, setCodigoB, 'Unidad B')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!comparacion ? (
|
{!comparacion ? (
|
||||||
<p className="mt-6 text-sm text-neutral-500">
|
<p className="mt-6 text-sm text-[#64748B]">
|
||||||
Selecciona dos unidades para ver la comparación lado a lado.
|
Selecciona dos unidades para ver la comparación lado a lado.
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-6 overflow-x-auto">
|
<div className="mt-6 overflow-x-auto">
|
||||||
<table className="w-full min-w-[480px] border-collapse text-sm">
|
<table className="w-full min-w-[480px] border-collapse text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr className="bg-[#F8FAFC]">
|
||||||
<th scope="col" className="w-48 p-3 text-left">
|
<th scope="col" className="w-48 p-3 text-left">
|
||||||
<span className="sr-only">Concepto</span>
|
<span className="sr-only">Concepto</span>
|
||||||
</th>
|
</th>
|
||||||
@@ -141,7 +141,7 @@ export function ComparadorUnidades({ unidades, tipo, apartado, acabados }: Props
|
|||||||
<th
|
<th
|
||||||
key={`${c.desarrolloId}|${c.unidad.codigo}`}
|
key={`${c.desarrolloId}|${c.unidad.codigo}`}
|
||||||
scope="col"
|
scope="col"
|
||||||
className="p-3 text-right text-xs font-bold tracking-widest text-neutral-900 uppercase"
|
className="p-3 text-right text-xs font-semibold uppercase tracking-wider text-[#111827]"
|
||||||
>
|
>
|
||||||
{c.unidad.codigo}
|
{c.unidad.codigo}
|
||||||
</th>
|
</th>
|
||||||
@@ -150,18 +150,18 @@ export function ComparadorUnidades({ unidades, tipo, apartado, acabados }: Props
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filas.map((fila) => (
|
{filas.map((fila) => (
|
||||||
<tr key={fila.etiqueta}>
|
<tr key={fila.etiqueta} className="hover:bg-[#F8FAFC]">
|
||||||
<th
|
<th
|
||||||
scope="row"
|
scope="row"
|
||||||
className="border-t border-neutral-200 p-3 text-left text-xs font-bold tracking-widest text-neutral-500 uppercase"
|
className="border-t border-[#E5E7EB] p-3 text-left text-xs font-medium text-[#64748B]"
|
||||||
>
|
>
|
||||||
{fila.etiqueta}
|
{fila.etiqueta}
|
||||||
</th>
|
</th>
|
||||||
{[comparacion.a, comparacion.b].map((c) => (
|
{[comparacion.a, comparacion.b].map((c) => (
|
||||||
<td
|
<td
|
||||||
key={`${c.desarrolloId}|${c.unidad.codigo}`}
|
key={`${c.desarrolloId}|${c.unidad.codigo}`}
|
||||||
className={`border-t border-neutral-200 p-3 text-right whitespace-nowrap ${
|
className={`border-t border-[#E5E7EB] p-3 text-right whitespace-nowrap ${
|
||||||
fila.destacada ? 'font-bold text-accent' : 'font-light text-neutral-900'
|
fila.destacada ? 'font-semibold text-accent' : 'text-[#111827]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{fila.obtener(c)}
|
{fila.obtener(c)}
|
||||||
@@ -171,7 +171,7 @@ export function ComparadorUnidades({ unidades, tipo, apartado, acabados }: Props
|
|||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<p className="mt-6 text-xs text-neutral-400">
|
<p className="mt-6 text-xs text-[#64748B]">
|
||||||
Cada unidad se calcula con los parámetros de su desarrollo y el mismo apartado.
|
Cada unidad se calcula con los parámetros de su desarrollo y el mismo apartado.
|
||||||
El enganche ya incluye la deducción del apartado.
|
El enganche ya incluye la deducción del apartado.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, type FormEvent } from 'react'
|
import { useState, type FormEvent } from 'react'
|
||||||
import { login, ErrorLogin, type UsuarioSesion } from '../services/auth'
|
import { login, ErrorLogin, type UsuarioSesion } from '../services/auth'
|
||||||
|
|
||||||
/** Pantalla de login StudioBlank: marca FRASA, usuario + contraseña, Enter envía */
|
/** Pantalla de login NovaSpark: marca FRASA, usuario + contraseña, Enter envía */
|
||||||
export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSesion) => void }) {
|
export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSesion) => void }) {
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
@@ -22,11 +22,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-white px-4">
|
<div className="flex min-h-screen items-center justify-center bg-[#F8FAFC] px-4">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm">
|
||||||
<div className="mb-10 text-center">
|
<div className="mb-8 text-center">
|
||||||
<p className="text-lg font-bold tracking-widest">FRASA</p>
|
<p className="text-lg font-bold tracking-wide">
|
||||||
<p className="mt-2 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
FRASA<span className="text-[#F97316]">.</span>
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-xs font-medium text-[#64748B]">
|
||||||
Acceso al ecosistema
|
Acceso al ecosistema
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +36,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="login-usuario"
|
htmlFor="login-usuario"
|
||||||
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Usuario
|
Usuario
|
||||||
</label>
|
</label>
|
||||||
@@ -46,13 +48,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className="w-full 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="login-password"
|
htmlFor="login-password"
|
||||||
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Contraseña
|
Contraseña
|
||||||
</label>
|
</label>
|
||||||
@@ -63,13 +65,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className="w-full 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="rounded-lg bg-red-50 px-4 py-3 text-xs font-medium text-red-700"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@@ -77,7 +79,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="w-full 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:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Entrando…' : 'Entrar'}
|
{cargando ? 'Entrando…' : 'Entrar'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export function MisCotizaciones({ cotizaciones, cargando, error, onRetomar }: Pr
|
|||||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<h2
|
<h2
|
||||||
id="mis-cotizaciones-titulo"
|
id="mis-cotizaciones-titulo"
|
||||||
className="text-xs font-bold tracking-widest text-neutral-500 uppercase"
|
className="text-xs font-semibold uppercase tracking-wider text-[#64748B]"
|
||||||
>
|
>
|
||||||
Mis cotizaciones
|
Mis cotizaciones
|
||||||
</h2>
|
</h2>
|
||||||
@@ -45,18 +45,18 @@ export function MisCotizaciones({ cotizaciones, cargando, error, onRetomar }: Pr
|
|||||||
aria-expanded={abierto}
|
aria-expanded={abierto}
|
||||||
aria-controls="mis-cotizaciones-contenido"
|
aria-controls="mis-cotizaciones-contenido"
|
||||||
onClick={() => setAbierto((v) => !v)}
|
onClick={() => setAbierto((v) => !v)}
|
||||||
className="border border-neutral-900 bg-white px-5 py-2.5 text-xs font-bold tracking-widest text-neutral-900 uppercase transition-colors duration-150 hover:bg-neutral-100"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-4 py-2 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
{abierto ? 'Ocultar historial' : `Ver historial (${cotizaciones.length})`}
|
{abierto ? 'Ocultar historial' : `Ver historial (${cotizaciones.length})`}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{abierto && (
|
{abierto && (
|
||||||
<div id="mis-cotizaciones-contenido" className="mt-6 border border-neutral-200 bg-white p-6">
|
<div id="mis-cotizaciones-contenido" className="mt-6 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||||
{cargando && <p className="text-sm text-neutral-500">Cargando historial…</p>}
|
{cargando && <p className="text-sm text-[#64748B]">Cargando historial…</p>}
|
||||||
{error && <p className="text-xs text-red-700">{error}</p>}
|
{error && <p className="text-xs text-red-700">{error}</p>}
|
||||||
{!cargando && !error && cotizaciones.length === 0 && (
|
{!cargando && !error && cotizaciones.length === 0 && (
|
||||||
<p className="text-sm text-neutral-500">
|
<p className="text-sm text-[#64748B]">
|
||||||
Aún no tienes cotizaciones guardadas. Se guardan al generar el PDF.
|
Aún no tienes cotizaciones guardadas. Se guardan al generar el PDF.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -64,13 +64,13 @@ export function MisCotizaciones({ cotizaciones, cargando, error, onRetomar }: Pr
|
|||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
<table className="w-full min-w-[640px] border-collapse text-sm">
|
<table className="w-full min-w-[640px] border-collapse text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr className="bg-[#F8FAFC]">
|
||||||
{['Fecha', 'Folio', 'Cliente', 'Unidad', 'Propuesta', 'Precio final', ''].map(
|
{['Fecha', 'Folio', 'Cliente', 'Unidad', 'Propuesta', 'Precio final', ''].map(
|
||||||
(h) => (
|
(h) => (
|
||||||
<th
|
<th
|
||||||
key={h}
|
key={h}
|
||||||
scope="col"
|
scope="col"
|
||||||
className="p-3 text-left text-xs font-bold tracking-widest text-neutral-500 uppercase"
|
className="p-3 text-left text-xs font-semibold uppercase tracking-wider text-[#64748B]"
|
||||||
>
|
>
|
||||||
{h}
|
{h}
|
||||||
</th>
|
</th>
|
||||||
@@ -80,36 +80,36 @@ export function MisCotizaciones({ cotizaciones, cargando, error, onRetomar }: Pr
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{cotizaciones.map((c) => (
|
{cotizaciones.map((c) => (
|
||||||
<tr key={c.id}>
|
<tr key={c.id} className="hover:bg-[#F8FAFC]">
|
||||||
<td className="border-t border-neutral-200 p-3 whitespace-nowrap">
|
<td className="border-t border-[#E5E7EB] p-3 whitespace-nowrap">
|
||||||
{formatoFechaHora(c.createdAt)}
|
{formatoFechaHora(c.createdAt)}
|
||||||
</td>
|
</td>
|
||||||
<td className="border-t border-neutral-200 p-3 whitespace-nowrap">
|
<td className="border-t border-[#E5E7EB] p-3 whitespace-nowrap">
|
||||||
{c.folio ?? '—'}
|
{c.folio ?? '—'}
|
||||||
</td>
|
</td>
|
||||||
<td className="border-t border-neutral-200 p-3">{c.clienteNombre}</td>
|
<td className="border-t border-[#E5E7EB] p-3">{c.clienteNombre}</td>
|
||||||
<td className="border-t border-neutral-200 p-3 whitespace-nowrap">
|
<td className="border-t border-[#E5E7EB] p-3 whitespace-nowrap">
|
||||||
{c.unidadCodigo}
|
{c.unidadCodigo}
|
||||||
<span className="ml-1 text-xs text-neutral-400">{c.desarrollo}</span>
|
<span className="ml-1 text-xs text-[#64748B]">{c.desarrollo}</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="border-t border-neutral-200 p-3 whitespace-nowrap">
|
<td className="border-t border-[#E5E7EB] p-3 whitespace-nowrap">
|
||||||
{TITULO_PROPUESTA[c.tipoPropuesta] ?? c.tipoPropuesta}
|
{TITULO_PROPUESTA[c.tipoPropuesta] ?? c.tipoPropuesta}
|
||||||
</td>
|
</td>
|
||||||
<td className="border-t border-neutral-200 p-3 text-right whitespace-nowrap">
|
<td className="border-t border-[#E5E7EB] p-3 text-right whitespace-nowrap">
|
||||||
{formatoMoneda(c.precioFinal)}
|
{formatoMoneda(c.precioFinal)}
|
||||||
</td>
|
</td>
|
||||||
<td className="border-t border-neutral-200 p-3 text-right">
|
<td className="border-t border-[#E5E7EB] p-3 text-right">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onRetomar(c)}
|
onClick={() => onRetomar(c)}
|
||||||
className="border border-accent bg-white px-3 py-1.5 text-xs font-bold tracking-widest text-accent uppercase transition-colors duration-150 hover:bg-accent hover:text-white"
|
className="rounded-lg border border-accent bg-white px-3 py-1.5 text-xs font-medium text-accent transition-all duration-100 ease-out hover:bg-accent hover:text-white"
|
||||||
>
|
>
|
||||||
Retomar
|
Retomar
|
||||||
</button>
|
</button>
|
||||||
<a
|
<a
|
||||||
href={`/client/?cotizacion=${c.id}`}
|
href={`/client/?cotizacion=${c.id}`}
|
||||||
className="border border-neutral-900 bg-white px-3 py-1.5 text-xs font-bold tracking-widest text-neutral-900 uppercase transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-[#374151] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Iniciar alta
|
Iniciar alta
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const campoInput =
|
const campoInput =
|
||||||
'w-full border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none'
|
'w-full 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'
|
||||||
const etiquetaCampo = 'mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500'
|
const etiquetaCampo = 'mb-2 block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal sencillo para pedir los datos del cliente cuando se va a
|
* Modal sencillo para pedir los datos del cliente cuando se va a
|
||||||
@@ -55,14 +55,14 @@ export function ModalCliente({ abierto, nombreInicial = '', telefonoInicial = ''
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="modal-cliente-titulo"
|
aria-labelledby="modal-cliente-titulo"
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-md border border-neutral-200 bg-white p-6">
|
<div className="w-full max-w-md rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-xl">
|
||||||
<h2
|
<h2
|
||||||
id="modal-cliente-titulo"
|
id="modal-cliente-titulo"
|
||||||
className="text-xs font-bold tracking-widest text-neutral-500 uppercase"
|
className="text-xs font-semibold uppercase tracking-wider text-[#64748B]"
|
||||||
>
|
>
|
||||||
Datos del cliente
|
Datos del cliente
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-2 text-sm text-neutral-600">
|
<p className="mt-2 text-sm text-[#4B5563]">
|
||||||
Se necesitan para guardar la cotización y generar el PDF.
|
Se necesitan para guardar la cotización y generar el PDF.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -113,14 +113,14 @@ export function ModalCliente({ abierto, nombreInicial = '', telefonoInicial = ''
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCancelar}
|
onClick={onCancelar}
|
||||||
className="border border-neutral-300 bg-white px-5 py-2.5 text-xs font-bold tracking-widest text-neutral-600 uppercase transition-colors duration-150 hover:bg-neutral-100"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-4 py-2 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={aceptar}
|
onClick={aceptar}
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold tracking-widest text-white uppercase transition-colors duration-150 hover:bg-accent-hover"
|
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"
|
||||||
>
|
>
|
||||||
Continuar
|
Continuar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -11,15 +11,15 @@ interface Props {
|
|||||||
function Renglon({ etiqueta, valor, nota }: { etiqueta: string; valor: string; nota?: string }) {
|
function Renglon({ etiqueta, valor, nota }: { etiqueta: string; valor: string; nota?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-baseline justify-between gap-2 py-2">
|
<div className="flex items-baseline justify-between gap-2 py-2">
|
||||||
<span className="text-xs font-bold tracking-widest text-neutral-500 uppercase">
|
<span className="text-xs font-medium text-[#64748B]">
|
||||||
{etiqueta}
|
{etiqueta}
|
||||||
{nota && (
|
{nota && (
|
||||||
<span className="mt-0.5 block text-xs font-normal tracking-normal text-neutral-400 normal-case">
|
<span className="mt-0.5 block text-xs font-normal text-[#64748B]">
|
||||||
{nota}
|
{nota}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-lg font-light text-neutral-900">{valor}</span>
|
<span className="text-lg font-medium text-[#111827]">{valor}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -29,18 +29,18 @@ export function ProposalCard({ numero, titulo, propuesta, onDescuentoChange }: P
|
|||||||
const porcentaje = propuesta.descuento * 100
|
const porcentaje = propuesta.descuento * 100
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="flex flex-col border border-neutral-200 bg-white p-6">
|
<article className="flex flex-col rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm transition-all duration-200 hover:shadow-md">
|
||||||
<header className="mb-6 border-b border-neutral-200 pb-4">
|
<header className="mb-6 border-b border-[#E5E7EB] pb-4">
|
||||||
<p className="text-xs font-bold tracking-widest text-accent uppercase">
|
<p className="text-xs font-semibold uppercase tracking-wider text-accent">
|
||||||
Propuesta comercial #{numero}
|
Propuesta comercial #{numero}
|
||||||
</p>
|
</p>
|
||||||
<h2 className="mt-1 text-xl font-light text-neutral-900">{titulo}</h2>
|
<h2 className="mt-1 text-xl font-semibold text-[#111827]">{titulo}</h2>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<label
|
<label
|
||||||
htmlFor={`descuento-${propuesta.tipo}`}
|
htmlFor={`descuento-${propuesta.tipo}`}
|
||||||
className="mb-2 block text-xs font-bold tracking-widest text-neutral-500 uppercase"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Descuento (%)
|
Descuento (%)
|
||||||
</label>
|
</label>
|
||||||
@@ -55,11 +55,11 @@ export function ProposalCard({ numero, titulo, propuesta, onDescuentoChange }: P
|
|||||||
const v = Number(e.target.value)
|
const v = Number(e.target.value)
|
||||||
onDescuentoChange(Number.isFinite(v) ? Math.min(100, Math.max(0, v)) / 100 : 0)
|
onDescuentoChange(Number.isFinite(v) ? Math.min(100, Math.max(0, v)) / 100 : 0)
|
||||||
}}
|
}}
|
||||||
className="w-28 border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none"
|
className="w-28 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"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divide-y divide-neutral-200">
|
<div className="divide-y divide-[#E5E7EB]">
|
||||||
<Renglon etiqueta="Monto de descuento" valor={formatoMoneda(propuesta.montoDescuento)} />
|
<Renglon etiqueta="Monto de descuento" valor={formatoMoneda(propuesta.montoDescuento)} />
|
||||||
<Renglon etiqueta="Precio final c/desc." valor={formatoMoneda(propuesta.precioFinal)} />
|
<Renglon etiqueta="Precio final c/desc." valor={formatoMoneda(propuesta.precioFinal)} />
|
||||||
<Renglon
|
<Renglon
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ interface Props {
|
|||||||
function Fila({ etiqueta, valor }: { etiqueta: string; valor: string }) {
|
function Fila({ etiqueta, valor }: { etiqueta: string; valor: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between py-2">
|
<div className="flex items-center justify-between py-2">
|
||||||
<dt className="text-sm text-neutral-500">{etiqueta}</dt>
|
<dt className="text-sm text-[#64748B]">{etiqueta}</dt>
|
||||||
<dd className="text-sm font-medium text-neutral-900">{valor}</dd>
|
<dd className="text-sm font-medium text-[#111827]">{valor}</dd>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -33,16 +33,18 @@ export function UnitInfoCard({ unidad, numMeses }: Props) {
|
|||||||
const extras = unidad.extras ?? {}
|
const extras = unidad.extras ?? {}
|
||||||
const etiquetaStatus = ETIQUETA_STATUS[unidad.status]
|
const etiquetaStatus = ETIQUETA_STATUS[unidad.status]
|
||||||
return (
|
return (
|
||||||
<section className="border border-neutral-200 bg-white p-6">
|
<section className="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||||
<h2 className="mb-4 text-xs font-bold tracking-widest text-neutral-900 uppercase">
|
<h2 className="mb-4 text-xs font-semibold uppercase tracking-wider text-[#111827]">
|
||||||
Unidad {unidad.codigo}
|
Unidad {unidad.codigo}
|
||||||
</h2>
|
</h2>
|
||||||
{etiquetaStatus !== undefined && unidad.etapa != null && unidad.etapa !== '' && (
|
{etiquetaStatus !== undefined && unidad.etapa != null && unidad.etapa !== '' && (
|
||||||
<p className="mb-4 text-xs text-neutral-500">
|
<p className="mb-4">
|
||||||
|
<span className="rounded-full bg-amber-50 px-2.5 py-0.5 text-xs font-medium text-amber-700">
|
||||||
{etiquetaStatus} — en {unidad.etapa.toLowerCase()}
|
{etiquetaStatus} — en {unidad.etapa.toLowerCase()}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<dl className="divide-y divide-neutral-200">
|
<dl className="divide-y divide-[#E5E7EB]">
|
||||||
<Fila etiqueta="Modelo" valor={unidad.modelo} />
|
<Fila etiqueta="Modelo" valor={unidad.modelo} />
|
||||||
{extras['prototipo'] != null && <Fila etiqueta="Prototipo" valor={String(extras['prototipo'])} />}
|
{extras['prototipo'] != null && <Fila etiqueta="Prototipo" valor={String(extras['prototipo'])} />}
|
||||||
{extras['etapa'] != null && <Fila etiqueta="Etapa" valor={String(extras['etapa'])} />}
|
{extras['etapa'] != null && <Fila etiqueta="Etapa" valor={String(extras['etapa'])} />}
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ export function UnitSelector({ unidades, valor, onChange }: Props) {
|
|||||||
<div ref={contenedorRef} className="relative">
|
<div ref={contenedorRef} className="relative">
|
||||||
<label
|
<label
|
||||||
htmlFor="unidad"
|
htmlFor="unidad"
|
||||||
className="mb-2 block text-xs font-bold tracking-widest text-neutral-500 uppercase"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Unidad
|
Unidad
|
||||||
</label>
|
</label>
|
||||||
@@ -100,17 +100,17 @@ export function UnitSelector({ unidades, valor, onChange }: Props) {
|
|||||||
}}
|
}}
|
||||||
onFocus={() => setAbierto(true)}
|
onFocus={() => setAbierto(true)}
|
||||||
onKeyDown={onKeyDown}
|
onKeyDown={onKeyDown}
|
||||||
className="w-full border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none"
|
className="w-full 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"
|
||||||
/>
|
/>
|
||||||
{abierto && (
|
{abierto && (
|
||||||
<ul
|
<ul
|
||||||
id="unidad-listbox"
|
id="unidad-listbox"
|
||||||
role="listbox"
|
role="listbox"
|
||||||
aria-label="Unidades disponibles"
|
aria-label="Unidades disponibles"
|
||||||
className="absolute z-10 mt-1 max-h-64 w-full overflow-auto border border-neutral-300 bg-white"
|
className="absolute z-10 mt-1 max-h-64 w-full overflow-auto rounded-lg border border-[#E5E7EB] bg-white shadow-xl"
|
||||||
>
|
>
|
||||||
{opciones.length === 0 ? (
|
{opciones.length === 0 ? (
|
||||||
<li className="px-3 py-2.5 text-sm text-neutral-500">Sin unidades que coincidan</li>
|
<li className="px-3 py-2.5 text-sm text-[#64748B]">Sin unidades que coincidan</li>
|
||||||
) : (
|
) : (
|
||||||
opciones.map((u, i) => (
|
opciones.map((u, i) => (
|
||||||
<li
|
<li
|
||||||
@@ -127,12 +127,12 @@ export function UnitSelector({ unidades, valor, onChange }: Props) {
|
|||||||
}}
|
}}
|
||||||
onMouseEnter={() => setIndiceActivo(i)}
|
onMouseEnter={() => setIndiceActivo(i)}
|
||||||
className={`cursor-pointer px-3 py-2.5 text-sm ${
|
className={`cursor-pointer px-3 py-2.5 text-sm ${
|
||||||
i === indiceActivo ? 'bg-neutral-100' : 'bg-white'
|
i === indiceActivo ? 'bg-[#F8FAFC]' : 'bg-white'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="font-medium text-neutral-900">{u.codigo}</span>
|
<span className="font-medium text-[#111827]">{u.codigo}</span>
|
||||||
<span className="text-neutral-500"> — Modelo {u.modelo} — </span>
|
<span className="text-[#64748B]"> — Modelo {u.modelo} — </span>
|
||||||
<span className="text-neutral-900">{formatoMoneda(u.precioSinAcabados)}</span>
|
<span className="text-[#111827]">{formatoMoneda(u.precioSinAcabados)}</span>
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,25 +2,18 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
--color-accent: #1B4332;
|
--color-accent: #2563EB;
|
||||||
--color-accent-hover: #132f23;
|
--color-accent-hover: #1D4ED8;
|
||||||
--radius-xs: 0;
|
--color-naranja: #F97316;
|
||||||
--radius-sm: 0;
|
|
||||||
--radius-md: 0;
|
|
||||||
--radius-lg: 0;
|
|
||||||
--radius-xl: 0;
|
|
||||||
--radius-2xl: 0;
|
|
||||||
--radius-3xl: 0;
|
|
||||||
--radius-4xl: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply bg-white text-neutral-900 antialiased;
|
@apply bg-[#F8FAFC] text-[#111827] antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline: 2px solid #1B4332;
|
outline: none;
|
||||||
outline-offset: 2px;
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ export function crearDocumentoPdf({ unidad, acabados, apartado, precioLista, pro
|
|||||||
startY: y,
|
startY: y,
|
||||||
margin: { left: MARGEN, right: MARGEN },
|
margin: { left: MARGEN, right: MARGEN },
|
||||||
styles: { fontSize: 8 },
|
styles: { fontSize: 8 },
|
||||||
headStyles: { fillColor: [4, 120, 87] },
|
headStyles: { fillColor: [37, 99, 235] },
|
||||||
head: [[`PROPUESTA COMERCIAL #${i + 1} — ${p.titulo.toUpperCase()}`, '%', 'PAGOS MENSUALES', 'TOTAL']],
|
head: [[`PROPUESTA COMERCIAL #${i + 1} — ${p.titulo.toUpperCase()}`, '%', 'PAGOS MENSUALES', 'TOTAL']],
|
||||||
body: [
|
body: [
|
||||||
['DESCUENTO', formatoPorcentaje(p.descuento), '', ''],
|
['DESCUENTO', formatoPorcentaje(p.descuento), '', ''],
|
||||||
|
|||||||
@@ -39,11 +39,11 @@ const esquemaCliente = z.object({
|
|||||||
type FormularioCliente = z.infer<typeof esquemaCliente>
|
type FormularioCliente = z.infer<typeof esquemaCliente>
|
||||||
|
|
||||||
const campoInputBase =
|
const campoInputBase =
|
||||||
'w-full border bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none'
|
'w-full rounded-lg border bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:ring-2 focus:ring-accent/20 focus:outline-none'
|
||||||
const campoInput = `${campoInputBase} border-neutral-300`
|
const campoInput = `${campoInputBase} border-[#D1D5DB]`
|
||||||
const campoInputError = `${campoInputBase} border-red-400`
|
const campoInputError = `${campoInputBase} border-red-400`
|
||||||
|
|
||||||
const etiquetaCampo = 'mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500'
|
const etiquetaCampo = 'mb-2 block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
export default function CotizadorPage() {
|
export default function CotizadorPage() {
|
||||||
const { unidades, cargando, error, usandoSeed } = useUnidades()
|
const { unidades, cargando, error, usandoSeed } = useUnidades()
|
||||||
@@ -254,18 +254,18 @@ export default function CotizadorPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white">
|
<div className="min-h-screen bg-[#F8FAFC]">
|
||||||
<header className="border-b border-neutral-200 bg-white">
|
<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">
|
<div className="mx-auto flex max-w-7xl flex-wrap items-baseline justify-between gap-2 px-4 py-6">
|
||||||
<h1 className="text-lg tracking-widest text-neutral-900">
|
<h1 className="text-lg text-[#111827]">
|
||||||
<span className="font-bold">FRASA DESARROLLOS</span>{' '}
|
<span className="font-bold">FRASA DESARROLLOS<span className="text-[#F97316]">.</span></span>{' '}
|
||||||
<span className="font-light text-neutral-500">— Cotizador</span>
|
<span className="font-normal text-[#64748B]">— Cotizador</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<p className="text-xs font-bold tracking-widest text-neutral-500 uppercase capitalize">
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B] capitalize">
|
||||||
{formatoFechaLarga(new Date())}
|
{formatoFechaLarga(new Date())}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-xs text-neutral-500">
|
<p className="mt-1 text-xs text-[#64748B]">
|
||||||
Vigencia: hasta el {formatoFechaLarga(calcularVigencia())}
|
Vigencia: hasta el {formatoFechaLarga(calcularVigencia())}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -275,18 +275,18 @@ export default function CotizadorPage() {
|
|||||||
<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]">
|
<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 */}
|
{/* Panel de configuración */}
|
||||||
<aside className="space-y-16">
|
<aside className="space-y-16">
|
||||||
<section className="space-y-8 border border-neutral-200 bg-white p-6">
|
<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>}
|
{error && <p className="text-xs text-red-700">{error}</p>}
|
||||||
{usandoSeed && !cargando && (
|
{usandoSeed && !cargando && (
|
||||||
<p className="text-xs text-amber-700">Datos de demostración (sin conexión al servidor)</p>
|
<p className="text-xs text-amber-700">Datos de demostración (sin conexión al servidor)</p>
|
||||||
)}
|
)}
|
||||||
{cargando ? (
|
{cargando ? (
|
||||||
<p className="text-sm text-neutral-500">Cargando unidades…</p>
|
<p className="text-sm text-[#64748B]">Cargando unidades…</p>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<span className={etiquetaCampo}>Desarrollo</span>
|
<span className={etiquetaCampo}>Desarrollo</span>
|
||||||
<div className="flex flex-wrap border border-neutral-300" role="group">
|
<div className="flex flex-wrap overflow-hidden rounded-lg border border-[#D1D5DB]" role="group">
|
||||||
{LISTA_DESARROLLOS.map((id) => {
|
{LISTA_DESARROLLOS.map((id) => {
|
||||||
const activo = id === desarrollo
|
const activo = id === desarrollo
|
||||||
const total = unidades.filter((u) => (u.desarrollo ?? 'DIAGONAL') === id).length
|
const total = unidades.filter((u) => (u.desarrollo ?? 'DIAGONAL') === id).length
|
||||||
@@ -299,21 +299,21 @@ export default function CotizadorPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-pressed={activo}
|
aria-pressed={activo}
|
||||||
onClick={() => setDesarrollo(id)}
|
onClick={() => setDesarrollo(id)}
|
||||||
className={`px-4 py-2.5 text-xs font-bold tracking-widest uppercase transition-colors duration-150 ${
|
className={`px-4 py-2.5 text-xs font-medium transition-colors duration-100 ${
|
||||||
activo
|
activo
|
||||||
? 'bg-accent text-white'
|
? 'bg-accent text-white'
|
||||||
: 'bg-white text-neutral-600 hover:bg-neutral-100'
|
: 'bg-white text-[#4B5563] hover:bg-[#F9FAFB]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{obtenerDesarrollo(id).nombre}
|
{obtenerDesarrollo(id).nombre}
|
||||||
<span className={`ml-1 font-normal ${activo ? 'text-white' : 'text-neutral-400'}`}>
|
<span className={`ml-1 font-normal ${activo ? 'text-blue-100' : 'text-[#64748B]'}`}>
|
||||||
{disponibles}/{total}
|
{disponibles}/{total}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-xs text-neutral-500">
|
<p className="mt-2 text-xs text-[#64748B]">
|
||||||
{unidadesDesarrollo.length} unidades · {disponiblesDesarrollo} disponibles
|
{unidadesDesarrollo.length} unidades · {disponiblesDesarrollo} disponibles
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -330,7 +330,7 @@ export default function CotizadorPage() {
|
|||||||
{!params.precioUnico && (
|
{!params.precioUnico && (
|
||||||
<div>
|
<div>
|
||||||
<span className={etiquetaCampo}>Cocina y closet</span>
|
<span className={etiquetaCampo}>Cocina y closet</span>
|
||||||
<div className="inline-flex border border-neutral-300" role="group">
|
<div className="inline-flex overflow-hidden rounded-lg border border-[#D1D5DB]" role="group">
|
||||||
{(['SI', 'NO'] as const).map((opcion) => {
|
{(['SI', 'NO'] as const).map((opcion) => {
|
||||||
const activo = (opcion === 'SI') === acabados
|
const activo = (opcion === 'SI') === acabados
|
||||||
return (
|
return (
|
||||||
@@ -339,10 +339,10 @@ export default function CotizadorPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-pressed={activo}
|
aria-pressed={activo}
|
||||||
onClick={() => setAcabados(opcion === 'SI')}
|
onClick={() => setAcabados(opcion === 'SI')}
|
||||||
className={`px-5 py-2.5 text-xs font-bold tracking-widest uppercase transition-colors duration-150 ${
|
className={`px-5 py-2.5 text-xs font-medium transition-colors duration-100 ${
|
||||||
activo
|
activo
|
||||||
? 'bg-accent text-white'
|
? 'bg-accent text-white'
|
||||||
: 'bg-white text-neutral-600 hover:bg-neutral-100'
|
: 'bg-white text-[#4B5563] hover:bg-[#F9FAFB]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{opcion}
|
{opcion}
|
||||||
@@ -372,7 +372,7 @@ export default function CotizadorPage() {
|
|||||||
{errorApartado ? (
|
{errorApartado ? (
|
||||||
<p className="mt-1 text-xs text-red-700">{errorApartado}</p>
|
<p className="mt-1 text-xs text-red-700">{errorApartado}</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-1 text-xs text-neutral-500">{formatoMoneda(apartado)}</p>
|
<p className="mt-1 text-xs text-[#64748B]">{formatoMoneda(apartado)}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -418,11 +418,11 @@ export default function CotizadorPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{unidad && (
|
{unidad && (
|
||||||
<div className="border-t border-neutral-200 pt-8">
|
<div className="border-t border-[#E5E7EB] pt-8">
|
||||||
<p className="text-xs font-bold tracking-widest text-neutral-500 uppercase">
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Precio de lista
|
Precio de lista
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-3xl font-light text-neutral-900">
|
<p className="mt-2 text-3xl font-semibold text-[#111827]">
|
||||||
{formatoMoneda(precioLista)}
|
{formatoMoneda(precioLista)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -432,7 +432,7 @@ export default function CotizadorPage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleNueva}
|
onClick={handleNueva}
|
||||||
className="w-full border border-neutral-300 bg-white px-5 py-2.5 text-xs font-bold tracking-widest text-neutral-600 uppercase transition-colors duration-150 hover:bg-neutral-100"
|
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
|
Nueva cotización
|
||||||
</button>
|
</button>
|
||||||
@@ -452,8 +452,8 @@ export default function CotizadorPage() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{!unidad ? (
|
{!unidad ? (
|
||||||
<div className="mt-16 flex h-64 items-center justify-center border border-dashed border-neutral-300 bg-white">
|
<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-neutral-500">
|
<p className="text-sm text-[#64748B]">
|
||||||
Selecciona una unidad para ver las propuestas comerciales.
|
Selecciona una unidad para ver las propuestas comerciales.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -485,7 +485,7 @@ export default function CotizadorPage() {
|
|||||||
id="propuesta-elegida"
|
id="propuesta-elegida"
|
||||||
value={Math.min(propuestaIndex, propuestas.length - 1)}
|
value={Math.min(propuestaIndex, propuestas.length - 1)}
|
||||||
onChange={(e) => setPropuestaIndex(Number(e.target.value))}
|
onChange={(e) => setPropuestaIndex(Number(e.target.value))}
|
||||||
className="border border-neutral-300 bg-white px-3 py-2.5 text-sm transition-colors duration-150 focus:border-neutral-900 focus:outline-none"
|
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) => (
|
{propuestas.map((p, i) => (
|
||||||
<option key={p.tipo} value={i}>
|
<option key={p.tipo} value={i}>
|
||||||
@@ -498,7 +498,7 @@ export default function CotizadorPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleCompartir}
|
onClick={handleCompartir}
|
||||||
disabled={!puedeGenerarPdf}
|
disabled={!puedeGenerarPdf}
|
||||||
className="border border-accent bg-white px-5 py-2.5 text-xs font-bold tracking-widest text-accent uppercase transition-colors duration-150 hover:bg-accent hover:text-white disabled:cursor-not-allowed disabled:opacity-40"
|
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
|
Compartir
|
||||||
</button>
|
</button>
|
||||||
@@ -506,14 +506,14 @@ export default function CotizadorPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={handleGenerarPdf}
|
onClick={handleGenerarPdf}
|
||||||
disabled={!puedeGenerarPdf}
|
disabled={!puedeGenerarPdf}
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold tracking-widest text-white uppercase transition-colors duration-150 hover:bg-accent-hover disabled:cursor-not-allowed disabled:opacity-40"
|
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'}
|
{guardando ? 'Guardando…' : 'Guardar y generar PDF'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{aviso && <p className="mt-2 text-right text-xs text-neutral-500">{aviso}</p>}
|
{aviso && <p className="mt-2 text-right text-xs text-[#64748B]">{aviso}</p>}
|
||||||
{!puedeGenerarPdf && !guardando && (
|
{!puedeGenerarPdf && !guardando && (
|
||||||
<p className="mt-2 text-right text-xs text-neutral-400">
|
<p className="mt-2 text-right text-xs text-[#64748B]">
|
||||||
Selecciona una unidad para guardar y generar el PDF.
|
Selecciona una unidad para guardar y generar el PDF.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white text-neutral-900">
|
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
|
||||||
<div className="border-b border-neutral-200">
|
<div className="border-b border-[#E5E7EB] bg-white">
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="text-xs font-medium text-[#4B5563]">
|
||||||
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setModalPassword(true)}
|
onClick={() => setModalPassword(true)}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:text-neutral-900"
|
className="rounded-lg px-2 py-1.5 text-xs font-medium text-[#4B5563] transition-all duration-100 ease-out hover:bg-[#F9FAFB] hover:text-[#111827]"
|
||||||
>
|
>
|
||||||
Contraseña
|
Contraseña
|
||||||
</button>
|
</button>
|
||||||
@@ -37,7 +37,7 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
logout()
|
logout()
|
||||||
setSesion(null)
|
setSesion(null)
|
||||||
}}
|
}}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:text-accent-hover"
|
className="rounded-lg px-2 py-1.5 text-xs font-medium text-accent transition-all duration-100 ease-out hover:bg-blue-50 hover:text-accent-hover"
|
||||||
>
|
>
|
||||||
Salir
|
Salir
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { validarConfirmacion, validarPasswordNueva } from '../lib/password'
|
|||||||
import { cambiarPassword } from '../services/auth'
|
import { cambiarPassword } from '../services/auth'
|
||||||
|
|
||||||
const CLASE_INPUT =
|
const CLASE_INPUT =
|
||||||
'w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none'
|
'w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
|
||||||
const CLASE_LABEL = 'mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500'
|
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
||||||
@@ -45,19 +45,19 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Cambiar contraseña"
|
aria-label="Cambiar contraseña"
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-sm border border-neutral-200 bg-white p-6">
|
<div className="w-full max-w-sm rounded-2xl bg-white p-6 shadow-xl">
|
||||||
<h2 className="mb-6 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h2 className="mb-6 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Cambiar contraseña
|
Cambiar contraseña
|
||||||
</h2>
|
</h2>
|
||||||
{exito ? (
|
{exito ? (
|
||||||
<>
|
<>
|
||||||
<p className="border border-accent px-4 py-3 text-xs font-bold uppercase tracking-widest text-accent">
|
<p className="rounded-lg bg-green-50 px-4 py-3 text-xs font-medium text-green-700">
|
||||||
Contraseña actualizada
|
Contraseña actualizada
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCerrar}
|
onClick={onCerrar}
|
||||||
className="mt-6 w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover"
|
className="mt-6 w-full 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"
|
||||||
>
|
>
|
||||||
Cerrar
|
Cerrar
|
||||||
</button>
|
</button>
|
||||||
@@ -92,7 +92,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
required
|
required
|
||||||
className={CLASE_INPUT}
|
className={CLASE_INPUT}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 text-xs text-neutral-400">Mínimo 8 caracteres y un número</p>
|
<p className="mt-2 text-xs text-[#64748B]">Mínimo 8 caracteres y un número</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="password-confirmar" className={CLASE_LABEL}>
|
<label htmlFor="password-confirmar" className={CLASE_LABEL}>
|
||||||
@@ -111,7 +111,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="rounded-lg bg-red-50 px-4 py-3 text-xs font-medium text-red-700"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@@ -120,14 +120,14 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="flex-1 bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="flex-1 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:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Guardando…' : 'Guardar'}
|
{cargando ? 'Guardando…' : 'Guardar'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCerrar}
|
onClick={onCerrar}
|
||||||
className="border border-neutral-300 px-5 py-3 text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:border-neutral-500"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2.5 text-sm font-medium text-[#374151] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import type { ReactNode } from 'react'
|
|||||||
|
|
||||||
export default function KpiCard({ titulo, valor, detalle }: { titulo: string; valor: ReactNode; detalle?: string }) {
|
export default function KpiCard({ titulo, valor, detalle }: { titulo: string; valor: ReactNode; detalle?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="border border-neutral-200 bg-white p-6">
|
<div className="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm transition-shadow duration-200 hover:shadow-md">
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">{titulo}</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">{titulo}</p>
|
||||||
<p className="mt-2 text-3xl font-light text-neutral-900">{valor}</p>
|
<p className="mt-2 text-3xl font-light text-[#111827]">{valor}</p>
|
||||||
{detalle && <p className="mt-2 text-xs text-neutral-500">{detalle}</p>}
|
{detalle && <p className="mt-2 text-xs text-[#4B5563]">{detalle}</p>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, type FormEvent } from 'react'
|
import { useState, type FormEvent } from 'react'
|
||||||
import { login, ErrorLogin, type UsuarioSesion } from '../services/auth'
|
import { login, ErrorLogin, type UsuarioSesion } from '../services/auth'
|
||||||
|
|
||||||
/** Pantalla de login StudioBlank: marca FRASA, usuario + contraseña, Enter envía */
|
/** Pantalla de login NovaSpark: marca FRASA, usuario + contraseña, Enter envía */
|
||||||
export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSesion) => void }) {
|
export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSesion) => void }) {
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
@@ -22,11 +22,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-white px-4">
|
<div className="flex min-h-screen items-center justify-center bg-[#F8FAFC] px-4">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm">
|
||||||
<div className="mb-10 text-center">
|
<div className="mb-10 text-center">
|
||||||
<p className="text-lg font-bold tracking-widest">FRASA</p>
|
<p className="text-lg font-bold tracking-widest">
|
||||||
<p className="mt-2 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
FRASA<span className="text-[#F97316]">.</span>
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Acceso al ecosistema
|
Acceso al ecosistema
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -34,7 +36,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="login-usuario"
|
htmlFor="login-usuario"
|
||||||
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Usuario
|
Usuario
|
||||||
</label>
|
</label>
|
||||||
@@ -46,13 +48,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="login-password"
|
htmlFor="login-password"
|
||||||
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Contraseña
|
Contraseña
|
||||||
</label>
|
</label>
|
||||||
@@ -63,13 +65,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="rounded-lg bg-red-50 px-4 py-3 text-xs font-medium text-red-700"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@@ -77,7 +79,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="w-full 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:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Entrando…' : 'Entrar'}
|
{cargando ? 'Entrando…' : 'Entrar'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { EstadoVacio, TituloSeccion } from './comunes'
|
|||||||
import { funnelMisLeads, type ResumenMiDia } from '../lib/mi-tablero'
|
import { funnelMisLeads, type ResumenMiDia } from '../lib/mi-tablero'
|
||||||
import { fetchResumenMiDia } from '../services/mi-dia'
|
import { fetchResumenMiDia } from '../services/mi-dia'
|
||||||
|
|
||||||
// Misma paleta del funnel del tablero general
|
// Misma paleta del funnel del tablero general (NovaSpark: azules + neutros)
|
||||||
const COLORES_FUNNEL = ['#a3a3a3', '#737373', '#52796f', '#1B4332']
|
const COLORES_FUNNEL = ['#94A3B8', '#64748B', '#3B82F6', '#2563EB']
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* "Mi tablero": KPIs personales del vendedor logueado (mis leads, actividades,
|
* "Mi tablero": KPIs personales del vendedor logueado (mis leads, actividades,
|
||||||
@@ -40,10 +40,10 @@ export default function MiTablero() {
|
|||||||
<main className="mx-auto max-w-6xl px-4 py-8">
|
<main className="mx-auto max-w-6xl px-4 py-8">
|
||||||
<div className="mb-8 flex items-end justify-between">
|
<div className="mb-8 flex items-end justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-accent">FRASA Desarrollos</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-accent">FRASA Desarrollos</p>
|
||||||
<h1 className="mt-2 text-3xl font-light text-neutral-900">Mi tablero</h1>
|
<h1 className="mt-2 text-3xl font-light text-[#111827]">Mi tablero</h1>
|
||||||
{resumen && (
|
{resumen && (
|
||||||
<p className="mt-1 text-xs text-neutral-400">
|
<p className="mt-1 text-xs text-[#64748B]">
|
||||||
Actualizado: {new Date(resumen.generadoEn).toLocaleString('es-MX')}
|
Actualizado: {new Date(resumen.generadoEn).toLocaleString('es-MX')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -52,20 +52,20 @@ export default function MiTablero() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => void cargar()}
|
onClick={() => void cargar()}
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="border border-accent px-5 py-2 text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:bg-accent hover:text-white disabled:opacity-50"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2 text-xs font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-blue-50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Actualizando…' : 'Actualizar'}
|
{cargando ? 'Actualizando…' : 'Actualizar'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p role="alert" className="mb-8 border border-amber-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-amber-700">
|
<p role="alert" className="mb-8 rounded-lg bg-amber-50 px-4 py-3 text-xs font-medium text-amber-700">
|
||||||
Sin datos del API — {error}
|
Sin datos del API — {error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{resumen?.sinMapeoMonday && (
|
{resumen?.sinMapeoMonday && (
|
||||||
<p className="mb-8 border border-amber-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-amber-700">
|
<p className="mb-8 rounded-lg bg-amber-50 px-4 py-3 text-xs font-medium text-amber-700">
|
||||||
Tu usuario aún no está ligado a Monday: las listas personales aparecen vacías
|
Tu usuario aún no está ligado a Monday: las listas personales aparecen vacías
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ const DIMENSIONES: { titulo: string; registro: (perfil: ResumenReportes['leads']
|
|||||||
function TarjetaDimension({ titulo, datos }: { titulo: string; datos: Record<string, number> }) {
|
function TarjetaDimension({ titulo, datos }: { titulo: string; datos: Record<string, number> }) {
|
||||||
const rebanadas = rebanadasDe(datos)
|
const rebanadas = rebanadasDe(datos)
|
||||||
return (
|
return (
|
||||||
<div className="border border-neutral-200 p-4">
|
<div className="rounded-xl border border-[#E5E7EB] bg-white p-4 shadow-sm transition-shadow duration-200 hover:shadow-md">
|
||||||
<p className="mb-2 text-xs font-bold uppercase tracking-widest text-neutral-500">{titulo}</p>
|
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-[#64748B]">{titulo}</p>
|
||||||
{rebanadas.length === 0 ? (
|
{rebanadas.length === 0 ? (
|
||||||
<EstadoVacio mensaje="Sin datos" />
|
<EstadoVacio mensaje="Sin datos" />
|
||||||
) : (
|
) : (
|
||||||
@@ -32,7 +32,7 @@ function TarjetaDimension({ titulo, datos }: { titulo: string; datos: Record<str
|
|||||||
<XAxis type="number" allowDecimals={false} tick={{ fontSize: 11 }} />
|
<XAxis type="number" allowDecimals={false} tick={{ fontSize: 11 }} />
|
||||||
<YAxis type="category" dataKey="nombre" width={90} tick={{ fontSize: 11 }} />
|
<YAxis type="category" dataKey="nombre" width={90} tick={{ fontSize: 11 }} />
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
<Bar dataKey="valor" name="Leads" fill="#1B4332" isAnimationActive={false} />
|
<Bar dataKey="valor" name="Leads" fill="#2563EB" isAnimationActive={false} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,9 +3,8 @@ import { EstadoVacio, TituloSeccion } from './comunes'
|
|||||||
import type { ResumenMiDia } from '../lib/mi-tablero'
|
import type { ResumenMiDia } from '../lib/mi-tablero'
|
||||||
import { fetchResumenMiDia } from '../services/mi-dia'
|
import { fetchResumenMiDia } from '../services/mi-dia'
|
||||||
|
|
||||||
const CLASE_CELDA = 'px-4 py-3 text-sm text-neutral-900'
|
const CLASE_CELDA = 'px-4 py-3 text-sm text-[#111827]'
|
||||||
const CLASE_ENCABEZADO =
|
const CLASE_ENCABEZADO = 'bg-[#F8FAFC] px-4 py-2 text-left text-xs font-semibold uppercase text-[#64748B]'
|
||||||
'px-4 py-2 text-left text-xs font-bold uppercase tracking-widest text-neutral-500'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sección "Equipo" (solo coordinador/corporativo/admin): tabla vendedor →
|
* Sección "Equipo" (solo coordinador/corporativo/admin): tabla vendedor →
|
||||||
@@ -34,31 +33,31 @@ export default function SeccionEquipo() {
|
|||||||
<section aria-label="Equipo" className="mt-12">
|
<section aria-label="Equipo" className="mt-12">
|
||||||
<TituloSeccion>Equipo: leads por vendedor</TituloSeccion>
|
<TituloSeccion>Equipo: leads por vendedor</TituloSeccion>
|
||||||
{error && (
|
{error && (
|
||||||
<p role="alert" className="border border-amber-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-amber-700">
|
<p role="alert" className="rounded-lg bg-amber-50 px-4 py-3 text-xs font-medium text-amber-700">
|
||||||
Sin datos del API — {error}
|
Sin datos del API — {error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{!error && !equipo && <EstadoVacio mensaje="Sin datos de equipo" />}
|
{!error && !equipo && <EstadoVacio mensaje="Sin datos de equipo" />}
|
||||||
{!error && equipo && (
|
{!error && equipo && (
|
||||||
<>
|
<>
|
||||||
<p className="mb-4 text-xs text-neutral-500">
|
<p className="mb-4 text-xs text-[#4B5563]">
|
||||||
Leads nuevos sin asignar:{' '}
|
Leads nuevos sin asignar:{' '}
|
||||||
<span className="font-bold text-neutral-900">{equipo.leadsNuevosSinAsignar}</span>
|
<span className="font-semibold text-[#111827]">{equipo.leadsNuevosSinAsignar}</span>
|
||||||
</p>
|
</p>
|
||||||
{equipo.porVendedor.length === 0 ? (
|
{equipo.porVendedor.length === 0 ? (
|
||||||
<EstadoVacio mensaje="Sin leads asignados al equipo" />
|
<EstadoVacio mensaje="Sin leads asignados al equipo" />
|
||||||
) : (
|
) : (
|
||||||
<table className="w-full border border-neutral-200 bg-white">
|
<table className="w-full overflow-hidden rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
|
||||||
<thead className="border-b border-neutral-200">
|
<thead className="border-b border-[#E5E7EB]">
|
||||||
<tr>
|
<tr>
|
||||||
<th className={CLASE_ENCABEZADO}>Vendedor</th>
|
<th className={CLASE_ENCABEZADO}>Vendedor</th>
|
||||||
<th className={CLASE_ENCABEZADO}>Leads asignados</th>
|
<th className={CLASE_ENCABEZADO}>Leads asignados</th>
|
||||||
<th className={CLASE_ENCABEZADO}>Nuevos esta semana</th>
|
<th className={CLASE_ENCABEZADO}>Nuevos esta semana</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody className="divide-y divide-[#E5E7EB]">
|
||||||
{equipo.porVendedor.map((v) => (
|
{equipo.porVendedor.map((v) => (
|
||||||
<tr key={v.mondayUserId} className="border-b border-neutral-100 last:border-b-0">
|
<tr key={v.mondayUserId} className="transition-colors duration-100 hover:bg-[#F8FAFC]">
|
||||||
<td className={CLASE_CELDA}>{v.nombre ?? `Monday #${v.mondayUserId}`}</td>
|
<td className={CLASE_CELDA}>{v.nombre ?? `Monday #${v.mondayUserId}`}</td>
|
||||||
<td className={CLASE_CELDA}>{v.leadsAsignados}</td>
|
<td className={CLASE_CELDA}>{v.leadsAsignados}</td>
|
||||||
<td className={CLASE_CELDA}>{v.leadsNuevosSemana}</td>
|
<td className={CLASE_CELDA}>{v.leadsNuevosSemana}</td>
|
||||||
|
|||||||
@@ -31,16 +31,16 @@ import {
|
|||||||
} from '../lib/reportes'
|
} from '../lib/reportes'
|
||||||
import { fetchResumen } from '../services/reportes'
|
import { fetchResumen } from '../services/reportes'
|
||||||
|
|
||||||
// Paleta StudioBlank: acento verde + neutros (sin gradientes ni sombras)
|
// Paleta NovaSpark: acento azul + semánticos (sin gradientes)
|
||||||
const COLORES_STATUS: Record<string, string> = {
|
const COLORES_STATUS: Record<string, string> = {
|
||||||
DISPONIBLE: '#1B4332',
|
DISPONIBLE: '#2563EB',
|
||||||
APARTADO: '#b45309',
|
APARTADO: '#F59E0B',
|
||||||
VENDIDO: '#525252',
|
VENDIDO: '#525252',
|
||||||
BLOQUEADO: '#b91c1c',
|
BLOQUEADO: '#EF4444',
|
||||||
OTROS: '#d4d4d4',
|
OTROS: '#d4d4d4',
|
||||||
}
|
}
|
||||||
const COLORES_FUNNEL = ['#a3a3a3', '#737373', '#52796f', '#1B4332']
|
const COLORES_FUNNEL = ['#94A3B8', '#64748B', '#3B82F6', '#2563EB']
|
||||||
const COLORES_COBRANZA = ['#1B4332', '#b45309']
|
const COLORES_COBRANZA = ['#2563EB', '#F59E0B']
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Vista "General": el tablero comercial de todo el ecosistema (KPIs, inventario,
|
* Vista "General": el tablero comercial de todo el ecosistema (KPIs, inventario,
|
||||||
@@ -79,10 +79,10 @@ export default function TableroGeneral({ mostrarEquipo }: { mostrarEquipo: boole
|
|||||||
<main className="mx-auto max-w-6xl px-4 py-8">
|
<main className="mx-auto max-w-6xl px-4 py-8">
|
||||||
<div className="mb-8 flex items-end justify-between">
|
<div className="mb-8 flex items-end justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-accent">FRASA Desarrollos</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-accent">FRASA Desarrollos</p>
|
||||||
<h1 className="mt-2 text-3xl font-light text-neutral-900">Tablero comercial</h1>
|
<h1 className="mt-2 text-3xl font-light text-[#111827]">Tablero comercial</h1>
|
||||||
{resumen && (
|
{resumen && (
|
||||||
<p className="mt-1 text-xs text-neutral-400">
|
<p className="mt-1 text-xs text-[#64748B]">
|
||||||
Actualizado: {new Date(resumen.generadoEn).toLocaleString('es-MX')}
|
Actualizado: {new Date(resumen.generadoEn).toLocaleString('es-MX')}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -91,14 +91,14 @@ export default function TableroGeneral({ mostrarEquipo }: { mostrarEquipo: boole
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => void cargar()}
|
onClick={() => void cargar()}
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="border border-accent px-5 py-2 text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:bg-accent hover:text-white disabled:opacity-50"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2 text-xs font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-blue-50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Actualizando…' : 'Actualizar'}
|
{cargando ? 'Actualizando…' : 'Actualizar'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p role="alert" className="mb-8 border border-amber-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-amber-700">
|
<p role="alert" className="mb-8 rounded-lg bg-amber-50 px-4 py-3 text-xs font-medium text-amber-700">
|
||||||
Sin datos del API — {error}
|
Sin datos del API — {error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -185,7 +185,7 @@ export default function TableroGeneral({ mostrarEquipo }: { mostrarEquipo: boole
|
|||||||
<XAxis type="number" allowDecimals={false} tick={{ fontSize: 11 }} />
|
<XAxis type="number" allowDecimals={false} tick={{ fontSize: 11 }} />
|
||||||
<YAxis type="category" dataKey="etapa" width={90} tick={{ fontSize: 11 }} />
|
<YAxis type="category" dataKey="etapa" width={90} tick={{ fontSize: 11 }} />
|
||||||
<Tooltip />
|
<Tooltip />
|
||||||
<Bar dataKey="valor" name="Unidades" fill="#1B4332" isAnimationActive={false} />
|
<Bar dataKey="valor" name="Unidades" fill="#2563EB" isAnimationActive={false} />
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
// Piezas visuales compartidas por las vistas del tablero (StudioBlank)
|
// Piezas visuales compartidas por las vistas del tablero (NovaSpark)
|
||||||
|
|
||||||
export function EstadoVacio({ mensaje }: { mensaje: string }) {
|
export function EstadoVacio({ mensaje }: { mensaje: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-64 items-center justify-center border border-dashed border-neutral-300">
|
<div className="flex h-64 items-center justify-center rounded-xl border border-dashed border-[#E5E7EB] bg-white">
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-400">{mensaje}</p>
|
<p className="text-xs font-medium uppercase tracking-wider text-[#64748B]">{mensaje}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TituloSeccion({ children }: { children: string }) {
|
export function TituloSeccion({ children }: { children: string }) {
|
||||||
return <h2 className="mb-4 text-xs font-bold uppercase tracking-widest text-neutral-500">{children}</h2>
|
return <h2 className="mb-4 text-xs font-semibold uppercase tracking-wider text-[#64748B]">{children}</h2>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,22 +2,17 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
--color-accent: #1B4332;
|
--color-accent: #2563EB;
|
||||||
--radius-sm: 0;
|
--color-accent-hover: #1D4ED8;
|
||||||
--radius-md: 0;
|
|
||||||
--radius-lg: 0;
|
|
||||||
--radius-xl: 0;
|
|
||||||
--radius-2xl: 0;
|
|
||||||
--radius-3xl: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply bg-white text-neutral-900 antialiased;
|
@apply bg-[#F8FAFC] text-[#111827] antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
*:focus-visible {
|
*:focus-visible {
|
||||||
outline: 2px solid #1B4332;
|
outline: none;
|
||||||
outline-offset: 2px;
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="border-b border-neutral-200 bg-white">
|
<div className="border-b border-[#E5E7EB] bg-white">
|
||||||
<div className="mx-auto flex max-w-6xl gap-2 px-4 py-3" role="tablist" aria-label="Vista del tablero">
|
<div className="mx-auto flex max-w-6xl gap-2 px-4 py-3" role="tablist" aria-label="Vista del tablero">
|
||||||
{VISTAS.map((v) => (
|
{VISTAS.map((v) => (
|
||||||
<button
|
<button
|
||||||
@@ -34,8 +34,8 @@ export default function DashboardPage() {
|
|||||||
onClick={() => setVista(v.id)}
|
onClick={() => setVista(v.id)}
|
||||||
className={
|
className={
|
||||||
vista === v.id
|
vista === v.id
|
||||||
? 'bg-accent px-5 py-2 text-xs font-bold uppercase tracking-widest text-white'
|
? 'rounded-lg bg-accent px-5 py-2 text-xs font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover'
|
||||||
: 'border border-neutral-300 px-5 py-2 text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:border-neutral-500'
|
: 'rounded-lg border border-[#D1D5DB] bg-white px-5 py-2 text-xs font-medium text-[#374151] transition-all duration-100 ease-out hover:bg-[#F9FAFB]'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{v.etiqueta}
|
{v.etiqueta}
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ import { ConfiguracionCuenta } from './components/ConfiguracionCuenta'
|
|||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col">
|
<div className="flex min-h-screen flex-col">
|
||||||
<header className="px-6 pt-20 pb-16 sm:px-12 lg:px-24">
|
<header className="border-b border-[#E5E7EB] bg-white px-6 pt-20 pb-16 sm:px-12 lg:px-24">
|
||||||
<p className="text-xs font-bold tracking-widest text-accent uppercase">FRASA Desarrollos</p>
|
<p className="text-xs font-semibold tracking-wider text-accent uppercase">
|
||||||
<h1 className="mt-4 text-4xl font-light text-neutral-900 sm:text-5xl">
|
FRASA Desarrollos <span aria-hidden="true" className="text-[#F97316]">●</span>
|
||||||
|
</p>
|
||||||
|
<h1 className="mt-4 text-4xl font-light text-[#111827] sm:text-5xl">
|
||||||
Ecosistema comercial
|
Ecosistema comercial
|
||||||
</h1>
|
</h1>
|
||||||
</header>
|
</header>
|
||||||
@@ -25,7 +27,7 @@ export default function App() {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer className="px-6 pt-24 pb-10 sm:px-12 lg:px-24">
|
<footer className="px-6 pt-24 pb-10 sm:px-12 lg:px-24">
|
||||||
<p className="text-xs text-neutral-400">Consult (AS) Technology</p>
|
<p className="text-xs text-[#64748B]">Consult (AS) Technology</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { useEffect, useState, type FormEvent } from 'react'
|
|||||||
import { ErrorLogin, getSesion, login, logout, type UsuarioSesion } from '../services/auth'
|
import { ErrorLogin, getSesion, login, logout, type UsuarioSesion } from '../services/auth'
|
||||||
import { getMe, getUsuariosMonday, guardarVinculoMonday, type UsuarioMonday } from '../services/api'
|
import { getMe, getUsuariosMonday, guardarVinculoMonday, type UsuarioMonday } from '../services/api'
|
||||||
|
|
||||||
const labelEstilo = 'block text-xs font-bold tracking-widest text-neutral-500 uppercase'
|
const labelEstilo = 'block text-xs font-medium text-[#4B5563]'
|
||||||
const inputEstilo =
|
const inputEstilo =
|
||||||
'mt-2 w-full border border-neutral-200 bg-white px-3 py-2 text-sm text-neutral-900 focus:border-neutral-900'
|
'mt-2 w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm text-[#111827] transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sección "Configuración de cuenta": login inline y, con sesión, el vínculo
|
* Sección "Configuración de cuenta": login inline y, con sesión, el vínculo
|
||||||
@@ -95,14 +95,17 @@ export function ConfiguracionCuenta() {
|
|||||||
perfil?.mondayUserId != null ? (usuarios?.find((u) => u.id === perfil.mondayUserId) ?? null) : null
|
perfil?.mondayUserId != null ? (usuarios?.find((u) => u.id === perfil.mondayUserId) ?? null) : null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section aria-labelledby="titulo-cuenta" className="mt-24 border-t border-neutral-200 pt-16">
|
<section aria-labelledby="titulo-cuenta" className="mt-24 border-t border-[#E5E7EB] pt-16">
|
||||||
<h2 id="titulo-cuenta" className="text-xs font-bold tracking-widest text-accent uppercase">
|
<h2 id="titulo-cuenta" className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Configuración de cuenta
|
Configuración de cuenta
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{!conSesion ? (
|
{!conSesion ? (
|
||||||
<form onSubmit={(e) => void entrar(e)} className="mt-8 max-w-sm">
|
<form
|
||||||
<p className="text-sm leading-relaxed text-neutral-500">
|
onSubmit={(e) => void entrar(e)}
|
||||||
|
className="mt-8 max-w-sm rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm"
|
||||||
|
>
|
||||||
|
<p className="text-sm leading-relaxed text-[#64748B]">
|
||||||
Inicia sesión para vincular tu usuario de Monday.
|
Inicia sesión para vincular tu usuario de Monday.
|
||||||
</p>
|
</p>
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
@@ -134,55 +137,55 @@ export function ConfiguracionCuenta() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errorLogin && (
|
{errorLogin && (
|
||||||
<p role="alert" className="mt-4 text-xs text-red-700">
|
<p role="alert" className="mt-4 text-sm text-red-600">
|
||||||
{errorLogin}
|
{errorLogin}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={entrando}
|
disabled={entrando}
|
||||||
className="mt-6 bg-accent px-5 py-2.5 text-xs font-bold tracking-widest text-white uppercase transition-colors duration-150 hover:bg-neutral-900 disabled:opacity-50"
|
className="mt-6 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:opacity-50"
|
||||||
>
|
>
|
||||||
{entrando ? 'Entrando…' : 'Entrar'}
|
{entrando ? 'Entrando…' : 'Entrar'}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-8 max-w-md">
|
<div className="mt-8 max-w-md rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm">
|
||||||
{cargandoPerfil && <p className="text-sm text-neutral-500">Cargando tu perfil…</p>}
|
{cargandoPerfil && <p className="text-sm text-[#64748B]">Cargando tu perfil…</p>}
|
||||||
|
|
||||||
{perfil && (
|
{perfil && (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm text-neutral-900">
|
<p className="text-sm text-[#111827]">
|
||||||
Sesión: <span className="font-medium">{perfil.nombre}</span>
|
Sesión: <span className="font-medium">{perfil.nombre}</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-2 text-sm text-neutral-500">
|
<p className="mt-2 text-sm text-[#64748B]">
|
||||||
Vínculo Monday actual:{' '}
|
Vínculo Monday actual:{' '}
|
||||||
{perfil.mondayUserId === null ? (
|
{perfil.mondayUserId === null ? (
|
||||||
<span className="text-neutral-400">sin vincular</span>
|
<span className="text-[#94A3B8]">sin vincular</span>
|
||||||
) : vinculado ? (
|
) : vinculado ? (
|
||||||
<span className="text-neutral-900">
|
<span className="text-[#111827]">
|
||||||
{vinculado.nombre} ({vinculado.email})
|
{vinculado.nombre} ({vinculado.email})
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-neutral-900">ID {perfil.mondayUserId}</span>
|
<span className="text-[#111827]">ID {perfil.mondayUserId}</span>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{errorUsuarios ? (
|
{errorUsuarios ? (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<p role="alert" className="text-xs text-red-700">
|
<p role="alert" className="text-sm text-red-600">
|
||||||
{errorUsuarios}
|
{errorUsuarios}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={cargarUsuarios}
|
onClick={cargarUsuarios}
|
||||||
className="mt-3 border border-neutral-900 bg-transparent px-4 py-2 text-xs font-bold tracking-widest text-neutral-900 uppercase transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="mt-3 rounded-lg border border-[#D1D5DB] bg-white px-4 py-2.5 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Reintentar
|
Reintentar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : usuarios === null ? (
|
) : usuarios === null ? (
|
||||||
<p className="mt-6 text-sm text-neutral-500">Cargando usuarios de Monday…</p>
|
<p className="mt-6 text-sm text-[#64748B]">Cargando usuarios de Monday…</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<label htmlFor="cuenta-monday" className={labelEstilo}>
|
<label htmlFor="cuenta-monday" className={labelEstilo}>
|
||||||
@@ -206,7 +209,7 @@ export function ConfiguracionCuenta() {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={guardando}
|
disabled={guardando}
|
||||||
onClick={() => void guardar(seleccion === '' ? null : Number(seleccion))}
|
onClick={() => void guardar(seleccion === '' ? null : Number(seleccion))}
|
||||||
className="bg-accent px-5 py-2.5 text-xs font-bold tracking-widest text-white uppercase transition-colors duration-150 hover:bg-neutral-900 disabled:opacity-50"
|
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:opacity-50"
|
||||||
>
|
>
|
||||||
{guardando ? 'Guardando…' : 'Guardar'}
|
{guardando ? 'Guardando…' : 'Guardar'}
|
||||||
</button>
|
</button>
|
||||||
@@ -215,7 +218,7 @@ export function ConfiguracionCuenta() {
|
|||||||
type="button"
|
type="button"
|
||||||
disabled={guardando}
|
disabled={guardando}
|
||||||
onClick={() => void guardar(null)}
|
onClick={() => void guardar(null)}
|
||||||
className="border border-red-300 bg-white px-5 py-2.5 text-xs font-bold tracking-widest text-red-700 uppercase transition-colors duration-150 hover:bg-red-50 disabled:opacity-50"
|
className="rounded-lg border border-red-300 bg-white px-5 py-2.5 text-sm font-medium text-red-600 transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Desvincular
|
Desvincular
|
||||||
</button>
|
</button>
|
||||||
@@ -225,12 +228,12 @@ export function ConfiguracionCuenta() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{aviso && (
|
{aviso && (
|
||||||
<p role="status" className="mt-4 text-xs font-bold tracking-widest text-accent uppercase">
|
<p role="status" className="mt-4 text-sm font-medium text-green-600">
|
||||||
{aviso}
|
{aviso}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{errorGuardar && (
|
{errorGuardar && (
|
||||||
<p role="alert" className="mt-4 text-xs text-red-700">
|
<p role="alert" className="mt-4 text-sm text-red-600">
|
||||||
{errorGuardar}
|
{errorGuardar}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -240,7 +243,7 @@ export function ConfiguracionCuenta() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={cerrarSesion}
|
onClick={cerrarSesion}
|
||||||
className="mt-8 border border-neutral-900 bg-transparent px-4 py-2 text-xs font-bold tracking-widest text-neutral-900 uppercase transition-colors duration-150 hover:bg-neutral-900 hover:text-white"
|
className="mt-8 rounded-lg border border-[#D1D5DB] bg-white px-4 py-2.5 text-sm font-medium text-[#374151] shadow-sm transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Cerrar sesión
|
Cerrar sesión
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,11 +10,11 @@ export function TarjetaModulo({ modulo }: Props) {
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={modulo.url}
|
href={modulo.url}
|
||||||
className="group flex h-full flex-col border border-neutral-200 bg-white p-8 transition-colors duration-150 hover:border-neutral-900"
|
className="group flex h-full flex-col rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm transition-all duration-200 ease-in-out hover:shadow-md"
|
||||||
>
|
>
|
||||||
<h2 className="text-2xl font-light text-neutral-900">{modulo.nombre}</h2>
|
<h2 className="text-2xl font-light text-[#111827]">{modulo.nombre}</h2>
|
||||||
<p className="mt-3 text-sm leading-relaxed text-neutral-500">{modulo.descripcion}</p>
|
<p className="mt-3 text-sm leading-relaxed text-[#64748B]">{modulo.descripcion}</p>
|
||||||
<span className="mt-auto pt-8 text-xs font-bold tracking-widest text-accent uppercase">
|
<span className="mt-auto pt-8 text-sm font-medium text-accent transition-colors duration-100 ease-out group-hover:text-accent-hover">
|
||||||
Entrar <span aria-hidden="true">→</span>
|
Entrar <span aria-hidden="true">→</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -2,26 +2,18 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
--color-accent: #1B4332;
|
--color-accent: #2563EB;
|
||||||
--color-accent-hover: #132f23;
|
--color-accent-hover: #1D4ED8;
|
||||||
--radius-xs: 0;
|
|
||||||
--radius-sm: 0;
|
|
||||||
--radius-md: 0;
|
|
||||||
--radius-lg: 0;
|
|
||||||
--radius-xl: 0;
|
|
||||||
--radius-2xl: 0;
|
|
||||||
--radius-3xl: 0;
|
|
||||||
--radius-4xl: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply bg-white text-neutral-900 antialiased;
|
@apply bg-[#F8FAFC] text-[#111827] antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Foco visible accesible: outline en color de acento */
|
/* Foco visible accesible: anillo azul en elementos interactivos */
|
||||||
:focus-visible {
|
:focus-visible {
|
||||||
outline: 2px solid #1B4332;
|
outline: none;
|
||||||
outline-offset: 2px;
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,17 +17,17 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white text-neutral-900">
|
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
|
||||||
<div className="border-b border-neutral-200">
|
<div className="border-b border-[#E5E7EB] bg-white">
|
||||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-2">
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="text-xs font-medium text-[#64748B]">
|
||||||
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
{sesion.usuario.nombre} · {sesion.usuario.rol}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setModalPassword(true)}
|
onClick={() => setModalPassword(true)}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:text-neutral-900"
|
className="rounded-md px-2 py-1.5 text-xs font-medium text-[#4B5563] transition-all duration-100 ease-out hover:text-[#111827]"
|
||||||
>
|
>
|
||||||
Contraseña
|
Contraseña
|
||||||
</button>
|
</button>
|
||||||
@@ -37,7 +37,7 @@ export default function AuthGate({ children }: { children: ReactNode }) {
|
|||||||
logout()
|
logout()
|
||||||
setSesion(null)
|
setSesion(null)
|
||||||
}}
|
}}
|
||||||
className="text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:text-accent-hover"
|
className="rounded-md px-2 py-1.5 text-xs font-medium text-accent transition-all duration-100 ease-out hover:text-accent-hover"
|
||||||
>
|
>
|
||||||
Salir
|
Salir
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { validarConfirmacion, validarPasswordNueva } from '../lib/password'
|
|||||||
import { cambiarPassword } from '../services/auth'
|
import { cambiarPassword } from '../services/auth'
|
||||||
|
|
||||||
const CLASE_INPUT =
|
const CLASE_INPUT =
|
||||||
'w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none'
|
'w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
|
||||||
const CLASE_LABEL = 'mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500'
|
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
* Modal "Cambiar contraseña": actual + nueva + confirmar. Valida en cliente
|
||||||
@@ -45,19 +45,19 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label="Cambiar contraseña"
|
aria-label="Cambiar contraseña"
|
||||||
>
|
>
|
||||||
<div className="w-full max-w-sm border border-neutral-200 bg-white p-6">
|
<div className="w-full max-w-sm rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-xl">
|
||||||
<h2 className="mb-6 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<h2 className="mb-6 text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||||
Cambiar contraseña
|
Cambiar contraseña
|
||||||
</h2>
|
</h2>
|
||||||
{exito ? (
|
{exito ? (
|
||||||
<>
|
<>
|
||||||
<p className="border border-accent px-4 py-3 text-xs font-bold uppercase tracking-widest text-accent">
|
<p className="rounded-lg border border-green-200 bg-green-50 px-4 py-3 text-sm font-medium text-green-700">
|
||||||
Contraseña actualizada
|
Contraseña actualizada
|
||||||
</p>
|
</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCerrar}
|
onClick={onCerrar}
|
||||||
className="mt-6 w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover"
|
className="mt-6 w-full rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover"
|
||||||
>
|
>
|
||||||
Cerrar
|
Cerrar
|
||||||
</button>
|
</button>
|
||||||
@@ -92,7 +92,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
required
|
required
|
||||||
className={CLASE_INPUT}
|
className={CLASE_INPUT}
|
||||||
/>
|
/>
|
||||||
<p className="mt-2 text-xs text-neutral-400">Mínimo 8 caracteres y un número</p>
|
<p className="mt-2 text-xs text-[#94A3B8]">Mínimo 8 caracteres y un número</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="password-confirmar" className={CLASE_LABEL}>
|
<label htmlFor="password-confirmar" className={CLASE_LABEL}>
|
||||||
@@ -111,7 +111,7 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm font-medium text-red-700"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@@ -120,14 +120,14 @@ export default function CambiarPasswordModal({ onCerrar }: { onCerrar: () => voi
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="flex-1 bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="flex-1 rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Guardando…' : 'Guardar'}
|
{cargando ? 'Guardando…' : 'Guardar'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onCerrar}
|
onClick={onCerrar}
|
||||||
className="border border-neutral-300 px-5 py-3 text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:border-neutral-500"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-3 text-sm font-medium text-[#374151] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ import type { ReactNode } from 'react'
|
|||||||
|
|
||||||
export default function KpiCard({ titulo, valor, detalle }: { titulo: string; valor: ReactNode; detalle?: string }) {
|
export default function KpiCard({ titulo, valor, detalle }: { titulo: string; valor: ReactNode; detalle?: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="border border-neutral-200 bg-white p-6">
|
<div className="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm transition-shadow duration-200 hover:shadow-md">
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">{titulo}</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">{titulo}</p>
|
||||||
<p className="mt-2 text-3xl font-light text-neutral-900">{valor}</p>
|
<p className="mt-2 text-3xl font-semibold text-[#111827]">{valor}</p>
|
||||||
{detalle && <p className="mt-2 text-xs text-neutral-500">{detalle}</p>}
|
{detalle && <p className="mt-2 text-xs text-[#64748B]">{detalle}</p>}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState, type FormEvent } from 'react'
|
import { useState, type FormEvent } from 'react'
|
||||||
import { login, ErrorLogin, type UsuarioSesion } from '../services/auth'
|
import { login, ErrorLogin, type UsuarioSesion } from '../services/auth'
|
||||||
|
|
||||||
/** Pantalla de login StudioBlank: marca FRASA, usuario + contraseña, Enter envía */
|
/** Pantalla de login: marca FRASA, usuario + contraseña, Enter envía */
|
||||||
export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSesion) => void }) {
|
export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSesion) => void }) {
|
||||||
const [username, setUsername] = useState('')
|
const [username, setUsername] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
@@ -22,19 +22,24 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen items-center justify-center bg-white px-4">
|
<div className="flex min-h-screen items-center justify-center bg-[#F8FAFC] px-4">
|
||||||
<div className="w-full max-w-sm">
|
<div className="w-full max-w-sm">
|
||||||
<div className="mb-10 text-center">
|
<div className="mb-8 text-center">
|
||||||
<p className="text-lg font-bold tracking-widest">FRASA</p>
|
<p className="text-lg font-bold tracking-widest text-[#111827]">
|
||||||
<p className="mt-2 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
FRASA<span aria-hidden="true" className="text-[#F97316]">.</span>
|
||||||
|
</p>
|
||||||
|
<p className="mt-2 text-xs font-medium uppercase tracking-wider text-[#64748B]">
|
||||||
Acceso al ecosistema
|
Acceso al ecosistema
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={enviar} className="space-y-6">
|
<form
|
||||||
|
onSubmit={enviar}
|
||||||
|
className="space-y-6 rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="login-usuario"
|
htmlFor="login-usuario"
|
||||||
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Usuario
|
Usuario
|
||||||
</label>
|
</label>
|
||||||
@@ -46,13 +51,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
autoComplete="username"
|
autoComplete="username"
|
||||||
autoFocus
|
autoFocus
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
htmlFor="login-password"
|
htmlFor="login-password"
|
||||||
className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500"
|
className="mb-2 block text-xs font-medium text-[#4B5563]"
|
||||||
>
|
>
|
||||||
Contraseña
|
Contraseña
|
||||||
</label>
|
</label>
|
||||||
@@ -63,13 +68,13 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className="w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{error && (
|
||||||
<p
|
<p
|
||||||
role="alert"
|
role="alert"
|
||||||
className="border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700"
|
className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm font-medium text-red-700"
|
||||||
>
|
>
|
||||||
{error}
|
{error}
|
||||||
</p>
|
</p>
|
||||||
@@ -77,7 +82,7 @@ export default function LoginScreen({ onLogin }: { onLogin: (usuario: UsuarioSes
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="w-full bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="w-full rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Entrando…' : 'Entrar'}
|
{cargando ? 'Entrando…' : 'Entrar'}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -2,22 +2,17 @@
|
|||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
--color-accent: #1B4332;
|
--color-accent: #2563EB;
|
||||||
--radius-sm: 0;
|
--color-accent-hover: #1D4ED8;
|
||||||
--radius-md: 0;
|
|
||||||
--radius-lg: 0;
|
|
||||||
--radius-xl: 0;
|
|
||||||
--radius-2xl: 0;
|
|
||||||
--radius-3xl: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
body {
|
body {
|
||||||
@apply bg-white text-neutral-900 antialiased;
|
@apply bg-[#F8FAFC] text-[#111827] antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
*:focus-visible {
|
*:focus-visible {
|
||||||
outline: 2px solid #1B4332;
|
outline: none;
|
||||||
outline-offset: 2px;
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.35);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,19 +17,24 @@ import { cambiarEstadoLead, crearLead, descartarLead, fetchResumenMiDia } from '
|
|||||||
import { getSesion } from '../services/auth'
|
import { getSesion } from '../services/auth'
|
||||||
|
|
||||||
function TituloSeccion({ children }: { children: string }) {
|
function TituloSeccion({ children }: { children: string }) {
|
||||||
return <h2 className="mb-4 text-xs font-bold uppercase tracking-widest text-neutral-500">{children}</h2>
|
return <h2 className="mb-4 text-xs font-semibold uppercase tracking-wider text-[#64748B]">{children}</h2>
|
||||||
}
|
}
|
||||||
|
|
||||||
function EstadoVacio({ mensaje }: { mensaje: string }) {
|
function EstadoVacio({ mensaje }: { mensaje: string }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-24 items-center justify-center border border-dashed border-neutral-300">
|
<div className="flex h-24 items-center justify-center rounded-xl border border-dashed border-[#E5E7EB] bg-white">
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-400">{mensaje}</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#94A3B8]">{mensaje}</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CLASE_BOTON_ACCION =
|
const CLASE_BOTON_ACCION =
|
||||||
'border border-accent px-3 py-1.5 text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:bg-accent hover:text-white disabled:opacity-50'
|
'rounded-lg border border-[#D1D5DB] bg-white px-3 py-1.5 text-xs font-medium text-accent shadow-sm transition-all duration-100 ease-out hover:bg-blue-50 disabled:opacity-50'
|
||||||
|
|
||||||
|
const CLASE_INPUT =
|
||||||
|
'w-full rounded-lg border border-[#D1D5DB] bg-white px-3 py-2.5 text-sm transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20'
|
||||||
|
|
||||||
|
const CLASE_LABEL = 'mb-2 block text-xs font-medium text-[#4B5563]'
|
||||||
|
|
||||||
/** Modal de descarte: motivo (dropdown CRM) + comentario opcional */
|
/** Modal de descarte: motivo (dropdown CRM) + comentario opcional */
|
||||||
function ModalDescartar({
|
function ModalDescartar({
|
||||||
@@ -47,9 +52,9 @@ function ModalDescartar({
|
|||||||
const [comentario, setComentario] = useState('')
|
const [comentario, setComentario] = useState('')
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-10 flex items-center justify-center bg-black/40 px-4" role="dialog" aria-modal="true">
|
<div className="fixed inset-0 z-10 flex items-center justify-center bg-black/40 px-4" role="dialog" aria-modal="true">
|
||||||
<div className="w-full max-w-md border border-neutral-300 bg-white p-6">
|
<div className="w-full max-w-md rounded-2xl border border-[#E5E7EB] bg-white p-6 shadow-xl">
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-neutral-500">Descartar lead</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">Descartar lead</p>
|
||||||
<p className="mt-2 text-lg font-light">{lead.nombre}</p>
|
<p className="mt-2 text-lg font-medium text-[#111827]">{lead.nombre}</p>
|
||||||
<form
|
<form
|
||||||
className="mt-6 space-y-4"
|
className="mt-6 space-y-4"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
@@ -58,14 +63,14 @@ function ModalDescartar({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="motivo-descarte" className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<label htmlFor="motivo-descarte" className={CLASE_LABEL}>
|
||||||
Motivo
|
Motivo
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="motivo-descarte"
|
id="motivo-descarte"
|
||||||
value={motivo}
|
value={motivo}
|
||||||
onChange={(e) => setMotivo(e.target.value as MotivoNoCompra)}
|
onChange={(e) => setMotivo(e.target.value as MotivoNoCompra)}
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className={CLASE_INPUT}
|
||||||
>
|
>
|
||||||
{MOTIVOS_NO_COMPRA.map((m) => (
|
{MOTIVOS_NO_COMPRA.map((m) => (
|
||||||
<option key={m} value={m}>
|
<option key={m} value={m}>
|
||||||
@@ -75,7 +80,7 @@ function ModalDescartar({
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="comentario-descarte" className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<label htmlFor="comentario-descarte" className={CLASE_LABEL}>
|
||||||
Comentario (opcional)
|
Comentario (opcional)
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
@@ -83,7 +88,7 @@ function ModalDescartar({
|
|||||||
value={comentario}
|
value={comentario}
|
||||||
onChange={(e) => setComentario(e.target.value)}
|
onChange={(e) => setComentario(e.target.value)}
|
||||||
rows={3}
|
rows={3}
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className={CLASE_INPUT}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end gap-3">
|
<div className="flex justify-end gap-3">
|
||||||
@@ -91,14 +96,14 @@ function ModalDescartar({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={onCancelar}
|
onClick={onCancelar}
|
||||||
disabled={enviando}
|
disabled={enviando}
|
||||||
className="border border-neutral-300 px-5 py-2 text-xs font-bold uppercase tracking-widest text-neutral-500 transition-colors duration-150 hover:border-neutral-500 disabled:opacity-50"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-5 py-2 text-sm font-medium text-[#374151] transition-all duration-100 ease-out hover:bg-[#F9FAFB] disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Cancelar
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={enviando}
|
disabled={enviando}
|
||||||
className="bg-accent px-5 py-2 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="rounded-lg bg-red-600 px-5 py-2 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-red-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{enviando ? 'Descartando…' : 'Descartar'}
|
{enviando ? 'Descartando…' : 'Descartar'}
|
||||||
</button>
|
</button>
|
||||||
@@ -124,16 +129,16 @@ function FilaLead({
|
|||||||
const [conCita, setConCita] = useState(false)
|
const [conCita, setConCita] = useState(false)
|
||||||
const [fechaCita, setFechaCita] = useState('')
|
const [fechaCita, setFechaCita] = useState('')
|
||||||
return (
|
return (
|
||||||
<li className="border border-neutral-200 p-4">
|
<li className="rounded-xl border border-[#E5E7EB] bg-white p-4 shadow-sm">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">{lead.nombre}</p>
|
<p className="text-sm font-medium">{lead.nombre}</p>
|
||||||
<p className="mt-1 text-xs text-neutral-500">
|
<p className="mt-1 text-xs text-[#64748B]">
|
||||||
{lead.telefono || 'Sin teléfono'}
|
{lead.telefono || 'Sin teléfono'}
|
||||||
{lead.origen ? ` · ${lead.origen}` : ''}
|
{lead.origen ? ` · ${lead.origen}` : ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className="border border-neutral-300 px-2 py-1 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="rounded-full bg-blue-50 px-2.5 py-0.5 text-xs font-medium text-blue-700">
|
||||||
{lead.estado || 'Sin estado'}
|
{lead.estado || 'Sin estado'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -158,14 +163,14 @@ function FilaLead({
|
|||||||
aria-label="Fecha de la cita"
|
aria-label="Fecha de la cita"
|
||||||
value={fechaCita}
|
value={fechaCita}
|
||||||
onChange={(e) => setFechaCita(e.target.value)}
|
onChange={(e) => setFechaCita(e.target.value)}
|
||||||
className="border border-neutral-300 px-2 py-1.5 text-xs focus:border-accent focus:outline-none"
|
className="rounded-lg border border-[#D1D5DB] bg-white px-2 py-1.5 text-xs transition-colors focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/20"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={enviando}
|
disabled={enviando}
|
||||||
onClick={onDescartar}
|
onClick={onDescartar}
|
||||||
className="border border-red-700 px-3 py-1.5 text-xs font-bold uppercase tracking-widest text-red-700 transition-colors duration-150 hover:bg-red-700 hover:text-white disabled:opacity-50"
|
className="rounded-lg border border-red-200 bg-white px-3 py-1.5 text-xs font-medium text-red-600 shadow-sm transition-all duration-100 ease-out hover:bg-red-50 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Descartar
|
Descartar
|
||||||
</button>
|
</button>
|
||||||
@@ -258,31 +263,33 @@ export default function MiDiaPage() {
|
|||||||
<main className="mx-auto max-w-6xl px-4 py-8">
|
<main className="mx-auto max-w-6xl px-4 py-8">
|
||||||
<div className="mb-8 flex items-end justify-between">
|
<div className="mb-8 flex items-end justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-bold uppercase tracking-widest text-accent">FRASA · Mi día</p>
|
<p className="text-xs font-semibold uppercase tracking-wider text-accent">
|
||||||
<h1 className="mt-2 text-3xl font-light text-neutral-900">{nombreVendedor}</h1>
|
FRASA <span aria-hidden="true" className="text-[#F97316]">·</span> Mi día
|
||||||
|
</p>
|
||||||
|
<h1 className="mt-2 text-3xl font-semibold text-[#111827]">{nombreVendedor}</h1>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => void cargar()}
|
onClick={() => void cargar()}
|
||||||
disabled={cargando}
|
disabled={cargando}
|
||||||
className="border border-accent px-5 py-2 text-xs font-bold uppercase tracking-widest text-accent transition-colors duration-150 hover:bg-accent hover:text-white disabled:opacity-50"
|
className="rounded-lg bg-accent px-5 py-2 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{cargando ? 'Actualizando…' : 'Actualizar'}
|
{cargando ? 'Actualizando…' : 'Actualizar'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p role="alert" className="mb-8 border border-amber-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-amber-700">
|
<p role="alert" className="mb-8 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm font-medium text-amber-700">
|
||||||
Sin datos del API — {error}
|
Sin datos del API — {error}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{aviso && (
|
{aviso && (
|
||||||
<p role="alert" className="mb-8 border border-red-700 px-4 py-3 text-xs font-bold uppercase tracking-widest text-red-700">
|
<p role="alert" className="mb-8 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm font-medium text-red-700">
|
||||||
{aviso}
|
{aviso}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{resumen?.sinMapeoMonday && (
|
{resumen?.sinMapeoMonday && (
|
||||||
<p className="mb-8 border border-neutral-300 px-4 py-3 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<p className="mb-8 rounded-lg border border-[#E5E7EB] bg-white px-4 py-3 text-sm font-medium text-[#64748B] shadow-sm">
|
||||||
Tu usuario aún no está vinculado a Monday; las acciones siguen funcionando
|
Tu usuario aún no está vinculado a Monday; las acciones siguen funcionando
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -309,14 +316,17 @@ export default function MiDiaPage() {
|
|||||||
) : (
|
) : (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{resumen.misActividadesHoy.map((a) => (
|
{resumen.misActividadesHoy.map((a) => (
|
||||||
<li key={a.id} className="flex items-center justify-between gap-3 border border-neutral-200 px-4 py-3">
|
<li
|
||||||
|
key={a.id}
|
||||||
|
className="flex items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm"
|
||||||
|
>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{a.nombre}
|
{a.nombre}
|
||||||
{a.tipo ? <span className="ml-2 text-xs uppercase tracking-widest text-neutral-400">{a.tipo}</span> : null}
|
{a.tipo ? <span className="ml-2 text-xs uppercase tracking-wider text-[#94A3B8]">{a.tipo}</span> : null}
|
||||||
</p>
|
</p>
|
||||||
<span
|
<span
|
||||||
className={`text-xs font-bold uppercase tracking-widest ${
|
className={`text-xs font-medium ${
|
||||||
esActividadVencida(a.fecha, hoy) ? 'text-red-700' : 'text-neutral-500'
|
esActividadVencida(a.fecha, hoy) ? 'text-red-600' : 'text-[#64748B]'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{formatoFechaCorta(a.fecha)}
|
{formatoFechaCorta(a.fecha)}
|
||||||
@@ -332,14 +342,17 @@ export default function MiDiaPage() {
|
|||||||
<TituloSeccion>Apartados por vencer</TituloSeccion>
|
<TituloSeccion>Apartados por vencer</TituloSeccion>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{resumen.apartadosPorVencer.map((a) => (
|
{resumen.apartadosPorVencer.map((a) => (
|
||||||
<li key={a.id} className="flex flex-wrap items-center justify-between gap-3 border border-neutral-200 px-4 py-3">
|
<li
|
||||||
|
key={a.id}
|
||||||
|
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm"
|
||||||
|
>
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
{a.nombre}
|
{a.nombre}
|
||||||
{a.unidadCodigo ? <span className="ml-2 text-xs text-neutral-400">{a.unidadCodigo}</span> : null}
|
{a.unidadCodigo ? <span className="ml-2 text-xs text-[#94A3B8]">{a.unidadCodigo}</span> : null}
|
||||||
</p>
|
</p>
|
||||||
<span
|
<span
|
||||||
className={`text-xs font-bold uppercase tracking-widest ${
|
className={`text-xs font-medium ${
|
||||||
a.diasRestantes <= 1 ? 'text-red-700' : 'text-amber-700'
|
a.diasRestantes <= 1 ? 'text-red-600' : 'text-amber-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{textoDiasRestantes(a.diasRestantes)} · {formatoFechaCorta(a.fechaLimite)}
|
{textoDiasRestantes(a.diasRestantes)} · {formatoFechaCorta(a.fechaLimite)}
|
||||||
@@ -376,19 +389,22 @@ export default function MiDiaPage() {
|
|||||||
) : (
|
) : (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{resumen.misCapturas.map((c) => (
|
{resumen.misCapturas.map((c) => (
|
||||||
<li key={c.leadId} className="flex flex-wrap items-center justify-between gap-3 border border-neutral-200 px-4 py-3">
|
<li
|
||||||
|
key={c.leadId}
|
||||||
|
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">{c.nombre}</p>
|
<p className="text-sm font-medium">{c.nombre}</p>
|
||||||
<p className="mt-1 text-xs text-neutral-500">
|
<p className="mt-1 text-xs text-[#64748B]">
|
||||||
{c.telefono || 'Sin teléfono'}
|
{c.telefono || 'Sin teléfono'}
|
||||||
{c.origen ? ` · ${c.origen}` : ''}
|
{c.origen ? ` · ${c.origen}` : ''}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="border border-neutral-300 px-2 py-1 text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">
|
||||||
{c.estado || '—'}
|
{c.estado || '—'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="text-xs font-medium text-[#64748B]">
|
||||||
{formatoFechaCorta(c.capturadoEn.slice(0, 10))}
|
{formatoFechaCorta(c.capturadoEn.slice(0, 10))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -405,24 +421,27 @@ export default function MiDiaPage() {
|
|||||||
) : (
|
) : (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{resumen.misClientes.map((c) => (
|
{resumen.misClientes.map((c) => (
|
||||||
<li key={c.id} className="flex flex-wrap items-center justify-between gap-3 border border-neutral-200 px-4 py-3">
|
<li
|
||||||
|
key={c.id}
|
||||||
|
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-sm font-medium">{c.nombre}</p>
|
<p className="text-sm font-medium">{c.nombre}</p>
|
||||||
<p className="mt-1 text-xs text-neutral-500">
|
<p className="mt-1 text-xs text-[#64748B]">
|
||||||
{[c.proyecto, c.unidadCodigo].filter((p) => p !== '').join(' · ') || 'Alta sin proyecto'}
|
{[c.proyecto, c.unidadCodigo].filter((p) => p !== '').join(' · ') || 'Alta sin proyecto'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
className={`border px-2 py-1 text-xs font-bold uppercase tracking-widest ${
|
className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${
|
||||||
c.completado
|
c.completado
|
||||||
? 'border-neutral-300 text-neutral-500'
|
? 'bg-gray-100 text-gray-600'
|
||||||
: 'border-amber-700 text-amber-700'
|
: 'bg-amber-50 text-amber-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{c.completado ? 'Cliente' : 'En captura'}
|
{c.completado ? 'Cliente' : 'En captura'}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="text-xs font-medium text-[#64748B]">
|
||||||
{formatoFechaCorta(c.capturadoEn.slice(0, 10))}
|
{formatoFechaCorta(c.capturadoEn.slice(0, 10))}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -434,9 +453,12 @@ export default function MiDiaPage() {
|
|||||||
|
|
||||||
<section aria-label="Captura rápida de lead" className="mt-12">
|
<section aria-label="Captura rápida de lead" className="mt-12">
|
||||||
<TituloSeccion>Captura rápida de lead</TituloSeccion>
|
<TituloSeccion>Captura rápida de lead</TituloSeccion>
|
||||||
<form onSubmit={enviarCaptura} className="grid gap-4 border border-neutral-200 p-6 sm:grid-cols-2">
|
<form
|
||||||
|
onSubmit={enviarCaptura}
|
||||||
|
className="grid gap-4 rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm sm:grid-cols-2"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="cap-nombre" className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<label htmlFor="cap-nombre" className={CLASE_LABEL}>
|
||||||
Nombre
|
Nombre
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -445,11 +467,11 @@ export default function MiDiaPage() {
|
|||||||
value={capNombre}
|
value={capNombre}
|
||||||
onChange={(e) => setCapNombre(e.target.value)}
|
onChange={(e) => setCapNombre(e.target.value)}
|
||||||
required
|
required
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className={CLASE_INPUT}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="cap-telefono" className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<label htmlFor="cap-telefono" className={CLASE_LABEL}>
|
||||||
Teléfono
|
Teléfono
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -457,11 +479,11 @@ export default function MiDiaPage() {
|
|||||||
type="tel"
|
type="tel"
|
||||||
value={capTelefono}
|
value={capTelefono}
|
||||||
onChange={(e) => setCapTelefono(e.target.value)}
|
onChange={(e) => setCapTelefono(e.target.value)}
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className={CLASE_INPUT}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="cap-email" className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<label htmlFor="cap-email" className={CLASE_LABEL}>
|
||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -469,18 +491,18 @@ export default function MiDiaPage() {
|
|||||||
type="email"
|
type="email"
|
||||||
value={capEmail}
|
value={capEmail}
|
||||||
onChange={(e) => setCapEmail(e.target.value)}
|
onChange={(e) => setCapEmail(e.target.value)}
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className={CLASE_INPUT}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="cap-origen" className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<label htmlFor="cap-origen" className={CLASE_LABEL}>
|
||||||
Origen
|
Origen
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="cap-origen"
|
id="cap-origen"
|
||||||
value={capOrigen}
|
value={capOrigen}
|
||||||
onChange={(e) => setCapOrigen(e.target.value)}
|
onChange={(e) => setCapOrigen(e.target.value)}
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className={CLASE_INPUT}
|
||||||
>
|
>
|
||||||
{ORIGENES_LEAD.map((o) => (
|
{ORIGENES_LEAD.map((o) => (
|
||||||
<option key={o} value={o}>
|
<option key={o} value={o}>
|
||||||
@@ -490,14 +512,14 @@ export default function MiDiaPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="cap-desarrollo" className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<label htmlFor="cap-desarrollo" className={CLASE_LABEL}>
|
||||||
Desarrollo de interés
|
Desarrollo de interés
|
||||||
</label>
|
</label>
|
||||||
<select
|
<select
|
||||||
id="cap-desarrollo"
|
id="cap-desarrollo"
|
||||||
value={capDesarrollo}
|
value={capDesarrollo}
|
||||||
onChange={(e) => setCapDesarrollo(e.target.value)}
|
onChange={(e) => setCapDesarrollo(e.target.value)}
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className={CLASE_INPUT}
|
||||||
>
|
>
|
||||||
<option value="">Sin especificar</option>
|
<option value="">Sin especificar</option>
|
||||||
{DESARROLLOS_LEAD.map((d) => (
|
{DESARROLLOS_LEAD.map((d) => (
|
||||||
@@ -508,7 +530,7 @@ export default function MiDiaPage() {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="cap-notas" className="mb-2 block text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<label htmlFor="cap-notas" className={CLASE_LABEL}>
|
||||||
Notas
|
Notas
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
@@ -516,14 +538,16 @@ export default function MiDiaPage() {
|
|||||||
type="text"
|
type="text"
|
||||||
value={capNotas}
|
value={capNotas}
|
||||||
onChange={(e) => setCapNotas(e.target.value)}
|
onChange={(e) => setCapNotas(e.target.value)}
|
||||||
className="w-full border border-neutral-300 px-4 py-3 text-sm focus:border-accent focus:outline-none"
|
className={CLASE_INPUT}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{capMensaje && (
|
{capMensaje && (
|
||||||
<p
|
<p
|
||||||
role="status"
|
role="status"
|
||||||
className={`border px-4 py-3 text-xs font-bold uppercase tracking-widest sm:col-span-2 ${
|
className={`rounded-lg border px-4 py-3 text-sm font-medium sm:col-span-2 ${
|
||||||
capMensaje.tipo === 'ok' ? 'border-accent text-accent' : 'border-red-700 text-red-700'
|
capMensaje.tipo === 'ok'
|
||||||
|
? 'border-green-200 bg-green-50 text-green-700'
|
||||||
|
: 'border-red-200 bg-red-50 text-red-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{capMensaje.texto}
|
{capMensaje.texto}
|
||||||
@@ -533,7 +557,7 @@ export default function MiDiaPage() {
|
|||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={capEnviando}
|
disabled={capEnviando}
|
||||||
className="bg-accent px-5 py-3 text-xs font-bold uppercase tracking-widest text-white transition-colors duration-150 hover:bg-accent-hover disabled:opacity-50"
|
className="rounded-lg bg-accent px-5 py-3 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover disabled:opacity-50"
|
||||||
>
|
>
|
||||||
{capEnviando ? 'Registrando…' : 'Registrar lead'}
|
{capEnviando ? 'Registrando…' : 'Registrar lead'}
|
||||||
</button>
|
</button>
|
||||||
@@ -544,7 +568,7 @@ export default function MiDiaPage() {
|
|||||||
{esRolConEquipo(rol) && resumen.equipo && (
|
{esRolConEquipo(rol) && resumen.equipo && (
|
||||||
<section aria-label="Equipo" className="mt-12">
|
<section aria-label="Equipo" className="mt-12">
|
||||||
<TituloSeccion>Equipo</TituloSeccion>
|
<TituloSeccion>Equipo</TituloSeccion>
|
||||||
<p className="mb-4 text-sm text-neutral-500">
|
<p className="mb-4 text-sm text-[#64748B]">
|
||||||
Leads nuevos sin asignar: <strong>{resumen.equipo.leadsNuevosSinAsignar}</strong>
|
Leads nuevos sin asignar: <strong>{resumen.equipo.leadsNuevosSinAsignar}</strong>
|
||||||
</p>
|
</p>
|
||||||
{resumen.equipo.porVendedor.length === 0 ? (
|
{resumen.equipo.porVendedor.length === 0 ? (
|
||||||
@@ -552,9 +576,12 @@ export default function MiDiaPage() {
|
|||||||
) : (
|
) : (
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{resumen.equipo.porVendedor.map((v) => (
|
{resumen.equipo.porVendedor.map((v) => (
|
||||||
<li key={v.mondayUserId} className="flex items-center justify-between border border-neutral-200 px-4 py-3">
|
<li
|
||||||
|
key={v.mondayUserId}
|
||||||
|
className="flex items-center justify-between rounded-xl border border-[#E5E7EB] bg-white px-4 py-3 shadow-sm"
|
||||||
|
>
|
||||||
<p className="text-sm">{v.nombre ?? `Monday #${v.mondayUserId}`}</p>
|
<p className="text-sm">{v.nombre ?? `Monday #${v.mondayUserId}`}</p>
|
||||||
<span className="text-xs font-bold uppercase tracking-widest text-neutral-500">
|
<span className="rounded-full bg-gray-100 px-2.5 py-0.5 text-xs font-medium text-gray-600">
|
||||||
{v.leadsAsignados} lead(s)
|
{v.leadsAsignados} lead(s)
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
Reference in New Issue
Block a user