Files
FRASA-CAS/apps/landing/src/App.tsx
Consult AS Technology c2a8f4e929 feat(captura): módulo de captura por departamentos
Backend: tabla CapturaDepartamento, 5 departamentos, 7 desarrollos,
service CRUD + importador Excel + 6 endpoints.
Frontend: tabla editable inline, tabs por dept, resumen avance,
paginación, filtro estatus, permisos por rol, dark mode.
746 API tests + 42 landing tests.
2026-09-07 07:59:14 +00:00

127 lines
4.3 KiB
TypeScript

import { useEffect, useState } from 'react'
import { MODULOS, esModuloVisible } from './data/modulos'
import { Navbar } from './components/Navbar'
import { TarjetaModulo } from './components/TarjetaModulo'
import { ConfiguracionCuenta } from './components/ConfiguracionCuenta'
import BannerModoLocal from './components/BannerModoLocal'
import { VistaContactos } from './components/VistaContactos'
import { ConfigPage } from './components/config/ConfigPage'
import { CapturaPage } from './pages/CapturaPage'
import { getToken, type UsuarioSesion } from './services/auth'
import { getMe } from './services/api'
type Vista = 'home' | 'contactos' | 'captura' | 'configuraciones'
export default function App() {
const [vista, setVista] = useState<Vista>('home')
// Módulos permitidos por el rol (GET /auth/me); null = sin sesión → se muestran todos
const [permitidos, setPermitidos] = useState<string[] | null>(null)
const [usuario, setUsuario] = useState<UsuarioSesion | null>(null)
useEffect(() => {
let cancelado = false
const cargar = () => {
if (!getToken()) {
setPermitidos(null)
setUsuario(null)
return
}
// Si /me falla se conserva lo anterior (o todos): el login wall de cada app sigue
getMe()
.then((u) => {
if (!cancelado) {
setPermitidos(u.modulos ?? [])
setUsuario(u)
}
})
.catch(() => {})
}
cargar()
window.addEventListener('frasa:sesion', cargar)
return () => {
cancelado = true
window.removeEventListener('frasa:sesion', cargar)
}
}, [])
if (vista === 'contactos') {
return (
<div className="flex min-h-screen flex-col">
<Navbar usuario={usuario} onConfiguraciones={() => setVista('configuraciones')} />
<BannerModoLocal />
<VistaContactos onVolver={() => setVista('home')} />
</div>
)
}
if (vista === 'captura') {
return (
<div className="flex min-h-screen flex-col">
<Navbar usuario={usuario} onConfiguraciones={() => setVista('configuraciones')} />
<BannerModoLocal />
<CapturaPage onVolver={() => setVista('home')} />
</div>
)
}
if (vista === 'configuraciones' && usuario?.rol === 'admin') {
return (
<div className="flex min-h-screen flex-col">
<Navbar usuario={usuario} onConfiguraciones={() => setVista('configuraciones')} />
<BannerModoLocal />
<ConfigPage onVolver={() => setVista('home')} />
</div>
)
}
return (
<div className="flex min-h-screen flex-col">
<Navbar usuario={usuario} onConfiguraciones={() => setVista('configuraciones')} />
<BannerModoLocal />
<main className="mx-auto w-full max-w-[1280px] flex-1 px-4 sm:px-6">
<section aria-labelledby="titulo-hero" className="py-12 sm:py-16">
<h1
id="titulo-hero"
className="text-3xl font-semibold tracking-tight text-[#111827] dark:text-[#E2E8F0] sm:text-4xl"
>
Ecosistema comercial
</h1>
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-[#64748B] dark:text-[#94A3B8] sm:text-base">
Las herramientas del equipo comercial de Frasa Desarrollos en un solo lugar:
cotizaciones, alta de clientes, cobranza, tableros y asistente IA.
</p>
</section>
<nav aria-label="Módulos del ecosistema">
<ul className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{MODULOS.filter((modulo) => esModuloVisible(modulo, permitidos)).map((modulo) => (
<TarjetaModulo
key={modulo.id}
modulo={modulo}
onAbrir={
modulo.id === 'contactos'
? () => setVista('contactos')
: modulo.id === 'captura'
? () => setVista('captura')
: undefined
}
/>
))}
</ul>
</nav>
<div className="mt-6">
<ConfiguracionCuenta />
</div>
</main>
<footer className="mt-16 border-t border-[#E5E7EB] dark:border-[#1E293B]">
<div className="mx-auto max-w-[1280px] px-6 py-8">
<p className="text-xs text-[#64748B] dark:text-[#94A3B8]">Consult (AS) Technology</p>
</div>
</footer>
</div>
)
}