feat: API Gateway (Express 5 + Prisma + Monday) y conexion de apps al API
- api-gateway: servicio Monday (retry 429/complejidad, paginacion cursor, column IDs por titulo), endpoints unidades/cotizaciones/clientes/planes, webhook con handshake challenge, Prisma SQLite dev, 55 tests - cotizador: inventario desde /api/unidades con fallback a seed - alta-clientes: POST /api/clientes + upload multipart de documentos - cobranza: persistencia via API con respaldo offline en localStorage - .env.example con board IDs del workspace Frasa-CAS
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import {
|
||||
Bar,
|
||||
@@ -13,8 +13,8 @@ import {
|
||||
XAxis,
|
||||
YAxis,
|
||||
} from 'recharts'
|
||||
import type { PlanPago, StatusPlan } from '../types'
|
||||
import { cargarPlanes } from '../services/repository'
|
||||
import type { StatusPlan } from '../types'
|
||||
import { listarPlanes, type ResultadoPlanes } from '../services/repository'
|
||||
import {
|
||||
alCorriente,
|
||||
avancePlan,
|
||||
@@ -45,7 +45,8 @@ function claveMes(fecha: Date): string {
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [planes] = useState<PlanPago[]>(() => cargarPlanes())
|
||||
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') ?? ''
|
||||
@@ -56,6 +57,22 @@ export default function DashboardPage() {
|
||||
const dir: 'asc' | 'desc' = searchParams.get('dir') === 'desc' ? 'desc' : 'asc'
|
||||
const hoy = useMemo(() => new Date(), [])
|
||||
|
||||
useEffect(() => {
|
||||
let activo = true
|
||||
listarPlanes()
|
||||
.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
|
||||
}
|
||||
}, [])
|
||||
|
||||
const planes = useMemo(() => resultado?.planes ?? [], [resultado])
|
||||
|
||||
const actualizarParams = (cambios: Record<string, string | null>) => {
|
||||
setSearchParams(
|
||||
(prev) => {
|
||||
@@ -134,6 +151,14 @@ export default function DashboardPage() {
|
||||
}))
|
||||
}, [planes, hoy])
|
||||
|
||||
if (!resultado) {
|
||||
return errorCarga ? (
|
||||
<div className="border border-red-300 bg-white px-4 py-3 text-sm text-red-700">{errorCarga}</div>
|
||||
) : (
|
||||
<p className="text-sm text-neutral-400">Cargando...</p>
|
||||
)
|
||||
}
|
||||
|
||||
const filtrados = derivados.filter((d) => {
|
||||
if (filtroStatus !== 'TODOS' && d.status !== filtroStatus) return false
|
||||
if (filtroProyecto !== 'TODOS' && d.plan.proyecto !== filtroProyecto) return false
|
||||
@@ -193,6 +218,15 @@ export default function DashboardPage() {
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Aviso discreto cuando los datos no vienen del API */}
|
||||
{resultado.origen === 'local' && (
|
||||
<p className="text-xs text-amber-700">
|
||||
{resultado.planesSoloLocales
|
||||
? 'El API no tiene planes registrados. Los planes guardados en este navegador se conservan solo localmente (no se migraron automáticamente).'
|
||||
: 'Modo offline: sin conexión al API; los cambios se guardan solo en este navegador.'}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* KPIs */}
|
||||
<div className="grid grid-cols-2 gap-6 lg:grid-cols-4">
|
||||
<KpiCard titulo="Planes activos" valor={activos.length} />
|
||||
|
||||
Reference in New Issue
Block a user