company-selection-patterns
Patrones para manejo de contexto de compañía en el proyecto. Usa esto cuando trabajes con módulos que necesiten filtrar datos por compañía. Incluye patrón global (CompanyContextService) y patrón local (selector manual).
- Category: discovered
- Tags: github, community
- Version: 1.0.0
- Author: walid
- Downloads: 0
- Updated: 2025-11-22
Download the latest version (v1.0.0) from this page.
SKILL.md
---
name: company-selection-patterns
description: Patrones para manejo de contexto de compañía en el proyecto. Usa esto cuando trabajes con módulos que necesiten filtrar datos por compañía. Incluye patrón global (CompanyContextService) y patrón local (selector manual).
---
# Company Selection Patterns 🏢
Guía para implementar correctamente el manejo de compañías en módulos del proyecto.
## Dos Patrones Disponibles
El proyecto tiene **dos patrones** para manejar la selección de compañía según las necesidades del módulo.
### Patrón 1: Global Context (Recomendado para la mayoría)
**Cuándo usar**: Cuando el módulo trabaja con datos que dependen de una compañía seleccionada a nivel global.
**Ubicación**: `src/app/core/services/company-context.service.ts`
### Patrón 2: Local Selection (Para módulos específicos)
**Cuándo usar**: Cuando el módulo necesita su propio selector independiente del contexto global (ej: Biométricos).
**Implementación**: Selector dropdown local + servicio `getCompaniesForDropdown()`
---
## Patrón 1: Global Context Service (Recomendado) ✅
### Servicio: CompanyContextService
```typescript
import { Injectable, signal, computed, effect } from '@angular/core';
interface CompanyContext {
companyId: number;
companyName: string;
}
@Injectable({
providedIn: 'root'
})
export class CompanyContextService {
// Private writable signal
private _selectedCompany = signal<CompanyContext | null>(null);
// Public readonly signal
selectedCompany = this._selectedCompany.asReadonly();
// Computed helper
hasCompanySelected = computed(() => this.selectedCompany() !== null);
constructor() {
// Auto-recover from localStorage
this.loadFromLocalStorage();
// Auto-persist on changes
effect(() => {
const company = this.selectedCompany();
if (company) {
localStorage.setItem('selectedCompany', JSON.stringify(company));
}
});
}
setCompany(company: CompanyContext): void {
this._selectedCompany.set(company);
}
clearCompany(): void {
this._selectedCompany.set(null);
localStorage.removeItem('selectedCompany');
}
private loadFromLocalStorage(): void {
const stored = localStorage.getItem('selectedCompany');
if (stored) {
try {
const company = JSON.parse(stored) as CompanyContext;
this._selectedCompany.set(company);
} catch (error) {
console.error('Error loading company from localStorage:', error);
}
}
}
}
```
### Uso en Feature Modules
```typescript
import { Component, inject, signal, effect, DestroyRef, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { CompanyContextService } from '@core/services';
@Component({
selector: 'app-salary-increase-list',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class SalaryIncreaseListComponent {
private companyContext = inject(CompanyContextService);
private salaryIncreaseService = inject(SalaryIncreaseService);
private messageService = inject(MessageService);
private cdr = inject(ChangeDetectorRef);
private destroyRef = inject(DestroyRef);
// Expose company signal to template
selectedCompany = this.companyContext.selectedCompany;
// Component state
salaryIncreases = signal<SalaryIncrease[]>([]);
loading = signal(false);
currentPage = signal(1);
pageSize = signal(10);
constructor() {
// React to company changes automatically
effect(() => {
const company = this.selectedCompany();
if (company) {
console.log('🏢 Company changed:', company.companyName);
this.currentPage.set(1); // Reset pagination
this.loadData();
}
});
}
loadData(): void {
const company = this.selectedCompany();
if (!company) {
console.warn('⚠️ No company selected');
return;
}
this.loading.set(true);
this.salaryIncreaseService
.getSalaryIncreases(company.companyId, this.currentPage(), this.pageSize())
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (data) => {
this.salaryIncreases.set(data);
this.loading.set(false);
this.cdr.markForCheck();
},
error: (error) => {
console.error('Error loading data:', error);
this.messageService.add({
severity: 'error',
summary: 'Error',
detail: 'No se pudieron cargar los datos'
});
this.loading.set(false);
this.cdr.markForCheck();
}
});
}
}
```
### Template Pattern
```html
<div class="container p-4">
<!-- Header with company info -->
<div class="mb-4">
<div class="flex justify-between items-center">
<div>
<h2 class="text-2xl font-bold">Aumentos Salariales</h2>
@if (selectedCompany()) {
<p class="text-sm text-gray-600 mt-1">
<i class="pi pi-building text-blue-600"></i>
{{ selectedCompany()?.companyName }}
</p>
}
</div>
<div class="flex gap-2">
@if (selectedCompany()) {
<p-button
label="Nuevo"
icon="pi pi-plus"
(click)="openCreateForm()">
</p-button>
}
</div>
</div>
</div>
<!-- Empty state when no company selected -->
@if (!selectedCompany()) {
<div class="bg-blue-50 border border-blue-200 rounded-lg p-8 text-center">
<i class="pi pi-building text-5xl text-blue-400 mb-4 block"></i>
<h3 class="text-xl font-semibold text-gray-700 mb-2">
Seleccione una empresa
</h3>
<p class="text-gray-600">
Seleccione una empresa desde la barra superior para ver los datos
</p>
</div>
}
<!-- Data table (only shows if company selected) -->
@if (selectedCompany()) {
<p-table [value]="salaryIncreases()" [loading]="loading()">
<!-- Table content -->
</p-table>
}
</div>
```
### Características del Patrón Global
1. **Persistencia Automática**: Se guarda en localStorage
2. **Recuperación Automática**: Se carga al iniciar la app
3. **Reactividad**: Los componentes reaccionan automáticamente a cambios
4. **Centralizado**: Una sola fuente de verdad para toda la app
5. **Type-safe**: Señales tipadas con `CompanyContext`
---
## Patrón 2: Local Selection 🎯
### Cuándo Usar
- El módulo necesita cambiar de compañía sin afectar otros módulos
- Funcionalidad administrativa que requiere consultar múltiples compañías
- Módulos de configuración o mantenimiento
### Implementación Completa
#### Servicio de Compañías (Ya existe)
```typescript
// src/app/features/structures/services/companies.service.ts
interface CompanyDropdownItem {
value: number; // companyId
text: string; // companyName
}
@Injectable({
providedIn: 'root'
})
export class CompaniesService {
private http = inject(HttpClient);
private apiUrl = `${environment.apiUrl}Structure/companies`;
getCompaniesForDropdown(): Observable<CompanyDropdownItem[]> {
const url = `${this.apiUrl}/dropdown`;
return this.http.get<ApiResponse<CompanyDropdownItem[]>>(url).pipe(
map(response => response.data || [])
);
}
}
```
#### Component TypeScript
```typescript
import { Component, inject, signal, effect, OnInit } from '@angular/core';
import { CompaniesService } from '@features/structures/services/companies.service';
import { CompanyDropdownItem } from '@features/structures/models/company.model';
@Component({
selector: 'app-biometric-list',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CommonModule,
FormsModule, // Required for [(ngModel)]
TableModule,
SelectModule,
// ... other modules
]
})
export class BiometricListComponent implements OnInit {
private companiesService = inject(CompaniesService);
private biometricService = inject(BiometricService);
private cdr = inject(ChangeDetectorRef);
private destroyRef = inject(DestroyRef);
// Local company selection state
companies = signal<CompanyDropdownItem[]>([]);
selectedCompanyId = signal<number | null>(null);
loadingCompanies = signal(false);
// Feature data
biometrics = signal<Biometric[]>([]);
loading = signal(false);
// Computed signals
showEmptyMessage = computed(() => !this.selectedCompanyId());
constructor() {
// Auto-load data when company changes
effect(() => {
const companyId = this.selectedCompanyId();
if (companyId) {
console.log('🏢 Company selected:', companyId);
this.loadBiometrics();
}
});
}
ngOnInit(): void {
this.loadCompanies();
}
loadCompanies(): void {
this.loadingCompanies.set(true);
this.companiesService.getCompaniesForDropdown()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (companies) => {
this.companies.set(companies);
this.loadingCompanies.set(false);
this.cdr.markForCheck();
},
error: (error) => {
console.error('Error loading companies:', error);
this.loadingCompanies.set(false);
this.cdr.markForCheck();
}
});
}
onCompanyChange(): void {
// Optional: Reset filters when company changes
this.currentPage.set(1);
// Data loads automatically via effect()
}
loadBiometrics(): void {
const companyId = this.selectedCompanyId();
if (!companyId) return;
this.loading.set(true);
this.biometricService.getBiometricsByCompany(companyId)
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe({
next: (biometrics) => {
this.biometrics.set(biometrics);
this.loading.set(false);
this.cdr.markForCheck();
},
error: (error) => {
console.error('Error loading biometrics:', error);
this.loading.set(false);
this.cdr.markForCheck();
}
});
}
}
```
#### Component HTML
```html
<div class="container p-4">
<!-- Header -->
<div class="mb-4">
<div class="flex justify-between items-center mb-4">
<div>
<h2 class="text-2xl font-bold">Dispositivos Biométricos</h2>
<p class="text-sm text-gray-600 mt-1">
Gestión de dispositivos por empresa
</p>
</div>
<div class="flex gap-2">
@if (selectedCompanyId()) {
<p-button
label="Nuevo"
icon="pi pi-plus"
(click)="openCreateForm()">
</p-button>
}
</div>
</div>
<!-- Company Selector -->
<div class="mb-4">
<label for="companySelector" class="block text-sm font-medium text-gray-700 mb-2">
Empresa <span class="text-red-500">*</span>
</label>
<p-select
inputId="companySelector"
[(ngModel)]="selectedCompanyId"
[options]="companies()"
optionLabel="text"
optionValue="value"
placeholder="Seleccione una empresa"
[loading]="loadingCompanies()"
(onChange)="onCompanyChange()"
[style]="{ width: '100%', maxWidth: '400px' }">
</p-select>
</div>
</div>
<!-- Empty state -->
@if (showEmptyMessage()) {
<div class="bg-blue-50 border border-blue-200 rounded-lg p-8 text-center">
<i class="pi pi-building text-5xl text-blue-400 mb-4 block"></i>
<h3 class="text-xl font-semibold text-gray-700 mb-2">
Seleccione una empresa
</h3>
<p class="text-gray-600">
Seleccione una empresa del selector superior
</p>
</div>
}
<!-- Data table -->
@if (!showEmptyMessage()) {
<p-table [value]="biometrics()" [loading]="loading()">
<!-- Table content -->
</p-table>
}
</div>
```
### Características del Patrón Local
1. **Independiente**: No afecta otros módulos
2. **Flexible**: Puede cambiar compañía libremente
3. **Visible**: Usuario ve claramente qué compañía está viendo
4. **No Persistente**: La selección no se guarda (se resetea al recargar)
---
## Comparación de Patrones
| Característica | Global Context | Local Selection |
|----------------|----------------|-----------------|
| **Persistencia** | ✅ localStorage | ❌ No persiste |
| **Afecta toda la app** | ✅ Sí | ❌ Solo el módulo |
| **Selector visible** | En barra superior | En el módulo |
| **Casos de uso** | Módulos operativos | Módulos administrativos |
| **Ejemplos** | Salary Increases, Vacations | Biometrics, Config |
## Mejores Prácticas
### ✅ Do's
- Usar Global Context para módulos operativos del día a día
- Usar Local Selection para módulos administrativos
- Siempre verificar `if (!company)` antes de cargar datos
- Usar `effect()` para reaccionar a cambios de compañía
- Resetear paginación cuando cambia la compañía
- Mostrar mensaje claro cuando no hay compañía seleccionada
- Usar `takeUntilDestroyed()` para limpiar suscripciones
### ❌ Don'ts
- No mezclar ambos patrones en el mismo módulo
- No olvidar `cdr.markForCheck()` con OnPush
- No cargar datos sin verificar que hay compañía seleccionada
- No permitir crear/editar sin compañía seleccionada
- No guardar `companyId` en estado local si usas Global Context
## Migración de Patrones
### De Global a Local
Si un módulo necesita cambiar de Global Context a Local Selection:
1. Remover inyección de `CompanyContextService`
2. Agregar `CompaniesService` y `FormsModule`
3. Crear signals: `companies`, `selectedCompanyId`
4. Agregar `loadCompanies()` y selector en template
5. Cambiar `effect()` para escuchar `selectedCompanyId()`
6. Actualizar empty states y condiciones
### De Local a Global
Si un módulo necesita cambiar de Local Selection a Global Context:
1. Agregar `CompanyContextService`
2. Remover signals locales de compañía
3. Usar `selectedCompany()` del servicio
4. Remover selector del template
5. Actualizar `effect()` para escuchar `selectedCompany()`
## Ejemplos Reales
### Usando Global Context
- `src/app/features/salary-increase/pages/salary-increase-list/`
- `src/app/features/vacations/pages/vacations-list/`
- `src/app/features/permissions/pages/permissions-list/`
### Usando Local Selection
- `src/app/features/biometrics/pages/biometric-list/`
## Referencias
- Servicio Global: `src/app/core/services/company-context.service.ts`
- Servicio Compañías: `src/app/features/structures/services/companies.service.ts`
- Modelo: `src/app/features/structures/models/company.model.ts`