feat(pendientes): cotizador (mensualidades, bloqueo de unidad, totales, numMensualidades), cobranza (recibos, atrasos, mora configurable, recordatorios), soporte (adjuntos, filtros, estados), clientes maestro (catalogos reales), contactos (ficha y duplicados) y permisos de captura por rol
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { SkeletonList } from "../components/Skeleton"
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import {
|
||||
Bar,
|
||||
@@ -15,8 +15,7 @@ import {
|
||||
YAxis,
|
||||
} from 'recharts'
|
||||
import type { StatusPlan } from '../types'
|
||||
import { listarPlanes, miosPorDefecto, type ResultadoPlanes } from '../services/repository'
|
||||
import { getSesion } from '../services/auth'
|
||||
import { usePlanesCobranza } from '../lib/usePlanesCobranza'
|
||||
import {
|
||||
alCorriente,
|
||||
avancePlan,
|
||||
@@ -33,6 +32,7 @@ import { useHoy } from '../lib/useHoy'
|
||||
import MoraDashboard from "../components/MoraDashboard"
|
||||
import KpiCard from '../components/KpiCard'
|
||||
import SemaforoDot, { semaforoPlan } from '../components/Semaforo'
|
||||
import ToggleVista from '../components/ToggleVista'
|
||||
|
||||
const MESES_CORTOS = ['ene', 'feb', 'mar', 'abr', 'may', 'jun', 'jul', 'ago', 'sep', 'oct', 'nov', 'dic']
|
||||
const COLORES_PIE = ['#2563EB', '#CBD5E1']
|
||||
@@ -49,8 +49,6 @@ function claveMes(fecha: Date): string {
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [resultado, setResultado] = useState<ResultadoPlanes | null>(null)
|
||||
const [errorCarga, setErrorCarga] = useState<string | null>(null)
|
||||
// Filtros y ordenamiento persistidos en la URL para que sean compartibles
|
||||
const [searchParams, setSearchParams] = useSearchParams()
|
||||
const busqueda = searchParams.get('q') ?? ''
|
||||
@@ -59,30 +57,13 @@ export default function DashboardPage() {
|
||||
const paramOrden = searchParams.get('sort')
|
||||
const orden: ColumnaOrden | null = esColumnaOrden(paramOrden) ? paramOrden : null
|
||||
const dir: 'asc' | 'desc' = searchParams.get('dir') === 'desc' ? 'desc' : 'asc'
|
||||
// Toggle "Mis planes / Todos": persiste en la URL (?vista=mios|todos); sin
|
||||
// param, los vendedores arrancan en "Mis planes" y el resto en "Todos"
|
||||
const paramVista = searchParams.get('vista')
|
||||
const mios = paramVista !== null ? paramVista === 'mios' : miosPorDefecto(getSesion()?.usuario.rol)
|
||||
// Toggle "Mis planes / Todos" (persiste en ?vista=): lo comparte con
|
||||
// "Cobranza del día" y avisa si el usuario no tiene planes asignados
|
||||
const { mios, definirVista, resultado, errorCarga, sinAsignados } = usePlanesCobranza()
|
||||
// "Hoy" se recalcula al enfocar la ventana (evita atrasos congelados si la
|
||||
// pestaña queda abierta de un día para otro)
|
||||
const hoy = useHoy()
|
||||
|
||||
useEffect(() => {
|
||||
let activo = true
|
||||
setResultado(null)
|
||||
setErrorCarga(null)
|
||||
listarPlanes(undefined, mios)
|
||||
.then((r) => {
|
||||
if (activo) setResultado(r)
|
||||
})
|
||||
.catch((e: unknown) => {
|
||||
if (activo) setErrorCarga(e instanceof Error ? e.message : 'Error al cargar los planes')
|
||||
})
|
||||
return () => {
|
||||
activo = false
|
||||
}
|
||||
}, [mios])
|
||||
|
||||
const planes = useMemo(() => resultado?.planes ?? [], [resultado])
|
||||
const idsPlan = useMemo(() => new Set(planes.map((p) => p.id)), [planes])
|
||||
|
||||
@@ -239,6 +220,11 @@ export default function DashboardPage() {
|
||||
: 'Modo offline: sin conexión al API; los cambios se guardan solo en este navegador.'}
|
||||
</p>
|
||||
)}
|
||||
{sinAsignados && (
|
||||
<p className="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-800">
|
||||
No tienes planes asignados a tu usuario; se muestra el listado de <strong>todos</strong> los planes.
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* KPIs */}
|
||||
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 md:gap-6 lg:grid-cols-4">
|
||||
@@ -302,29 +288,8 @@ export default function DashboardPage() {
|
||||
<div className="overflow-hidden rounded-xl border border-[#E5E7EB] bg-white shadow-sm dark:border-[#1E293B] dark:bg-[#1E293B]">
|
||||
{/* Filtros */}
|
||||
<div className="flex flex-wrap items-center gap-3 border-b border-[#E5E7EB] p-4 md:gap-4 md: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 dark:border-[#334155] dark:bg-[#0F172A]" role="group" aria-label="Alcance de planes">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => actualizarParams({ vista: mios ? null : 'mios' })}
|
||||
aria-pressed={mios}
|
||||
className={`rounded-l-lg px-4 py-2.5 text-sm font-medium transition-all duration-100 ease-out ${
|
||||
mios ? 'bg-accent text-white shadow-sm' : 'bg-white text-[#64748B] dark:text-[#94A3B8] hover:text-[#111827] dark:hover:text-[#E2E8F0] dark:bg-[#0F172A] dark:hover:bg-[#1E293B]'
|
||||
}`}
|
||||
>
|
||||
Mis planes
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => actualizarParams({ vista: mios ? 'todos' : null })}
|
||||
aria-pressed={!mios}
|
||||
className={`rounded-r-lg px-4 py-2.5 text-sm font-medium transition-all duration-100 ease-out ${
|
||||
mios ? 'bg-white text-[#64748B] dark:text-[#94A3B8] hover:text-[#111827] dark:hover:text-[#E2E8F0] dark:bg-[#0F172A] dark:hover:bg-[#1E293B]' : 'bg-accent text-white shadow-sm'
|
||||
}`}
|
||||
>
|
||||
Todos
|
||||
</button>
|
||||
</div>
|
||||
{/* Toggle Mis planes / Todos (?vista= filtra en el API por el vendedor del token) */}
|
||||
<ToggleVista mios={mios} onCambiar={definirVista} />
|
||||
<label htmlFor="filtro-busqueda" className="sr-only">
|
||||
Buscar por cliente o unidad
|
||||
</label>
|
||||
@@ -348,6 +313,7 @@ export default function DashboardPage() {
|
||||
<option value="TODOS">Todos los status</option>
|
||||
<option value="ACTIVO">ACTIVO</option>
|
||||
<option value="LIQUIDADO">LIQUIDADO</option>
|
||||
<option value="SIN_PLAN">SIN PLAN CAPTURADO</option>
|
||||
<option value="CANCELADO">CANCELADO</option>
|
||||
</select>
|
||||
<label htmlFor="filtro-proyecto" className="sr-only">
|
||||
|
||||
Reference in New Issue
Block a user