CULTIVA IA desarrollador-angular Angular 18 · Signals

Panel de Seguimiento de Clientes

Código generado con la skill Angular Developer — ClientDashboardComponent con signals modernos

Código generado
📄 client.service.ts TypeScript nuevo
@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;
  });
}
📄 client-dashboard.component.ts TypeScript
@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);
  }
}
📄 client-dashboard.component.html Template
<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>
Vista previa — ClientDashboardComponent
Clientes
5
MRR Total
2.550 €
% Activos
60%
Todos (5) Activos (3) En riesgo (1) Pausados (1)
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 €
signal()
Fuente de verdad mutable: clients = signal([…]). Angular detecta cambios automáticamente sin ngOnChanges.
computed()
Estadísticas derivadas perezosas: totalMrr, activeCount, activePct. Solo recalcula cuando dependen cambia.
linkedSignal()
Estado del filtro UI ligado a la fuente: filter = linkedSignal(() => 'todos'). Se reinicia al cambiar el origen.