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,7 +1,12 @@
|
||||
import { Fragment, useMemo, useState } from 'react'
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react'
|
||||
import { Link, useNavigate, useParams } from 'react-router-dom'
|
||||
import type { Pago, PlanPago } from '../types'
|
||||
import { agregarNota, cancelarPlan, obtenerPlan, registrarAbonoEnPlan } from '../services/repository'
|
||||
import {
|
||||
agregarNotaAsync,
|
||||
cancelarPlanAsync,
|
||||
obtenerPlanPorId,
|
||||
registrarAbonoAsync,
|
||||
} from '../services/repository'
|
||||
import {
|
||||
avancePlan,
|
||||
diasAtraso,
|
||||
@@ -23,7 +28,10 @@ import ModalAbono from '../components/ModalAbono'
|
||||
export default function PlanDetallePage() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
const [plan, setPlan] = useState<PlanPago | undefined>(() => (id ? obtenerPlan(id) : undefined))
|
||||
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 [expandidos, setExpandidos] = useState<Set<string>>(new Set())
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
@@ -31,6 +39,37 @@ export default function PlanDetallePage() {
|
||||
const [errorNota, setErrorNota] = useState<string | null>(null)
|
||||
const hoy = useMemo(() => new Date(), [])
|
||||
|
||||
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])
|
||||
|
||||
if (cargando) {
|
||||
return <p className="text-sm text-neutral-400">Cargando...</p>
|
||||
}
|
||||
|
||||
if (errorCarga) {
|
||||
return <div className="border border-red-300 bg-white px-4 py-3 text-sm text-red-700">{errorCarga}</div>
|
||||
}
|
||||
|
||||
if (!plan) {
|
||||
return (
|
||||
<div className="border border-neutral-200 bg-white p-16 text-center">
|
||||
@@ -57,11 +96,12 @@ export default function PlanDetallePage() {
|
||||
})
|
||||
}
|
||||
|
||||
const guardarAbono = (datos: { monto: number; fechaPago: string; referencia: string }) => {
|
||||
const guardarAbono = async (datos: { monto: number; fechaPago: string; referencia: string }) => {
|
||||
if (!pagoAbono) return
|
||||
try {
|
||||
const actualizado = registrarAbonoEnPlan(plan.id, datos, pagoAbono.id)
|
||||
setPlan(actualizado)
|
||||
const r = await registrarAbonoAsync(plan.id, datos, pagoAbono.id)
|
||||
setPlan(r.plan)
|
||||
setOffline(r.origen === 'local')
|
||||
setPagoAbono(null)
|
||||
setError(null)
|
||||
} catch (e) {
|
||||
@@ -69,14 +109,23 @@ export default function PlanDetallePage() {
|
||||
}
|
||||
}
|
||||
|
||||
const cancelar = () => {
|
||||
const cancelar = async () => {
|
||||
if (!window.confirm('¿Cancelar este plan? El saldo pendiente dejará de cobrarse.')) return
|
||||
setPlan(cancelarPlan(plan.id))
|
||||
try {
|
||||
const r = await cancelarPlanAsync(plan.id)
|
||||
setPlan(r.plan)
|
||||
setOffline(r.origen === 'local')
|
||||
setError(null)
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Error al cancelar el plan')
|
||||
}
|
||||
}
|
||||
|
||||
const guardarNota = () => {
|
||||
const guardarNota = async () => {
|
||||
try {
|
||||
setPlan(agregarNota(plan.id, notaTexto))
|
||||
const r = await agregarNotaAsync(plan, notaTexto)
|
||||
setPlan(r.plan)
|
||||
setOffline(r.origen === 'local')
|
||||
setNotaTexto('')
|
||||
setErrorNota(null)
|
||||
} catch (e) {
|
||||
@@ -120,6 +169,12 @@ export default function PlanDetallePage() {
|
||||
</div>
|
||||
</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>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="border border-red-300 bg-white px-4 py-3 text-sm text-red-700">{error}</div>
|
||||
)}
|
||||
@@ -277,7 +332,7 @@ export default function PlanDetallePage() {
|
||||
className="mt-8"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
guardarNota()
|
||||
void guardarNota()
|
||||
}}
|
||||
>
|
||||
<label
|
||||
|
||||
Reference in New Issue
Block a user