feat(mejoras): 22 mejoras UX en todos los módulos
Mi día: cancelación con motivo, conteo escrituración real, cita sin retroceder, calendario completo Cobranza: MoraDashboard respeta alcance, hoy dinámico, cancelar con motivo Landing: contactos paginados, error vs vacío diferenciado Dashboards: KPI renombrado, drill-down planes activos Chat: historial recarga, preguntas rotativas, error solo toast Transversal: shadow-sm corrupto corregido en 9 archivos 553 API tests + 7 apps construidas
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react'
|
||||
import { Fragment, useEffect, useState } from 'react'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import type { Pago, PlanPago } from '../types'
|
||||
import {
|
||||
@@ -22,9 +22,11 @@ import {
|
||||
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() {
|
||||
@@ -35,11 +37,15 @@ export default function PlanDetallePage() {
|
||||
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)
|
||||
const hoy = useMemo(() => new Date(), [])
|
||||
// "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) {
|
||||
@@ -74,7 +80,7 @@ export default function PlanDetallePage() {
|
||||
|
||||
if (!plan) {
|
||||
return (
|
||||
<div className="rounded-xl border border-[#E5E7EB] bg-white p- dark:border-[#1E293B] dark:bg-[#1E293B] 16 text-center shadow-sm">
|
||||
<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
|
||||
@@ -111,14 +117,27 @@ export default function PlanDetallePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const cancelar = async () => {
|
||||
if (!window.confirm('¿Cancelar este plan? El saldo pendiente dejará de cobrarse.')) return
|
||||
/** 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')
|
||||
}
|
||||
}
|
||||
@@ -244,7 +263,7 @@ export default function PlanDetallePage() {
|
||||
</section>
|
||||
|
||||
{/* Tabla de pagos */}
|
||||
<div className="overflow-x-auto rounded-xl border border-[#E5E7EB] bg-white shadow dark:border-[#1E293B] dark:bg-[#1E293B] -sm">
|
||||
<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]">
|
||||
@@ -393,7 +412,7 @@ export default function PlanDetallePage() {
|
||||
</button>
|
||||
{status === 'ACTIVO' && saldo > 0 && (
|
||||
<button
|
||||
onClick={cancelar}
|
||||
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
|
||||
@@ -477,6 +496,15 @@ export default function PlanDetallePage() {
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user