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:
2026-08-21 23:39:17 +00:00
parent ecd4edac02
commit 0546f35d2a
48 changed files with 7129 additions and 66 deletions

View File

@@ -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} />