Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 0 additions & 29 deletions talent-management/src/app/routes/dashboard/dashboard.html
Original file line number Diff line number Diff line change
@@ -1,34 +1,5 @@
<page-header />

<!-- AI Insights Card (hidden when aiEnabled is false) -->
<mat-card *ngIf="aiEnabled" class="ai-insights-card">
<mat-card-header>
<mat-icon mat-card-avatar>smart_toy</mat-icon>
<mat-card-title>AI Workforce Insights</mat-card-title>
<mat-card-subtitle>Generated from live dashboard metrics</mat-card-subtitle>
</mat-card-header>
<mat-card-content>

<!-- Loading -->
<div *ngIf="aiInsightLoading" class="ai-insight-loading">
<mat-spinner diameter="24"></mat-spinner>
<span>Analyzing workforce data&hellip;</span>
</div>

<!-- Insight text -->
<p *ngIf="!aiInsightLoading && aiInsight" class="ai-insight-text">
{{ aiInsight }}
</p>

<!-- Error -->
<p *ngIf="!aiInsightLoading && aiInsightError" class="ai-insight-error">
<mat-icon>warning</mat-icon>
{{ aiInsightError }}
</p>

</mat-card-content>
</mat-card>

<!-- Loading Spinner -->
<div *ngIf="loading" class="loading-spinner">
<mat-spinner></mat-spinner>
Expand Down
53 changes: 3 additions & 50 deletions talent-management/src/app/routes/dashboard/dashboard.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { Component, OnInit, inject } from '@angular/core';
import { Router } from '@angular/router';
import { CommonModule } from '@angular/common';
import { MatCardModule } from '@angular/material/card';
Expand All @@ -9,13 +9,10 @@ import { MatListModule } from '@angular/material/list';
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
import { BaseChartDirective } from 'ng2-charts';
import { ChartConfiguration } from 'chart.js';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { PageHeader } from '@shared';
import { HasRoleDirective } from '../../shared/directives/has-role.directive';
import { DashboardMetrics, DepartmentMetric, PositionMetric, SalaryRangeMetric } from '../../models';
import { DashboardService, AiService } from '../../services/api';
import { environment } from '../../../environments/environment';
import { DashboardService } from '../../services/api';

@Component({
selector: 'app-dashboard',
Expand All @@ -35,25 +32,17 @@ import { environment } from '../../../environments/environment';
HasRoleDirective,
],
})
export class Dashboard implements OnInit, OnDestroy {
export class Dashboard implements OnInit {
private dashboardService = inject(DashboardService);
private aiService = inject(AiService);
private router = inject(Router);
private snackBar = inject(MatSnackBar);
private destroy$ = new Subject<void>();

// Loading state
loading = true;

// Dashboard metrics
metrics: DashboardMetrics | null = null;

// AI Insights state
aiEnabled = environment.aiEnabled;
aiInsight = '';
aiInsightLoading = false;
aiInsightError = '';

// Chart configurations
departmentChartData: ChartConfiguration<'pie'>['data'] | null = null;
departmentChartOptions: ChartConfiguration<'pie'>['options'] = {
Expand Down Expand Up @@ -152,11 +141,6 @@ export class Dashboard implements OnInit, OnDestroy {
this.loadDashboardMetrics();
}

ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}

loadDashboardMetrics(): void {
this.loading = true;

Expand All @@ -165,9 +149,6 @@ export class Dashboard implements OnInit, OnDestroy {
this.metrics = metrics;
this.prepareCharts(metrics);
this.loading = false;
if (this.aiEnabled) {
this.loadAiInsight(metrics);
}
},
error: error => {
console.error('Error loading dashboard metrics:', error);
Expand All @@ -177,34 +158,6 @@ export class Dashboard implements OnInit, OnDestroy {
});
}

private loadAiInsight(metrics: DashboardMetrics): void {
this.aiInsightLoading = true;
this.aiInsightError = '';
this.aiInsight = '';

const systemPrompt = `You are an HR analytics assistant. Analyze the following workforce metrics and provide a concise executive summary (3-4 sentences) highlighting key observations, any notable patterns, and one actionable recommendation. Be specific — reference the actual numbers.

Workforce Metrics:
${JSON.stringify(metrics, null, 2)}`;

const question = 'Provide a brief executive summary of the current workforce.';

this.aiService
.chat(question, systemPrompt)
.pipe(takeUntil(this.destroy$))
.subscribe({
next: response => {
this.aiInsight = response.reply;
this.aiInsightLoading = false;
},
error: err => {
this.aiInsightError =
err?.error?.detail ?? 'AI insights unavailable. Is the API running with AiEnabled: true?';
this.aiInsightLoading = false;
},
});
}

private prepareCharts(metrics: DashboardMetrics): void {
this.prepareDepartmentChart(metrics.employeesByDepartment);
this.preparePositionChart(metrics.employeesByPosition);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,41 +1,5 @@
<page-header></page-header>

<!-- Natural Language Search Bar (hidden when aiEnabled is false) -->
<mat-card *ngIf="aiEnabled" class="nl-search-card">
<mat-card-content>
<mat-form-field appearance="outline" class="nl-search-field">
<mat-label>
<mat-icon>smart_toy</mat-icon>
Search in plain English
</mat-label>
<input
matInput
#nlInput
type="text"
[value]="nlQuery"
(input)="onNlQueryChange(nlInput.value)"
placeholder='e.g. "find all engineers" or "employees named Johnson"'
autocomplete="off"
/>
<mat-icon matSuffix *ngIf="!nlLoading && !nlQuery">search</mat-icon>
<mat-spinner matSuffix diameter="20" *ngIf="nlLoading"></mat-spinner>
<button mat-icon-button matSuffix *ngIf="nlQuery && !nlLoading" (click)="clearNlSearch()" type="button">
<mat-icon>close</mat-icon>
</button>
</mat-form-field>

<div *ngIf="nlParsedExpression && !nlLoading" class="nl-parsed-hint">
<mat-icon>auto_awesome</mat-icon>
<span>AI interpreted: <em>{{ nlParsedExpression }}</em></span>
</div>

<div *ngIf="nlError" class="nl-error">
<mat-icon>warning</mat-icon>
<span>{{ nlError }}</span>
</div>
</mat-card-content>
</mat-card>

<mat-card>
<mat-card-header>
<mat-card-title>Employee Directory</mat-card-title>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,10 @@ import { PageHeader } from '@shared/components/page-header/page-header';
import { ConfirmDialogComponent, ConfirmDialogData } from '../../shared/components/confirm-dialog/confirm-dialog';
import { Employee } from '../../models';
import { EmployeeService } from '../../services/api';
import { AiService, NlEmployeeFilter } from '../../services/api/ai.service';
import { OidcAuthService } from '../../core/authentication/oidc-auth.service';
import { HasRoleDirective } from '../../shared/directives/has-role.directive';
import { Observable, Subject, of } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap, map, startWith, catchError, takeUntil } from 'rxjs/operators';
import { environment } from '../../../environments/environment';

@Component({
selector: 'app-employee-list',
Expand Down Expand Up @@ -51,7 +49,6 @@ import { environment } from '../../../environments/environment';
})
export class EmployeeListComponent implements OnInit, OnDestroy {
private employeeService = inject(EmployeeService);
private aiService = inject(AiService);
private authService = inject(OidcAuthService);
private router = inject(Router);
private fb = inject(FormBuilder);
Expand All @@ -75,14 +72,6 @@ export class EmployeeListComponent implements OnInit, OnDestroy {
filteredEmails$!: Observable<string[]>;
filteredPositionTitles$!: Observable<string[]>;

// AI Natural Language Search
aiEnabled = environment.aiEnabled;
nlQuery = '';
nlLoading = false;
nlError = '';
nlParsedExpression = '';
private nlSearch$ = new Subject<string>();

private destroy$ = new Subject<void>();

// Table columns
Expand All @@ -100,7 +89,6 @@ export class EmployeeListComponent implements OnInit, OnDestroy {
this.setupAutocomplete();
this.setupAutoSubmit();
this.loadEmployees();
this.setupNlSearch();
}

ngOnDestroy(): void {
Expand Down Expand Up @@ -318,61 +306,4 @@ export class EmployeeListComponent implements OnInit, OnDestroy {
return this.authService.isHRAdmin() || this.authService.isManager();
}

// NL Search ---------------------------------------------------------------

onNlQueryChange(value: string): void {
this.nlQuery = value;
this.nlSearch$.next(value);
}

clearNlSearch(): void {
this.nlQuery = '';
this.nlParsedExpression = '';
this.nlError = '';
this.searchForm.reset();
this.pageNumber = 1;
this.loadEmployees();
}

private setupNlSearch(): void {
this.nlSearch$
.pipe(
debounceTime(600),
distinctUntilChanged(),
switchMap(query => {
if (!query || query.length < 3) {
this.nlParsedExpression = '';
this.nlError = '';
return of(null);
}
this.nlLoading = true;
this.nlError = '';
return this.aiService.nlEmployeeSearch(query).pipe(
catchError(err => {
this.nlLoading = false;
this.nlError = err?.error?.detail ?? 'Could not parse query. Try rephrasing.';
return of(null);
})
);
}),
takeUntil(this.destroy$)
)
.subscribe(filter => {
if (filter) {
this.nlLoading = false;
this.nlParsedExpression = filter.parsedExpression;
this.applyNlFilter(filter);
}
});
}

private applyNlFilter(filter: NlEmployeeFilter): void {
this.searchForm.patchValue({
FirstName: filter.firstName,
LastName: filter.lastName,
Email: filter.email,
EmployeeNumber: filter.employeeNumber,
PositionTitle: filter.positionTitle,
});
}
}
Original file line number Diff line number Diff line change
@@ -1,74 +1,5 @@
<page-header></page-header>

<!-- Semantic Position Search -->
<mat-card class="nl-search-card" *ngIf="aiEnabled">
<mat-card-header>
<mat-card-title>
<mat-icon style="vertical-align: middle; margin-right: 8px;">auto_awesome</mat-icon>
Semantic Position Search
</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-form-field appearance="outline" style="width: 100%;">
<mat-label>Describe the position you're looking for…</mat-label>
<input
matInput
#semanticInput
[value]="semanticQuery"
(input)="onSemanticQueryChange(semanticInput.value)"
placeholder="e.g. senior software engineer in finance with high salary"
/>
<mat-icon matSuffix>search</mat-icon>
</mat-form-field>

<div *ngIf="semanticLoading" style="display: flex; align-items: center; gap: 8px; margin-top: 8px;">
<mat-spinner diameter="20"></mat-spinner>
<span>Searching…</span>
</div>

<div *ngIf="semanticError" class="nl-error">{{ semanticError }}</div>

<div *ngIf="semanticResults !== null && !semanticLoading">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;">
<span class="nl-parsed-hint">{{ semanticResults.length }} result(s) found</span>
<button mat-stroked-button (click)="clearSemanticSearch()">
<mat-icon>clear</mat-icon> Clear
</button>
</div>

<table mat-table [dataSource]="semanticResults" class="position-table" style="width: 100%;">
<ng-container matColumnDef="score">
<th mat-header-cell *matHeaderCellDef>Score</th>
<td mat-cell *matCellDef="let r">{{ r.score | number:'1.3-3' }}</td>
</ng-container>

<ng-container matColumnDef="positionNumber">
<th mat-header-cell *matHeaderCellDef>Position #</th>
<td mat-cell *matCellDef="let r">{{ r.positionNumber }}</td>
</ng-container>

<ng-container matColumnDef="positionTitle">
<th mat-header-cell *matHeaderCellDef>Title</th>
<td mat-cell *matCellDef="let r">{{ r.positionTitle }}</td>
</ng-container>

<ng-container matColumnDef="departmentName">
<th mat-header-cell *matHeaderCellDef>Department</th>
<td mat-cell *matCellDef="let r">{{ r.departmentName }}</td>
</ng-container>

<ng-container matColumnDef="salaryRangeName">
<th mat-header-cell *matHeaderCellDef>Salary Range</th>
<td mat-cell *matCellDef="let r">{{ r.salaryRangeName }}</td>
</ng-container>

<tr mat-header-row *matHeaderRowDef="['score','positionNumber','positionTitle','departmentName','salaryRangeName']"></tr>
<tr mat-row *matRowDef="let row; columns: ['score','positionNumber','positionTitle','departmentName','salaryRangeName'];"></tr>
</table>
</div>
</mat-card-content>
</mat-card>

<mat-card>
<mat-card-header>
<mat-card-title>Positions</mat-card-title>
Expand Down
Loading