Código generado con la skill Angular Developer — ClientDashboardComponent con signals modernos
@Injectable({ providedIn: 'root' }) export class ClientService { // Signal writable — fuente de verdad readonly clients = signal<Client[]>([ { id: 1, name: 'Cafetería Origen', sector: 'HoReCa', plan: 'growth', status: 'activo', mrr: 320, }, { id: 2, name: 'LegalBot SL', sector: 'LegalTech', plan: 'enterprise', status: 'activo', mrr: 850, }, { id: 3, name: 'Farmacia Biosana', sector: 'Salud', plan: 'starter', status: 'en-riesgo', mrr: 180, }, { id: 4, name: 'MindMap Academy', sector: 'EdTech', plan: 'growth', status: 'pausado', mrr: 0, }, { id: 5, name: 'DataFlow Analytics', sector: 'SaaS', plan: 'enterprise', status: 'activo', mrr: 1200, }, ]); // Computed — estadísticas derivadas readonly totalMrr = computed(() => this.clients().reduce( (sum, c) => sum + c.mrr, 0 ) ); readonly activeCount = computed(() => this.clients() .filter(c => c.status === 'activo') .length ); readonly activePercent = computed(() => { const total = this.clients().length; return total ? Math.round( (this.activeCount() / total) * 100 ) : 0; }); }
@Component({ selector: 'app-client-dashboard', standalone: true, templateUrl: './client-dashboard.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ClientDashboardComponent { // inject() en lugar de constructor DI private clientSvc = inject(ClientService); // Exposición de computed del servicio protected readonly clients = this.clientSvc.clients; protected readonly totalMrr = this.clientSvc.totalMrr; protected readonly activeCount= this.clientSvc.activeCount; protected readonly activePct = this.clientSvc.activePercent; // linkedSignal — filtro ligado a la UI protected readonly filter = linkedSignal( () => 'todos' as StatusFilter ); // computed — lista filtrada reactiva protected readonly filtered = computed(() => { const f = this.filter(); return f === 'todos' ? this.clients() : this.clients().filter(c => c.status === f ); }); setFilter(f: StatusFilter): void { this.filter.set(f); } }
<div class="dashboard"> <!-- Estadísticas con computed signals --> <div class="stats-grid"> <app-stat-card label="Clientes" [value]="clients().length" /> <app-stat-card label="MRR Total" [value]="totalMrr() | currency:'EUR'" /> <app-stat-card label="% Activos" [value]="activePct() + '%'" /> </div> <!-- Filtros reactivos (linkedSignal) --> <div class="filter-row"> <button @for="(f of filters; track f.key)" [class.active]="filter() === f.key" (click)="setFilter(f.key)" > {{ f.label }} </button> </div> <!-- Lista filtrada con @for moderno --> <table> <tbody> @for (client of filtered(); track client.id) { <tr> <td>{{ client.name }}</td> <td>{{ client.sector }}</td> <td> <app-plan-badge [plan]="client.plan" /> </td> <td> <app-status-badge [status]="client.status" /> </td> <td> <span class="mrr"> {{ client.mrr | currency:'EUR' }} </span> </td> </tr> } @empty { <tr><td colspan="5"> Sin clientes para este filtro. </td></tr> } </tbody> </table> </div>
| Cliente | Plan | Estado | MRR |
|---|---|---|---|
|
Cafetería Origen
HoReCa
|
Growth | Activo | 320 € |
|
LegalBot SL
LegalTech
|
Enterprise | Activo | 850 € |
|
Farmacia Biosana
Salud
|
Starter | En riesgo | 180 € |
|
MindMap Academy
EdTech
|
Growth | Pausado | 0 € |
|
DataFlow Analytics
SaaS
|
Enterprise | Activo | 1.200 € |
clients = signal([…]). Angular detecta cambios automáticamente sin ngOnChanges.totalMrr, activeCount, activePct. Solo recalcula cuando dependen cambia.filter = linkedSignal(() => 'todos'). Se reinicia al cambiar el origen.