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

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:

  1. Loading State: Signal computed que indica si la configuración está cargando
  2. Signals Derivados: Computeds para todos los aspectos de la configuración
  3. Navegación Reactiva: Grupos e items se actualizan automáticamente
  4. Dashboard Widgets: Tanto configurados como disponibles
  5. TopNav Configuration: Visibilidad de elementos del top nav
  6. Branding Dinámico: Logo y color primario del tenant
  7. 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:

🎨 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

🔧 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

🤝 Contribución

Guidelines

  1. Standalone Components: Siempre usar standalone: true
  2. OnPush Strategy: Obligatorio para componentes
  3. Signals > Observables: Para estado local reactivo
  4. No Timeouts: Prohibido en el proyecto
  5. RxJS > Promises: Para operaciones asíncronas
  6. Kebab-case: Para nombres de archivos
  7. Tests Obligatorios: Para toda nueva funcionalidad

Code Review Checklist

📝 Licencia

MIT © GoGrab Space