Files
FRASA-CAS/apps/cobranza/src/pages/PlanDetallePage.tsx

635 lines
28 KiB
TypeScript

import { Fragment, useEffect, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import type { Pago, PlanPago } from '../types'
import {
actualizarPlanAsync,
agregarNotaAsync,
cancelarPlanAsync,
obtenerPlanPorId,
puedeEditarMora,
registrarAbonoAsync,
} from '../services/repository'
import { getSesion } from '../services/auth'
import {
avancePlan,
diasAtraso,
moraDePago,
pagadoTotal,
proximoVencimiento,
round2,
saldoPlan,
statusPago,
statusPlan,
totalMora,
} from '../lib/cobranza'
import { formatoFechaCorta, formatoMoneda, formatoPorcentaje } from '../lib/formatters'
import { descargar } from '../lib/pdf/estadoCuenta'
import { descargarRecibo } from '../lib/pdf/reciboAbono'
import { useHoy } from '../lib/useHoy'
import BadgeStatus from '../components/BadgeStatus'
import KpiCard from '../components/KpiCard'
import ModalAbono from '../components/ModalAbono'
import ModalCancelarPlan from '../components/ModalCancelarPlan'
import TimelinePagos from '../components/TimelinePagos'
export default function PlanDetallePage() {
const { id } = useParams<{ id: string }>()
const navigate = useNavigate()
const [plan, setPlan] = useState<PlanPago | undefined>(undefined)
const [cargando, setCargando] = useState(true)
const [offline, setOffline] = useState(false)
const [errorCarga, setErrorCarga] = useState<string | null>(null)
const [pagoAbono, setPagoAbono] = useState<Pago | null>(null)
const [modalCancelar, setModalCancelar] = useState(false)
const [cancelando, setCancelando] = useState(false)
const [expandidos, setExpandidos] = useState<Set<string>>(new Set())
const [error, setError] = useState<string | null>(null)
const [notaTexto, setNotaTexto] = useState('')
const [errorNota, setErrorNota] = useState<string | null>(null)
// Captura de la tasa de mora (vive en el plan, editable por admin/manager/dirección)
const [tasaInput, setTasaInput] = useState('0')
const [guardandoMora, setGuardandoMora] = useState(false)
const [errorMora, setErrorMora] = useState<string | null>(null)
const [avisoMora, setAvisoMora] = useState<string | null>(null)
const puedeEditarTasa = puedeEditarMora(getSesion()?.usuario.rol)
// "Hoy" se recalcula al enfocar la ventana (evita atrasos/mora congelados si
// la pestaña queda abierta de un día para otro)
const hoy = useHoy()
useEffect(() => {
if (!id) {
setCargando(false)
return
}
let activo = true
obtenerPlanPorId(id)
.then((r) => {
if (!activo) return
setPlan(r.plan)
setOffline(r.origen === 'local')
})
.catch((e: unknown) => {
if (activo) setErrorCarga(e instanceof Error ? e.message : 'Error al cargar el plan')
})
.finally(() => {
if (activo) setCargando(false)
})
return () => {
activo = false
}
}, [id])
// La caja de la tasa se prellena al cargar el plan (o 0 si no tiene). No se
// reinicia con cada recarga del plan para no borrar lo que el usuario escribe.
useEffect(() => {
if (plan) setTasaInput(String(plan.tasaMoraMensual ?? 0))
}, [plan?.id])
if (cargando) {
return <p className="text-sm text-[#64748B] dark:text-[#94A3B8]">Cargando...</p>
}
if (errorCarga) {
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) {
return (
<div className="rounded-xl border border-[#E5E7EB] bg-white p-16 text-center shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
<p className="text-[#64748B] dark:text-[#94A3B8]">Plan no encontrado.</p>
<Link to="/" className="mt-4 inline-block text-accent hover:underline">
Volver al dashboard
</Link>
</div>
)
}
const status = statusPlan(plan)
const saldo = saldoPlan(plan)
const atraso = diasAtraso(plan, hoy)
const tasaMora = plan.tasaMoraMensual ?? 0
const mora = totalMora(plan, hoy)
// Mora que resultaría con lo capturado en la caja (vista previa antes de guardar)
const tasaCapturada = Number(tasaInput)
const tasaCapturadaValida = Number.isFinite(tasaCapturada) && tasaCapturada >= 0 && tasaCapturada <= 100
const moraConTasaCapturada = tasaCapturadaValida ? totalMora({ ...plan, tasaMoraMensual: tasaCapturada }, hoy) : 0
const tasaSinCambios = tasaCapturadaValida && round2(tasaCapturada) === round2(tasaMora)
const toggleExpandido = (pagoId: string) => {
setExpandidos((prev) => {
const next = new Set(prev)
if (next.has(pagoId)) next.delete(pagoId)
else next.add(pagoId)
return next
})
}
const guardarAbono = async (datos: { monto: number; fechaPago: string; referencia: string }) => {
if (!pagoAbono) return
try {
const r = await registrarAbonoAsync(plan.id, datos, pagoAbono.id)
setPlan(r.plan)
setOffline(r.origen === 'local')
setPagoAbono(null)
setError(null)
} catch (e) {
setError(e instanceof Error ? e.message : 'Error al registrar el abono')
}
}
/** Cancela el plan y registra el motivo (+comentario) en la bitácora */
const confirmarCancelacion = async (motivo: string, comentario: string) => {
if (!plan) return
setCancelando(true)
try {
const r = await cancelarPlanAsync(plan.id)
setPlan(r.plan)
setOffline(r.origen === 'local')
setError(null)
setCancelando(false)
setModalCancelar(false)
// El motivo queda asentado en la bitácora (best-effort)
const detalle = `Plan cancelado. Motivo: ${motivo}${comentario !== '' ? ` — ${comentario}` : ''}`
try {
const rn = await agregarNotaAsync(r.plan, detalle)
setPlan(rn.plan)
} catch {
// la cancelación ya se aplicó; la nota es complementaria
}
} catch (e) {
setCancelando(false)
setError(e instanceof Error ? e.message : 'Error al cancelar el plan')
}
}
const guardarNota = async () => {
try {
const r = await agregarNotaAsync(plan, notaTexto)
setPlan(r.plan)
setOffline(r.origen === 'local')
setNotaTexto('')
setErrorNota(null)
} catch (e) {
setErrorNota(e instanceof Error ? e.message : 'Error al guardar la nota')
}
}
/** Guarda la tasa de mora mensual del plan (PATCH /planes/:id) */
const guardarTasaMora = async () => {
if (!tasaCapturadaValida) {
setErrorMora('Captura una tasa mensual entre 0 y 100 (%)')
return
}
setGuardandoMora(true)
setErrorMora(null)
setAvisoMora(null)
try {
const r = await actualizarPlanAsync(plan.id, { tasaMoraMensual: round2(tasaCapturada) })
setPlan(r.plan)
setOffline(r.origen === 'local')
setAvisoMora(
r.origen === 'local'
? 'Sin conexión al API: la tasa se guardó solo en este navegador.'
: 'Tasa de mora actualizada.',
)
} catch (e) {
setErrorMora(e instanceof Error ? e.message : 'Error al guardar la tasa de mora')
} finally {
setGuardandoMora(false)
}
}
const avance = avancePlan(plan)
/** Siguiente pago sin cubrir (futuro próximo o vencido más antiguo) */
const proximoPago = status === 'ACTIVO' ? proximoVencimiento(plan, hoy) : null
/** Abonar al próximo pago: hace scroll a su fila y abre el modal prellenado */
const abonarProximoPago = (pago: Pago) => {
document.getElementById(`pago-${pago.id}`)?.scrollIntoView({ behavior: 'smooth', block: 'center' })
setPagoAbono(pago)
}
return (
<div className="space-y-6">
<div className="flex flex-wrap items-start justify-between gap-4">
<div>
<Link to="/" className="text-sm text-accent hover:underline">
← Volver al dashboard
</Link>
<h1 className="mt-2 text-xl font-semibold text-[#111827] dark:text-[#E2E8F0]">{plan.clienteNombre}</h1>
<p className="mt-1 text-sm text-[#64748B] dark:text-[#94A3B8]">
{plan.unidadCodigo} · {plan.proyecto} · Venta {plan.ventaRef || '—'} ·{' '}
<span className="font-semibold text-[#111827] dark:text-[#E2E8F0]">
{status === 'SIN_PLAN' ? 'SIN PLAN CAPTURADO' : status}
</span>
{atraso > 0 && <span className="text-red-700"> · {atraso} días de atraso</span>}
</p>
<p className="mt-1 text-sm text-[#64748B] dark:text-[#94A3B8]">
Tel. {plan.clienteTelefono || '—'}
{tasaMora > 0 && <span> · Mora {tasaMora}% mensual</span>}
</p>
</div>
{proximoPago && (
<button
type="button"
onClick={() => abonarProximoPago(proximoPago)}
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"
aria-label="Registrar abono al próximo pago no cubierto"
>
Abonar al próximo pago
</button>
)}
</div>
{offline && (
<p className="text-xs text-amber-700">
Modo offline: sin conexión al API; los cambios se guardan solo en este navegador.
</p>
)}
{status === 'SIN_PLAN' && (
<div className="rounded-lg border border-orange-200 bg-orange-50 px-4 py-3 text-sm text-orange-800">
<strong>Sin plan capturado:</strong> este plan no tiene calendario de pagos (enganche y
mensualidades en cero), así que no aparece en “Cobranza del día” ni en el reporte de mora. No es
un plan liquidado: hay que capturar su calendario para poder cobrarlo.
</div>
)}
{error && (
<div className="rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-700">{error}</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 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm sm:p-6"
aria-labelledby="titulo-resumen"
>
<h2 id="titulo-resumen" className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
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] dark:text-[#94A3B8]">Avance del plan</span>
<span className="font-semibold text-[#111827] dark:text-[#E2E8F0]">{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>
{/* Timeline visual de pagos */}
<section className="rounded-xl border border-[#E5E7EB] bg-white p-5 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm" aria-labelledby="titulo-timeline">
<h2 id="titulo-timeline" className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
Línea de pagos
</h2>
<div className="mt-4">
<TimelinePagos pagos={plan.pagos} />
</div>
</section>
{/* Tabla de pagos */}
<div className="overflow-x-auto rounded-xl border border-[#E5E7EB] bg-white shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
<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] dark:text-[#94A3B8]">
<th scope="col" className="px-4 py-3">#</th>
<th scope="col" className="px-4 py-3">Vencimiento</th>
<th scope="col" className="px-4 py-3 text-right">Monto</th>
<th scope="col" className="px-4 py-3 text-right">Pagado</th>
<th scope="col" className="px-4 py-3 text-right">Restante</th>
{tasaMora > 0 && <th scope="col" className="px-4 py-3 text-right">Mora</th>}
<th scope="col" className="px-4 py-3">Status</th>
<th scope="col" className="px-4 py-3">Abonos</th>
<th scope="col" className="px-4 py-3" />
</tr>
</thead>
<tbody className="divide-y divide-[#E5E7EB]">
{plan.pagos.map((pago) => {
const st = statusPago(pago, hoy)
const restante = round2(pago.monto - pago.montoPagado)
const moraPago = tasaMora > 0 ? moraDePago(pago, tasaMora, hoy) : 0
const expandido = expandidos.has(pago.id)
return (
<Fragment key={pago.id}>
<tr id={`pago-${pago.id}`} className="transition-colors duration-100 ease-out hover:bg-[#F8FAFC] dark:bg-[#0F172A]">
<td className="px-4 py-3 font-medium">{pago.numPago}</td>
<td className="px-4 py-3">{formatoFechaCorta(pago.fechaVencimiento)}</td>
<td className="px-4 py-3 text-right">{formatoMoneda(pago.monto)}</td>
<td className="px-4 py-3 text-right">{formatoMoneda(pago.montoPagado)}</td>
<td className="px-4 py-3 text-right font-medium">{formatoMoneda(restante)}</td>
{tasaMora > 0 && (
<td className="px-4 py-3 text-right">
{moraPago > 0 ? (
<span className="text-red-700">{formatoMoneda(moraPago)}</span>
) : (
<span className="text-neutral-400">—</span>
)}
</td>
)}
<td className="px-4 py-3">
<BadgeStatus status={st} />
</td>
<td className="px-4 py-3">
{pago.aplicaciones.length > 0 ? (
<button
onClick={() => toggleExpandido(pago.id)}
className="text-accent hover:underline"
>
{expandido ? 'Ocultar' : `Ver (${pago.aplicaciones.length})`}
</button>
) : (
<span className="text-neutral-400">—</span>
)}
</td>
<td className="px-4 py-3 text-right">
{st !== 'PAGADO' && status === 'ACTIVO' && (
<button
onClick={() => setPagoAbono(pago)}
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
</button>
)}
</td>
</tr>
{expandido && (
<tr className="bg-[#F8FAFC] dark:bg-[#0F172A]">
<td colSpan={tasaMora > 0 ? 9 : 8} className="px-4 py-4">
{/* Detalle del pago vencido */}
<div className="mb-3 grid grid-cols-2 gap-2 text-xs sm:grid-cols-4">
<div>
<span className="font-semibold text-[#64748B] dark:text-[#94A3B8]">Vencimiento:</span>{' '}
<span className="text-[#111827] dark:text-[#E2E8F0]">{formatoFechaCorta(pago.fechaVencimiento.slice(0, 10))}</span>
</div>
<div>
<span className="font-semibold text-[#64748B] dark:text-[#94A3B8]">Monto:</span>{' '}
<span className="text-[#111827] dark:text-[#E2E8F0]">{formatoMoneda(pago.monto)}</span>
</div>
<div>
<span className="font-semibold text-[#64748B] dark:text-[#94A3B8]">Pagado:</span>{' '}
<span className="text-[#111827] dark:text-[#E2E8F0]">{formatoMoneda(pago.montoPagado)}</span>
</div>
<div>
<span className="font-semibold text-[#64748B] dark:text-[#94A3B8]">Restante:</span>{' '}
<span className={`font-medium ${restante > 0 ? 'text-red-700' : 'text-green-700'}`}>{formatoMoneda(restante)}</span>
</div>
</div>
{/* Historial de abonos */}
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
Historial de abonos
</p>
<table className="w-full text-xs">
<thead>
<tr className="text-left font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
<th scope="col" className="py-1">Fecha pago</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">Referencia</th>
<th scope="col" className="py-1 text-right">Recibo</th>
</tr>
</thead>
<tbody>
{pago.aplicaciones.map((a) => (
<tr key={a.id} className="border-t border-[#E5E7EB] dark:border-[#1E293B]">
<td className="py-1">{formatoFechaCorta(a.fechaPago.slice(0, 10))}</td>
<td className="py-1 text-[#94A3B8]">{formatoFechaCorta(pago.fechaVencimiento.slice(0, 10))}</td>
<td className="py-1 text-right">{formatoMoneda(a.monto)}</td>
<td className="py-1">{a.referencia || '—'}</td>
<td className="py-1 text-right">
<button
onClick={() => descargarRecibo(plan, pago, a)}
aria-label={`Descargar recibo PDF del abono ${a.referencia}`}
className="text-accent hover:underline"
>
Recibo PDF
</button>
</td>
</tr>
))}
</tbody>
</table>
</td>
</tr>
)}
</Fragment>
)
})}
</tbody>
</table>
</div>
</div>
{/* 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 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm"
aria-labelledby="titulo-acciones"
>
<h2 id="titulo-acciones" className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
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 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
>
Estado de cuenta PDF
</button>
{status === 'ACTIVO' && saldo > 0 && (
<button
onClick={() => setModalCancelar(true)}
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>
{/* Mora del plan: la tasa vive en el plan y la editan admin/manager/dirección */}
<section
className="rounded-xl border border-[#E5E7EB] bg-white p-5 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm"
aria-labelledby="titulo-mora"
>
<h2 id="titulo-mora" className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
Mora por atraso
</h2>
{puedeEditarTasa ? (
<form
className="mt-4"
onSubmit={(e) => {
e.preventDefault()
void guardarTasaMora()
}}
>
<label htmlFor="tasa-mora" className="block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]">
Tasa de mora mensual (%)
</label>
<input
id="tasa-mora"
type="number"
min={0}
max={100}
step="0.01"
inputMode="decimal"
value={tasaInput}
onChange={(e) => {
setTasaInput(e.target.value)
setErrorMora(null)
setAvisoMora(null)
}}
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 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0]"
/>
<p className="mt-1 text-xs text-[#64748B] dark:text-[#94A3B8]">
La tasa es <strong>mensual en %</strong> y se prorratea por día (tasa ÷ 30) sobre el restante
de cada pago vencido, incluidos los parciales vencidos. 0 = sin mora.
</p>
<p className="mt-2 text-xs text-[#64748B] dark:text-[#94A3B8]">
Mora acumulada con esta tasa:{' '}
<span className="font-semibold text-[#111827] dark:text-[#E2E8F0]">
{formatoMoneda(moraConTasaCapturada)}
</span>
</p>
{errorMora && <p className="mt-1 text-xs text-red-700">{errorMora}</p>}
{avisoMora && <p className="mt-1 text-xs text-green-700">{avisoMora}</p>}
<div className="mt-4 flex justify-end">
<button
type="submit"
disabled={guardandoMora || !tasaCapturadaValida || tasaSinCambios}
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"
>
{guardandoMora ? 'Guardando…' : 'Guardar tasa'}
</button>
</div>
</form>
) : (
<div className="mt-4 space-y-2 text-sm text-[#64748B] dark:text-[#94A3B8]">
<p>
Tasa: <span className="font-semibold text-[#111827] dark:text-[#E2E8F0]">{tasaMora}% mensual</span>
</p>
<p>
Mora acumulada: <span className="font-semibold text-[#111827] dark:text-[#E2E8F0]">{formatoMoneda(mora)}</span>
</p>
<p className="text-xs">Solo admin, manager y dirección pueden modificar la tasa.</p>
</div>
)}
</section>
{/* Bitácora del plan */}
<section className="rounded-xl border border-[#E5E7EB] bg-white p-5 dark:border-[#1E293B] dark:bg-[#1E293B] shadow-sm" aria-labelledby="titulo-bitacora">
<h2 id="titulo-bitacora" className="text-xs font-semibold uppercase tracking-wider text-[#64748B] dark:text-[#94A3B8]">
Bitácora
</h2>
{(plan.notas ?? []).length === 0 ? (
<p className="mt-6 text-sm text-[#64748B] dark:text-[#94A3B8]">Sin notas registradas.</p>
) : (
<ul className="mt-6 space-y-4">
{[...(plan.notas ?? [])]
.sort((a, b) => b.fecha.localeCompare(a.fecha))
.map((nota) => (
<li key={nota.id} className="border-l-2 border-[#E5E7EB] pl-4">
<p className="text-xs text-[#64748B] dark:text-[#94A3B8]">
{new Date(nota.fecha).toLocaleString('es-MX', {
day: '2-digit',
month: '2-digit',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
})}
</p>
<p className="mt-1 text-sm text-[#111827] dark:text-[#E2E8F0]">{nota.texto}</p>
</li>
))}
</ul>
)}
<form
className="mt-6"
onSubmit={(e) => {
e.preventDefault()
void guardarNota()
}}
>
<label
htmlFor="nota-texto"
className="block text-xs font-medium text-[#4B5563] dark:text-[#CBD5E1]"
>
Nueva nota
</label>
<textarea
id="nota-texto"
value={notaTexto}
onChange={(e) => setNotaTexto(e.target.value)}
rows={3}
placeholder="Gestión realizada, acuerdos con el cliente…"
className="mt-1 w-full rounded-lg border border-[#D1D5DB] bg-white px-3 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] 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>}
<div className="mt-4 flex justify-end">
<button
type="submit"
disabled={notaTexto.trim().length < 3}
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
</button>
</div>
</form>
</section>
</aside>
</div>
{status === 'CANCELADO' && (
<button
onClick={() => void navigate('/')}
className="rounded-lg border border-[#D1D5DB] bg-white px-5 dark:border-[#334155] dark:bg-[#0F172A] dark:text-[#E2E8F0] py-2.5 text-sm font-medium text-[#374151] dark:text-[#CBD5E1] transition-all duration-100 ease-out hover:bg-[#F9FAFB]"
>
Volver
</button>
)}
{pagoAbono && (
<ModalAbono pago={pagoAbono} onCancelar={() => setPagoAbono(null)} onGuardar={guardarAbono} />
)}
{modalCancelar && plan && (
<ModalCancelarPlan
clienteNombre={plan.clienteNombre}
enviando={cancelando}
onCancelar={() => setModalCancelar(false)}
onConfirmar={(motivo, comentario) => void confirmarCancelacion(motivo, comentario)}
/>
)}
</div>
)
}