Estructura completa del módulo de empresas: componente de selección con tabla, servicio de comunicación con API, guarda de rutas inteligente, configuración de columnas y persistencia de estado.
📋 CompanyComponent + Tabla
🛡️ companyGuard
🌐 CompanyService + JWT
💾 Persistencia de estado
🔐 Renovación de token
index.ts
export { CompanyComponent } from './lib/feature-company/src/lib/company/company.component';
export { COMPANY\_ROUTES } from './lib/company.routes';
export { CompanyService } from './lib/data-access/src/lib/company.service';
export type {
Company,
JwtSelectCompanyResponse,
ApiResponse,
} from './lib/domain/src/lib/company.model';
🚪 Exportaciones para consumo externo
✔️ CompanyComponent: componente de selección de empresas.
✔️ COMPANY_ROUTES: configuración de rutas con guarda.
✔️ CompanyService: servicio para gestión de empresas y selección con JWT.
✔️ Tipos: Company, JwtSelectCompanyResponse, ApiResponse para tipado externo.
company.routes.ts
export const COMPANY\_ROUTES: Routes = \[
{
path: '',
component: CompanyComponent,
title: 'Nebula ERP - Empresas',
canActivate: \[companyGuard\],
data: {
showSidebar: false,
showHeader: false,
showBreadcrumb: false,
},
},
\];
Ruta con guarda y layout minimalista
guard/company.guard.ts
export const companyGuard: CanActivateFn = (route, state) => {
const companyService = inject(CompanyService);
const router = inject(Router);
const customerId = companyService.getCustomerId();
if (!customerId) return true;
return companyService.readCustomer(customerId).pipe(
map(companies => Array.isArray(companies) ? companies : \[\]),
catchError(() => of(\[\])),
mergeMap(companies => {
if (companies.length !== 1) return of(true);
const singleCompany = companies\[0\];
return companyService.jwtSelectCompany(singleCompany.id).pipe(
tap(response => {
if (response?.response?.content?.accessToken) {
localStorage.setItem('access\_token', accessToken);
router.navigate(\['/layout/products'\], {
queryParams: { companyId: singleCompany.id, autoRedirect: 'true' }
});
}
})
);
})
);
};
** Guarda inteligente con auto-redirección**
✔️ Si no hay customerId: permite acceso normal al componente.
✔️ Si solo una empresa: automáticamente selecciona la empresa, renueva el token JWT y redirige a productos.
✔️ Si múltiples empresas: retorna true, mostrando CompanyComponent para que el usuario elija.
✔️ Manejo de errores: catchError previene fallos en la carga.
✔️ Redirección con parámetros: incluye autoRedirect=true para que ProductsComponent muestre botón de volver.
feature-company/company.component.ts
@Component({
selector: 'app-layout',
standalone: true,
imports: \[CommonModule, TableComponent, InputComponent, ...\],
templateUrl: './company.component.html',
})
export class CompanyComponent implements OnInit {
companies = signal<CompanyData\[\]>(\[\]);
searchTerm = signal<string>('');
loading = signal(false);
loadCompanies(): void {
this.companyService.readCustomer(this.currentCustomerId).subscribe({
next: (companies) => {
const transformed = companies.map(c => ({
empresa: c.name, identificacion: c.nit
}));
this.originalCompanies.set(transformed);
this.applyFilter();
}
});
}
onSelectCompany(company: CompanyData): void {
this.companyService.jwtSelectCompany(company.id).subscribe({
next: (response) => {
this.authService.updateTokensAfterCompanySelection(jwtToken, refreshToken);
this.router.navigate(\['/layout/products'\], {
queryParams: { companyId: company.id }
});
}
});
}
}
** Componente de selección de empresas**
company.component.html
<div class="companies-layout">
<header class="companies-header">
<h1 class="header-title">Empresas</h1>
<p class="header-subtitle">Busca y administra tus empresas en un solo lugar.</p>
</header>
<div class="search-wrapper">
<lib-input \[value\]="searchTerm()"
(valueChange)="onSearchChange($event)"
placeholder="Buscar"
suffixIcon="Search">
</lib-input>
</div>
<lib-table \[config\]="tableConfig"
\[data\]="companies()"
\[loading\]="loading()"
(rowClick)="onSelectCompany($event)">
<ng-template #cellTemplate let-item>
<div class="company-name" (click)="onSelectCompany(item)">
<span>{{ item.empresa }}</span>
</div>
</ng-template>
</lib-table>
</div>
Ui con tabla y búsqueda integrada
✔️ Header: título y subtítulo descriptivo.
✔️ lib-input: campo de búsqueda con ícono de lupa.
✔️ lib-table: tabla configurable con ordenamiento, paginación y loading state.
✔️ cellTemplate personalizado: muestra el nombre de la empresa con evento click.
✔️ rowClick: evento principal para seleccionar empresa.
company-table.config.ts
export const COMPANY\_TABLE\_CONFIG = {
columns: \[
{ key: 'empresa', label: 'Empresa', sortable: true, width: '25%', cellTemplate: true },
{ key: 'identificacion', label: 'N° Identificación', sortable: true, hideOnMobile: true, width: '25%' }
\],
showPagination: true,
showSearch: false,
actionsLabel: 'Acciones',
};
export interface CompanyData {
id?: string;
empresa: string;
identificacion: string;
}
⚙️ Configuración declarativa de columnas
✔️ Columnas: Empresa (con template personalizado) e Identificación.
✔️ sortable: ambas columnas permiten ordenamiento ascendente/descendente.
✔️ hideOnMobile: columna de identificación se oculta en móviles (responsive).
✔️ cellTemplate: permite renderizado personalizado del nombre de empresa.
✔️ CompanyData: interfaz para tipado de datos en la tabla.
data-access/company.service.ts
@Injectable({ providedIn: 'root' })
export class CompanyService {
private readonly http = inject(HttpService);
private readonly authService = inject(AuthService);
jwtSelectCompany(companyId: string): Observable<ApiResponse<JwtSelectCompanyResponse>> {
const accessToken = this.authService.getToken();
const headers = new HttpHeaders({ AccessToken: accessToken });
return this.http.post(this.OAUTH\_ENDPOINT, {}, { companyId }, headers);
}
readCustomer(customerId: number): Observable<Company\[\]> {
return this.http.getContent<Company\[\]>(\`${this.COMPANY\_ENDPOINT}/read-customer\`, { customerId });
}
getCustomerId(): number | null {
const userData = localStorage.getItem('user\_data');
return userData ? JSON.parse(userData).customerId : null;
}
}
Servicio de empresas con renovación JWT
✔️ jwtSelectCompany: endpoint para seleccionar empresa y obtener nuevo token JWT con contexto de empresa.
✔️ readCustomer: obtiene lista de empresas asociadas al customerId.
✔️ getCustomerId: helper para extraer customerId de localStorage.
✔️ Headers personalizados: envía AccessToken en header para autenticación.
✔️ Endpoints: /simappe-admin/api/v1/company/read-customer y /simappe-oauth2-server/api/v1/jwt-select-company.
domain + const
// domain/company.model.ts
export interface Company {
id: string;
name: string;
nit: string;
email?: string;
phone?: string;
address?: string;
}
export interface JwtSelectCompanyResponse {
content: { accessToken: string; refreshToken: string; expiresIn: number };
}
// const/company.constant.ts
export const ERROR\_MESSAGES = { UNAUTHORIZED: 'Usuario no autenticado' };
export const STORAGE\_KEYS = { USER\_DATA: 'user\_data' };
export const API\_ENDPOINTS = {
COMPANY: '/simappe-admin/api/v1/company',
JWT\_SELECT\_COMPANY: '/simappe-oauth2-server/api/v1/jwt-select-company',
};
** Tipado fuerte y constantes centralizadas**
✔️ Company: interfaz completa de empresa (id, name, nit, contacto).
✔️ JwtSelectCompanyResponse: estructura de respuesta con accessToken, refreshToken y expiresIn.
✔️ ERROR_MESSAGES: mensajes de error reutilizables.
✔️ STORAGE_KEYS: claves de localStorage centralizadas.
✔️ API_ENDPOINTS: rutas de API para evitar duplicación.
Secuencia completa
📋 Flujo completo del módulo de empresas:
1. Usuario completa login exitosamente
2. Redirigido a /layout/company
3. companyGuard ejecuta lógica:
- Obtiene customerId desde localStorage
- Llama a readCustomer() para obtener empresas
Si 1 empresa: auto-selecciona, renueva token JWT y redirige a productos
Si múltiples: permite acceso a CompanyComponent
4. CompanyComponent carga y muestra tabla de empresas
5. Usuario busca o selecciona una empresa
6. Al seleccionar: llama a jwtSelectCompany()
7. Actualiza tokens en localStorage (access_token, refresh_token)
8. Guarda companyId en user_data
9. Redirige a /layout/products con parámetros
🔐 Seguridad: El token JWT se renueva al seleccionar empresa para incluir el contexto de empresa en futuras peticiones.
🔗 Integración con otros módulos: CompanyComponent es el segundo paso después del login. Selecciona el contexto empresarial antes de mostrar los productos/módulos disponibles. La información de empresa seleccionada se usa en todas las peticiones posteriores.
📁 raíz del módulo
index.ts, company.routes.ts
🎨 feature-company/
CompanyComponent, template, estilos, table config
📁 data-access/
CompanyService → API + JWT renewal
🛡️ guards/
companyGuard → auto-redirección inteligente
📦 domain + const
Modelos Company, constantes de API y errores
✨ Mejores prácticas implementadas: Guarda de rutas con auto-redirección, persistencia de estado de tabla (búsqueda + ordenamiento), renovación de token JWT al seleccionar empresa, componentes standalone, signals para estado reactivo, configuración declarativa de tabla, y separación clara de responsabilidades.
nx test company → pruebas unitarias para CompanyService y CompanyComponent.
🏢 @nebula/company · Módulo de selección de empresas con guarda inteligente, tabla interactiva y renovación automática de tokens JWT.