README
Documentation for GoGrab Admin
GoGrab Admin - Aplicación de Administración
Aplicación Angular 20+ de administración multi-tenant para GoGrab Space
📋 Descripción General
GoGrab Admin es la aplicación principal de administración del ecosistema GoGrab Space. Proporciona una interfaz de usuario moderna y reactiva para la gestión multi-tenant con autenticación, RBAC, y módulos dinámicos.
🏗️ Arquitectura
Stack Tecnológico
- Framework: Angular 20.x con standalone components
- Build: Nx 21.x monorepo
- Estado: NGXS + Angular Signals
- Routing: Angular Router con lazy loading
- Styling: SCSS con design system personalizado
- Testing: Vitest + Playwright
Estructura de la Aplicación
apps/gograb-admin/
├── src/
│ ├── app/
│ │ ├── core/ # Servicios core y configuración global
│ │ │ ├── mocks/ # Mock APIs para desarrollo
│ │ │ │ ├── module-config-mock-api.ts
│ │ │ │ └── mock-config.ts
│ │ │ └── services/
│ │ │ └── bootstrap-initializer.service.ts
│ │ ├── features/ # Feature modules (lazy loaded)
│ │ │ ├── dashboard/ # Dashboard principal
│ │ │ ├── config-demo/ # Demo de module-config
│ │ │ └── login/ # Login y autenticación
│ │ ├── shared/ # Componentes compartidos
│ │ ├── app.component.ts # Componente raíz
│ │ ├── app.config.ts # Configuración de la app
│ │ └── app.routes.ts # Definición de rutas
│ ├── assets/ # Assets estáticos
│ ├── index.html # HTML principal
│ └── main.ts # Entry point
├── project.json # Configuración Nx
└── vite.config.ts # Configuración Vite
🚀 Inicio Rápido
Desarrollo Local
# Servir aplicación en modo desarrollo
npx nx serve gograb-admin
# Acceder en http://localhost:4200
Build de Producción
# Build optimizado
npx nx build gograb-admin
# Output en dist/apps/gograb-admin/
Tests
# Tests unitarios
npx nx test gograb-admin
# Tests E2E con Playwright
npx nx e2e gograb-admin-e2e
🔧 Configuración de la Aplicación
app.config.ts - Providers Centrales
import { ApplicationConfig } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideMockApi } from '@libs-core/mock-api';
import { provideEventBus } from '@libs-core/event-system';
import { provideLogger } from '@libs-core/log-system';
import { provideSecurity } from '@libs-core/security';
import { provideAppConfig } from '@libs-core/module-config';
export const appConfig: ApplicationConfig = {
providers: [
// Routing
provideRouter(appRoutes, withComponentInputBinding()),
// HTTP Client con Mock API en desarrollo
provideHttpClient(
withInterceptors([
// Interceptores se configuran en provideHttpClient
])
),
// Mock API (solo en desarrollo)
provideMockApi({
enabled: true,
definitions: mockConfig,
persistence: {
enabled: true,
storage: 'indexeddb',
ttl: 3600000 // 1 hora
}
}),
// Sistema de Eventos
provideEventBus({
enableLogging: true,
enableMetrics: true
}),
// Sistema de Logging
provideLogger({
level: 'debug',
appenders: [new ConsoleAppender()],
formatters: [new JsonFormatter()]
}),
// Security (Auth + RBAC)
provideSecurity({
tokenRefreshEnabled: true,
tokenRefreshThreshold: 300000 // 5 minutos
}),
// Module Config
provideAppConfig(),
// Bootstrap Initializer (carga módulos después del login)
{
provide: APP_INITIALIZER,
useFactory: () => {
const bootstrapInitializer = inject(BootstrapInitializerService);
return () => {}; // El servicio se auto-inicializa en su constructor
},
multi: true
}
]
};
🔐 Flujo de Autenticación y Bootstrap
Secuencia de Inicialización
1. APP_INITIALIZER
└─> BootstrapInitializerService constructor
2. BootstrapInitializerService
├─> Verifica si existe contexto (currentContext)
│ ├─> SI: Bootstrap inmediato
│ └─> NO: Espera evento 'context.changed'
│
└─> executeBootstrap()
├─> BrowserBootstrapperService.initialize()
│ ├─> Descubre módulos
│ ├─> Valida dependencias
│ └─> Inicializa módulos
│
└─> AppConfigService.loadConfiguration()
├─> Intenta GET /api/config/ui
│ ├─> SUCCESS: Usa config del backend
│ └─> ERROR: Fallback a construcción local
│
└─> EventBus.publish(AppConfigLoadedEvent)
Flujo de Login
Usuario → LoginComponent → AuthService.login()
└─> Backend responde con token JWT
└─> AuthService guarda token
└─> ContextService.selectContext(tenant)
└─> EventBus.publish('context.changed')
└─> BootstrapInitializerService detecta evento
└─> executeBootstrap()
└─> Módulos se cargan + UI config se carga
└─> Router navega a /dashboard
📱 Módulo de Configuración UI
ConfigDemoComponent - Ejemplo de Uso
El ConfigDemoComponent demuestra el uso completo de la API de @libs-core/module-config:
Ubicación: apps/gograb-admin/src/app/features/config-demo/config-demo.component.ts
export class ConfigDemoComponent implements OnInit {
private configService = inject(AppConfigService);
// Estados de carga
loading = computed(() => this.configService.configuration() === null);
// Configuración completa
config = this.configService.configuration;
version = computed(() => this.config()?.version || 'N/A');
generatedAt = computed(() => {
const date = this.config()?.generatedAt;
return date ? new Date(date).toLocaleString() : 'N/A';
});
// Navegación
navigationConfig = this.configService.navigationConfig;
navigationGroups = computed(() => this.navigationConfig()?.groups || []);
navigationGroupCount = computed(() => this.navigationGroups().length);
navigationItemCount = computed(() =>
this.navigationGroups().reduce((total, group) => total + group.items.length, 0)
);
// Dashboard
dashboardConfig = this.configService.dashboardConfig;
availableWidgets = computed(() => this.dashboardConfig()?.availableWidgets || []);
configuredWidgets = computed(() => this.dashboardConfig()?.widgets || []);
// TopNav
topNavConfig = this.configService.topNavConfig;
showTenantSelector = computed(() => this.topNavConfig()?.showTenantSelector ?? true);
// Branding
brandingLogo = computed(() => this.config()?.branding.logo || '');
primaryColor = computed(() => this.config()?.branding.primaryColor || '#1976d2');
ngOnInit(): void {
this.configService.loadConfiguration();
}
}
Features demostradas:
- Loading State: Signal computed que indica si la configuración está cargando
- Signals Derivados: Computeds para todos los aspectos de la configuración
- Navegación Reactiva: Grupos e items se actualizan automáticamente
- Dashboard Widgets: Tanto configurados como disponibles
- TopNav Configuration: Visibilidad de elementos del top nav
- Branding Dinámico: Logo y color primario del tenant
- OnPush Compatible: Sin necesidad de ChangeDetectorRef
Mock API para Desarrollo
Ubicación: apps/gograb-admin/src/app/core/mocks/module-config-mock-api.ts
export const moduleConfigMockAPI: MockDefinition = {
'GET /api/config/ui': (request: MockRequest): MockResponse => {
const tenantId = request.headers?.['x-tenantId'] || 'default-tenant';
const response: ApiResponse<AppUiConfiguration> = {
success: true,
statusCode: 200,
data: {
navigation: {
position: 'left',
collapsible: true,
showBreadcrumbs: true,
groups: [
{
id: 'main',
label: 'Main Navigation',
order: 1,
visible: true,
items: [
{
id: 'dashboard',
label: 'Dashboard',
icon: '📊',
route: '/dashboard',
order: 1,
visible: true,
enabled: true,
moduleId: 'core',
permissions: undefined // Visible para todos
},
{
id: 'users',
label: 'User Management',
icon: '👥',
route: '/users',
order: 2,
visible: true,
enabled: true,
moduleId: 'user-management',
permissions: ['users:read'] // Solo con permiso
}
]
}
]
},
dashboard: {
columns: 4,
widgets: [/* widgets configurados */],
availableWidgets: [/* registry de widgets */]
},
topNav: {
showTenantSelector: true,
showUserMenu: true,
showNotifications: true
},
branding: {
logo: '/assets/gograb-logo.png',
primaryColor: '#1976d2'
},
generatedAt: new Date(),
version: '1.0.0'
},
message: 'UI configuration loaded successfully'
};
return {
status: 200,
data: response,
delay: 150 // Simular latencia de red
};
},
'PUT /api/config/ui': (request: MockRequest): MockResponse => {
const tenantId = request.headers?.['x-tenantId'] || 'default-tenant';
const customization = request.body as TenantUiCustomization;
// Aplicar customización y retornar config actualizada
// ...
return {
status: 200,
data: response,
delay: 200
};
}
};
Características del Mock:
- Endpoint correcto:
/api/config/ui(compatible con ConfigApiClient) - ApiResponse wrapper: Estructura correcta con
success,statusCode,data - Header x-tenantId: Simula inyección automática de ContextInterceptor
- Datos completos: Mock con navegación, dashboard, topNav, branding
- Latencia simulada: Delays de 150-200ms para realismo
- Console logging: Logs para debugging
🎨 UI Framework y Design System
Componentes de Layout
// Header con tenant selector
<app-header
[showTenantSelector]="showTenantSelector()"
[logo]="logo()"
[primaryColor]="primaryColor()"
/>
// Sidebar con navegación dinámica
<app-sidebar
[groups]="navGroups()"
[position]="navPosition()"
[collapsible]="navCollapsible()"
/>
// Dashboard con grid de widgets
<app-dashboard
[columns]="dashColumns()"
[widgets]="configuredWidgets()"
/>
Theme Dinámico
// Aplicar color primario del tenant
this.renderer.setStyle(
document.documentElement,
'--primary-color',
this.primaryColor()
);
🔄 Gestión de Estado
NGXS para Estado Global
@State<AuthStateModel>({
name: 'auth',
defaults: {
user: null,
token: null,
isAuthenticated: false
}
})
export class AuthState {
@Action(Login)
login(ctx: StateContext<AuthStateModel>, action: Login) {
return this.authService.login(action.credentials).pipe(
tap(result => {
ctx.patchState({
user: result.user,
token: result.token,
isAuthenticated: true
});
})
);
}
}
Signals para Estado Local
// Preferir Signals sobre Observables para estado local
export class DashboardComponent {
private configService = inject(AppConfigService);
widgets = this.configService.dashboardConfig;
columns = computed(() => this.widgets()?.columns || 4);
activeWidgets = computed(() =>
this.widgets()?.widgets?.filter(w => w.enabled) || []
);
}
🧪 Testing
Unit Tests con Vitest
describe('ConfigDemoComponent', () => {
it('should display loading state initially', () => {
const component = createComponent(ConfigDemoComponent);
expect(component.loading()).toBe(true);
});
it('should load configuration on init', () => {
const configService = TestBed.inject(AppConfigService);
const spy = vi.spyOn(configService, 'loadConfiguration');
const component = createComponent(ConfigDemoComponent);
component.ngOnInit();
expect(spy).toHaveBeenCalled();
});
it('should compute navigation groups correctly', () => {
const component = createComponent(ConfigDemoComponent);
configService.configSignal.set(mockConfig);
expect(component.navigationGroups()).toHaveLength(1);
expect(component.navigationItemCount()).toBe(4);
});
});
E2E Tests con Playwright
test('should navigate to dashboard after login', async ({ page }) => {
await page.goto('/login');
await page.fill('input[name="email"]', '[email protected]');
await page.fill('input[name="password"]', 'password');
await page.click('button[type="submit"]');
await expect(page).toHaveURL('/dashboard');
await expect(page.locator('h1')).toContainText('Dashboard');
});
test('should display navigation items based on permissions', async ({ page }) => {
await loginAsUser(page, '[email protected]'); // Sin permisos de admin
// Verificar items visibles
await expect(page.locator('nav a[href="/dashboard"]')).toBeVisible();
// Verificar items ocultos (requieren permisos)
await expect(page.locator('nav a[href="/admin"]')).not.toBeVisible();
});
📦 Dependencias Principales
{
"dependencies": {
"@angular/core": "^20.1.0",
"@angular/common": "^20.1.0",
"@angular/router": "^20.1.0",
"@libs-core/module-base": "*",
"@libs-core/module-config": "*",
"@libs-core/module-integrator": "*",
"@libs-core/security": "*",
"@libs-core/event-system": "*",
"@libs-core/log-system": "*",
"@libs-core/ui-framework": "*",
"@gograb-space/user-management": "*",
"@libs-core/mock-api": "*",
"@ngxs/store": "^18.x",
"rxjs": "~7.8.0"
}
}
🚀 Deployment
Build de Producción
# Build optimizado
npx nx build gograb-admin --configuration=production
# Output
dist/apps/gograb-admin/
├── index.html
├── main-*.js # Código de la app (tree-shaken)
├── polyfills-*.js # Polyfills necesarios
├── chunk-*.js # Lazy-loaded chunks
└── assets/ # Assets estáticos
Optimizaciones Aplicadas
- Tree Shaking: Código no usado se elimina
- Lazy Loading: Módulos se cargan bajo demanda
- Code Splitting: Chunks separados para mejor caching
- Minification: Código minimizado y ofuscado
- AOT Compilation: Compilación ahead-of-time
- OnPush Strategy: Detección de cambios optimizada
- Signals: Máxima performance reactiva
🔧 Configuración de Desarrollo
Entorno de Desarrollo
// environment.development.ts
export const environment = {
production: false,
apiUrl: 'http://localhost:3000/api',
mockApiEnabled: true,
logLevel: 'debug'
};
Hot Module Replacement (HMR)
El servidor de desarrollo usa Vite con HMR:
# Cambios en archivos se reflejan inmediatamente
# Sin necesidad de recargar la página completa
npx nx serve gograb-admin
📚 Recursos Adicionales
- Arquitectura:
docs/app/gograb-admin-implementation-design.md - Requirements:
docs/app/gograb-admin-requirements.md - Testing Status:
docs/app/gograb-admin-testing-status.md - MVP Plan:
docs/app/gograb-admin-mvp-plan.md
🤝 Contribución
Guidelines
- Standalone Components: Siempre usar
standalone: true - OnPush Strategy: Obligatorio para componentes
- Signals > Observables: Para estado local reactivo
- No Timeouts: Prohibido en el proyecto
- RxJS > Promises: Para operaciones asíncronas
- Kebab-case: Para nombres de archivos
- Tests Obligatorios: Para toda nueva funcionalidad
Code Review Checklist
- Componente standalone con OnPush
- Signals usados correctamente
- No hay any types
- Tests incluidos
- Build pasa sin errores
- Lint pasa sin warnings
- Documentación actualizada
📝 Licencia
MIT © GoGrab Space