feat: ecosistema FRASA — apps Cotizador, Alta Clientes, Cobranza y Landing
- Cotizador (/quote): 4 propuestas con formulas validadas del Excel, comparador, combobox de unidades, vigencia, PDF ANEXO A - Alta Clientes (/client): wizard 8 pasos, validacion CURP RENAPO, OCR de INE, checklist de expediente, firma digital, 5 PDFs legales - Cobranza (/pay): planes de enganche, mora, vista cobranza del dia, recibos PDF, bitacora, dashboard con filtros - Landing (/): entrada al ecosistema - Design system StudioBlank (monocromo, acento #1B4332) en las 4 apps - 123 tests vitest
This commit is contained in:
29
apps/landing/src/App.tsx
Normal file
29
apps/landing/src/App.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { MODULOS } from './data/modulos'
|
||||
import { TarjetaModulo } from './components/TarjetaModulo'
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
<header className="px-6 pt-20 pb-16 sm:px-12 lg:px-24">
|
||||
<p className="text-xs font-bold tracking-widest text-accent uppercase">FRASA Desarrollos</p>
|
||||
<h1 className="mt-4 text-4xl font-light text-neutral-900 sm:text-5xl">
|
||||
Ecosistema comercial
|
||||
</h1>
|
||||
</header>
|
||||
|
||||
<main className="flex-1 px-6 sm:px-12 lg:px-24">
|
||||
<nav aria-label="Módulos del ecosistema">
|
||||
<ul className="grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{MODULOS.map((modulo) => (
|
||||
<TarjetaModulo key={modulo.id} modulo={modulo} />
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</main>
|
||||
|
||||
<footer className="px-6 pt-24 pb-10 sm:px-12 lg:px-24">
|
||||
<p className="text-xs text-neutral-400">Consult (AS) Technology</p>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
23
apps/landing/src/components/TarjetaModulo.tsx
Normal file
23
apps/landing/src/components/TarjetaModulo.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import type { Modulo } from '../data/modulos'
|
||||
|
||||
interface Props {
|
||||
modulo: Modulo
|
||||
}
|
||||
|
||||
/** Tarjeta de acceso a un módulo del ecosistema */
|
||||
export function TarjetaModulo({ modulo }: Props) {
|
||||
return (
|
||||
<li>
|
||||
<a
|
||||
href={modulo.url}
|
||||
className="group flex h-full flex-col border border-neutral-200 bg-white p-8 transition-colors duration-150 hover:border-neutral-900"
|
||||
>
|
||||
<h2 className="text-2xl font-light text-neutral-900">{modulo.nombre}</h2>
|
||||
<p className="mt-3 text-sm leading-relaxed text-neutral-500">{modulo.descripcion}</p>
|
||||
<span className="mt-auto pt-8 text-xs font-bold tracking-widest text-accent uppercase">
|
||||
Entrar <span aria-hidden="true">→</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
32
apps/landing/src/data/modulos.test.ts
Normal file
32
apps/landing/src/data/modulos.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { MODULOS } from './modulos'
|
||||
|
||||
describe('MODULOS', () => {
|
||||
it('expone los 3 módulos del ecosistema', () => {
|
||||
expect(MODULOS).toHaveLength(3)
|
||||
expect(MODULOS.map((m) => m.id)).toEqual(['cotizador', 'alta-clientes', 'cobranza'])
|
||||
})
|
||||
|
||||
it('cada módulo tiene nombre, descripción y URL de preview válida', () => {
|
||||
for (const modulo of MODULOS) {
|
||||
expect(modulo.nombre.length).toBeGreaterThan(0)
|
||||
expect(modulo.descripcion.length).toBeGreaterThan(0)
|
||||
expect(() => new URL(modulo.url)).not.toThrow()
|
||||
expect(new URL(modulo.url).protocol).toBe('http:')
|
||||
}
|
||||
})
|
||||
|
||||
it('apunta a los puertos y prefijos de preview correctos', () => {
|
||||
const urls = Object.fromEntries(MODULOS.map((m) => [m.id, m.url]))
|
||||
expect(urls['cotizador']).toBe('http://192.168.10.123:3001/quote/')
|
||||
expect(urls['alta-clientes']).toBe('http://192.168.10.123:3002/client/')
|
||||
expect(urls['cobranza']).toBe('http://192.168.10.123:3003/pay/')
|
||||
})
|
||||
|
||||
it('no hay ids ni URLs duplicadas', () => {
|
||||
const ids = MODULOS.map((m) => m.id)
|
||||
const urls = MODULOS.map((m) => m.url)
|
||||
expect(new Set(ids).size).toBe(MODULOS.length)
|
||||
expect(new Set(urls).size).toBe(MODULOS.length)
|
||||
})
|
||||
})
|
||||
29
apps/landing/src/data/modulos.ts
Normal file
29
apps/landing/src/data/modulos.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
export interface Modulo {
|
||||
id: string
|
||||
nombre: string
|
||||
descripcion: string
|
||||
url: string
|
||||
}
|
||||
|
||||
// TODO: tras montar el NGINX de producción cambiar las URLs absolutas de
|
||||
// preview por rutas relativas '/quote/', '/client/' y '/pay/'.
|
||||
export const MODULOS: Modulo[] = [
|
||||
{
|
||||
id: 'cotizador',
|
||||
nombre: 'Cotizador',
|
||||
descripcion: 'Propuestas comerciales y PDF en tiempo real',
|
||||
url: 'http://192.168.10.123:3001/quote/',
|
||||
},
|
||||
{
|
||||
id: 'alta-clientes',
|
||||
nombre: 'Alta de clientes',
|
||||
descripcion: 'Expediente digital, firmas y formatos PLD',
|
||||
url: 'http://192.168.10.123:3002/client/',
|
||||
},
|
||||
{
|
||||
id: 'cobranza',
|
||||
nombre: 'Cobranza',
|
||||
descripcion: 'Planes de enganche, vencidos y estados de cuenta',
|
||||
url: 'http://192.168.10.123:3003/pay/',
|
||||
},
|
||||
]
|
||||
27
apps/landing/src/index.css
Normal file
27
apps/landing/src/index.css
Normal file
@@ -0,0 +1,27 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||
--color-accent: #1B4332;
|
||||
--color-accent-hover: #132f23;
|
||||
--radius-xs: 0;
|
||||
--radius-sm: 0;
|
||||
--radius-md: 0;
|
||||
--radius-lg: 0;
|
||||
--radius-xl: 0;
|
||||
--radius-2xl: 0;
|
||||
--radius-3xl: 0;
|
||||
--radius-4xl: 0;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-white text-neutral-900 antialiased;
|
||||
}
|
||||
|
||||
/* Foco visible accesible: outline en color de acento */
|
||||
:focus-visible {
|
||||
outline: 2px solid #1B4332;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
}
|
||||
14
apps/landing/src/main.tsx
Normal file
14
apps/landing/src/main.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import '@fontsource/inter/300.css'
|
||||
import '@fontsource/inter/400.css'
|
||||
import '@fontsource/inter/500.css'
|
||||
import '@fontsource/inter/700.css'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
Reference in New Issue
Block a user