style: rediseno de layout profesional en las 7 apps
Headers sticky con marca y acciones, contenedores max-w-[1280px], grids de contenido con rails laterales sticky (cotizador, mi-dia, detalle de cobranza), stepper vertical en el wizard de alta, toolbar+ tabla unificadas en cobranza, tabs como segmented control en dashboards, chat con barra de entrada fija y preguntas sugeridas, landing con navbar + hero + grid de modulos + footer.
This commit is contained in:
@@ -1,8 +1,23 @@
|
||||
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,
|
||||
plano = false,
|
||||
}: {
|
||||
titulo: string
|
||||
valor: ReactNode
|
||||
detalle?: string
|
||||
/** `plano` quita sombra/hover para anidar la tarjeta dentro de otra tarjeta */
|
||||
plano?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-xl border border-[#E5E7EB] bg-white p-6 shadow-sm transition-shadow duration-200 ease-in-out hover:shadow-md">
|
||||
<div
|
||||
className={`rounded-xl border border-[#E5E7EB] bg-white p-6 ${
|
||||
plano ? '' : 'shadow-sm transition-shadow duration-200 ease-in-out hover:shadow-md'
|
||||
}`}
|
||||
>
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">{titulo}</p>
|
||||
<p className="mt-2 text-3xl font-light text-[#111827]">{valor}</p>
|
||||
{detalle && <p className="mt-2 text-xs text-[#64748B]">{detalle}</p>}
|
||||
|
||||
@@ -4,14 +4,14 @@ import { Link } from 'react-router-dom'
|
||||
export default function Layout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#F8FAFC] text-[#111827]">
|
||||
<header className="border-b border-[#E5E7EB] bg-white">
|
||||
<div className="mx-auto flex max-w-6xl items-center justify-between px-4 py-4">
|
||||
<header className="sticky top-0 z-10 border-b border-[#E5E7EB] bg-white/95 backdrop-blur">
|
||||
<div className="mx-auto flex h-16 max-w-[1280px] items-center justify-between px-4 sm:px-6">
|
||||
<Link to="/" className="text-sm tracking-widest">
|
||||
<span className="font-bold">FRASA</span>
|
||||
<span className="text-[#F97316]">.</span>{' '}
|
||||
<span className="font-light text-[#64748B]">COBRANZA</span>
|
||||
</Link>
|
||||
<nav className="flex items-center gap-6 text-sm">
|
||||
<nav className="flex items-center gap-3 text-sm sm:gap-6">
|
||||
<Link
|
||||
to="/"
|
||||
className="text-[#64748B] transition-colors duration-150 hover:text-[#111827]"
|
||||
@@ -20,20 +20,20 @@ export default function Layout({ children }: { children: ReactNode }) {
|
||||
</Link>
|
||||
<Link
|
||||
to="/hoy"
|
||||
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"
|
||||
className="rounded-lg border border-accent px-3 py-2 text-sm font-medium text-accent transition-all duration-100 ease-out hover:bg-accent hover:text-white sm:px-5 sm:py-2.5"
|
||||
>
|
||||
Cobranza del día
|
||||
</Link>
|
||||
<Link
|
||||
to="/nuevo"
|
||||
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"
|
||||
className="rounded-lg bg-accent px-3 py-2 text-sm font-medium text-white shadow-sm transition-all duration-100 ease-out hover:bg-accent-hover sm:px-5 sm:py-2.5"
|
||||
>
|
||||
Nuevo plan
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main className="mx-auto max-w-6xl px-4 py-10">{children}</main>
|
||||
<main className="mx-auto max-w-[1280px] px-4 py-8 sm:px-6">{children}</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -214,9 +214,9 @@ export default function DashboardPage() {
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="text-3xl font-light text-[#111827]">Módulo Cobranza — Planes de enganche</h1>
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 className="text-xl font-semibold text-[#111827]">Módulo Cobranza — Planes de enganche</h1>
|
||||
<Link
|
||||
to="/nuevo"
|
||||
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"
|
||||
@@ -292,8 +292,10 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filtros */}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{/* Tarjeta unificada: toolbar de filtros/búsqueda + tabla de planes */}
|
||||
<div className="overflow-hidden rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
|
||||
{/* Filtros */}
|
||||
<div className="flex flex-wrap items-center gap-3 border-b border-[#E5E7EB] p-4 sm:gap-4 sm:px-5">
|
||||
{/* Toggle Mis planes / Todos (?mios=1 filtra en el API por el vendedor del token) */}
|
||||
<div className="flex rounded-lg border border-[#D1D5DB] bg-white" role="group" aria-label="Alcance de planes">
|
||||
<button
|
||||
@@ -358,10 +360,10 @@ export default function DashboardPage() {
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabla de planes */}
|
||||
<div className="overflow-x-auto rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
|
||||
{/* Tabla de planes */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-[#E5E7EB] bg-[#F8FAFC] text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
@@ -417,6 +419,7 @@ export default function DashboardPage() {
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -43,9 +43,9 @@ export default function HoyPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-light text-[#111827]">Cobranza del día</h1>
|
||||
<h1 className="text-xl font-semibold text-[#111827]">Cobranza del día</h1>
|
||||
<p className="mt-1 text-sm text-[#64748B]">
|
||||
Pagos vencidos y vencimientos de los próximos 7 días, en todos los planes activos.
|
||||
</p>
|
||||
|
||||
@@ -88,8 +88,8 @@ export default function NuevoPlanPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl space-y-8">
|
||||
<h1 className="text-3xl font-light text-[#111827]">Nuevo plan de enganche</h1>
|
||||
<div className="mx-auto max-w-3xl space-y-6">
|
||||
<h1 className="text-xl font-semibold text-[#111827]">Nuevo plan de enganche</h1>
|
||||
|
||||
<form
|
||||
className="rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm"
|
||||
|
||||
@@ -133,40 +133,24 @@ export default function PlanDetallePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const avance = avancePlan(plan)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-wrap items-start justify-between gap-6">
|
||||
<div>
|
||||
<Link to="/" className="text-sm text-accent hover:underline">
|
||||
← Volver al dashboard
|
||||
</Link>
|
||||
<h1 className="mt-2 text-3xl font-light text-[#111827]">{plan.clienteNombre}</h1>
|
||||
<p className="mt-1 text-sm text-[#64748B]">
|
||||
{plan.unidadCodigo} · {plan.proyecto} · Venta {plan.ventaRef || '—'} ·{' '}
|
||||
<span className="font-semibold text-[#111827]">{status}</span>
|
||||
{atraso > 0 && <span className="text-red-700"> · {atraso} días de atraso</span>}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-[#64748B]">
|
||||
Tel. {plan.clienteTelefono || '—'}
|
||||
{tasaMora > 0 && <span> · Mora {tasaMora}% mensual</span>}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={() => descargar(plan)}
|
||||
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
|
||||
</button>
|
||||
{status === 'ACTIVO' && saldo > 0 && (
|
||||
<button
|
||||
onClick={cancelar}
|
||||
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
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Link to="/" className="text-sm text-accent hover:underline">
|
||||
← Volver al dashboard
|
||||
</Link>
|
||||
<h1 className="mt-2 text-xl font-semibold text-[#111827]">{plan.clienteNombre}</h1>
|
||||
<p className="mt-1 text-sm text-[#64748B]">
|
||||
{plan.unidadCodigo} · {plan.proyecto} · Venta {plan.ventaRef || '—'} ·{' '}
|
||||
<span className="font-semibold text-[#111827]">{status}</span>
|
||||
{atraso > 0 && <span className="text-red-700"> · {atraso} días de atraso</span>}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-[#64748B]">
|
||||
Tel. {plan.clienteTelefono || '—'}
|
||||
{tasaMora > 0 && <span> · Mora {tasaMora}% mensual</span>}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{offline && (
|
||||
@@ -179,21 +163,52 @@ export default function PlanDetallePage() {
|
||||
<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 */}
|
||||
<div className="grid grid-cols-2 gap-6 lg:grid-cols-5">
|
||||
<KpiCard titulo="Enganche total" valor={formatoMoneda(plan.engancheTotal)} />
|
||||
<KpiCard titulo="Pagado" valor={formatoMoneda(pagadoTotal(plan))} />
|
||||
<KpiCard titulo="Saldo" valor={formatoMoneda(saldo)} />
|
||||
<KpiCard titulo="% avance" valor={formatoPorcentaje(avancePlan(plan))} />
|
||||
<KpiCard titulo="Mensualidad" valor={formatoMoneda(plan.mensualidad)} detalle={`${plan.numPagos} pagos`} />
|
||||
{mora > 0 && (
|
||||
<KpiCard
|
||||
titulo="Mora acumulada"
|
||||
valor={<span className="text-red-700">{formatoMoneda(mora)}</span>}
|
||||
detalle={`Tasa ${tasaMora}% mensual`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* En lg: columna principal (flex-1) + rail derecho sticky de ~340px */}
|
||||
<div className="space-y-6 lg:flex lg:items-start lg:gap-6 lg:space-y-0">
|
||||
{/* Columna principal */}
|
||||
<div className="min-w-0 flex-1 space-y-6">
|
||||
{/* Resumen del plan */}
|
||||
<section
|
||||
className="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-sm sm:p-6"
|
||||
aria-labelledby="titulo-resumen"
|
||||
>
|
||||
<h2 id="titulo-resumen" className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
Resumen del plan
|
||||
</h2>
|
||||
<div className="mt-5 grid grid-cols-2 gap-4 sm:grid-cols-3">
|
||||
<KpiCard plano titulo="Enganche total" valor={formatoMoneda(plan.engancheTotal)} />
|
||||
<KpiCard plano titulo="Pagado" valor={formatoMoneda(pagadoTotal(plan))} />
|
||||
<KpiCard plano titulo="Saldo" valor={formatoMoneda(saldo)} />
|
||||
<KpiCard plano titulo="% avance" valor={formatoPorcentaje(avance)} />
|
||||
<KpiCard
|
||||
plano
|
||||
titulo="Mensualidad"
|
||||
valor={formatoMoneda(plan.mensualidad)}
|
||||
detalle={`${plan.numPagos} pagos`}
|
||||
/>
|
||||
{mora > 0 && (
|
||||
<KpiCard
|
||||
plano
|
||||
titulo="Mora acumulada"
|
||||
valor={<span className="text-red-700">{formatoMoneda(mora)}</span>}
|
||||
detalle={`Tasa ${tasaMora}% mensual`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{/* Barra de avance */}
|
||||
<div className="mt-6">
|
||||
<div className="flex items-center justify-between text-xs">
|
||||
<span className="font-medium text-[#64748B]">Avance del plan</span>
|
||||
<span className="font-semibold text-[#111827]">{formatoPorcentaje(avance)}</span>
|
||||
</div>
|
||||
<div className="mt-2 h-2 rounded-full bg-[#E5E7EB]">
|
||||
<div
|
||||
className="h-full rounded-full bg-accent"
|
||||
style={{ width: `${Math.min(100, avance * 100)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Tabla de pagos */}
|
||||
<div className="overflow-x-auto rounded-xl border border-[#E5E7EB] bg-white shadow-sm">
|
||||
@@ -299,10 +314,38 @@ export default function PlanDetallePage() {
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bitácora del plan */}
|
||||
<section className="rounded-xl border border-[#E5E7EB] bg-white p-8 shadow-sm" aria-labelledby="titulo-bitacora">
|
||||
{/* Rail derecho: acciones y bitácora */}
|
||||
<aside className="space-y-6 lg:sticky lg:top-20 lg:w-[340px] lg:shrink-0">
|
||||
<section
|
||||
className="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-sm"
|
||||
aria-labelledby="titulo-acciones"
|
||||
>
|
||||
<h2 id="titulo-acciones" className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
Acciones
|
||||
</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
<button
|
||||
onClick={() => descargar(plan)}
|
||||
className="w-full 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
|
||||
</button>
|
||||
{status === 'ACTIVO' && saldo > 0 && (
|
||||
<button
|
||||
onClick={cancelar}
|
||||
className="w-full 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
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Bitácora del plan */}
|
||||
<section className="rounded-xl border border-[#E5E7EB] bg-white p-5 shadow-sm" aria-labelledby="titulo-bitacora">
|
||||
<h2 id="titulo-bitacora" className="text-xs font-semibold uppercase tracking-wider text-[#64748B]">
|
||||
Bitácora
|
||||
</h2>
|
||||
@@ -329,7 +372,7 @@ export default function PlanDetallePage() {
|
||||
</ul>
|
||||
)}
|
||||
<form
|
||||
className="mt-8"
|
||||
className="mt-6"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
void guardarNota()
|
||||
@@ -360,7 +403,9 @@ export default function PlanDetallePage() {
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</section>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
{status === 'CANCELADO' && (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user