consolodiated benchmark moal

This commit is contained in:
2026-04-08 16:39:19 -07:00
parent 3ad5cde69e
commit f798801212
9 changed files with 61 additions and 403 deletions
@@ -1,48 +0,0 @@
<div class="toolbar">
<button class="toolbar-btn back-btn" (click)="onBack()">&#8593; Benchmarks</button>
<span class="toolbar-title">{{ isNew() ? 'New Benchmark' : 'Benchmark Detail' }}</span>
<span class="spacer"></span>
</div>
@if (errorMessage()) {
<p class="error">{{ errorMessage() }}</p>
}
@if (loaded()) {
<div class="detail-card">
<div class="field">
<span class="field-label">Goal: {{ goalCategory }}</span>
</div>
<div class="field">
<label class="field-label" for="benchmarkText">Benchmark</label>
<textarea id="benchmarkText" class="field-input field-textarea" [(ngModel)]="benchmarkText" rows="4"
placeholder="Enter benchmark text..."></textarea>
</div>
<div class="field">
<label class="field-label" for="shortName">Short Name</label>
<input id="shortName" class="field-input" type="text" [(ngModel)]="shortName" maxlength="50"
placeholder="Optional" />
</div>
@if (!isNew()) {
<div class="metadata">
@if (createdByName) {
<span class="meta-item">Created by: {{ createdByName }}</span>
}
<span class="meta-item">Created: {{ createdAt | date:'medium' }}</span>
@if (updatedAt) {
<span class="meta-item">Updated: {{ updatedAt | date:'medium' }}</span>
}
</div>
}
<div class="actions">
<button class="toolbar-btn" (click)="onCancel()" [disabled]="!hasChanges()">Cancel</button>
<button class="toolbar-btn save-btn" (click)="onSave()" [disabled]="!hasChanges() || saving()">
{{ saving() ? 'Saving...' : 'Save' }}
</button>
</div>
</div>
}
@if (successMessage()) {
<p class="success">{{ successMessage() }}</p>
}
@@ -1,120 +0,0 @@
:host {
display: flex;
flex-direction: column;
height: 100%;
padding: 24px 28px;
}
.toolbar {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 20px;
flex-shrink: 0;
}
.toolbar-btn {
padding: 6px 14px;
background: transparent;
color: var(--accent-indigo);
border: 1px solid var(--accent-indigo);
border-radius: var(--radius-md);
font-size: 13px;
font-weight: 500;
cursor: pointer;
font-family: inherit;
&:hover {
background: #EEF2FF;
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
.toolbar-title {
font-weight: 600;
font-size: 18px;
color: var(--text-primary);
}
.spacer {
flex: 1;
}
.error {
font-size: 13px;
color: #dc2626;
margin: 0 0 12px;
}
.success {
font-size: 13px;
color: #16a34a;
margin: 12px 0 0;
}
.detail-card {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-xl);
padding: 22px;
max-width: 600px;
}
.field {
display: flex;
flex-direction: column;
margin-bottom: 14px;
}
.field-label {
font-size: 12px;
font-weight: 600;
color: #666;
margin-bottom: 4px;
}
.field-input {
padding: 8px 10px;
border: 1px solid var(--border-muted);
border-radius: var(--radius-md);
font-size: 13px;
font-family: inherit;
outline: none;
}
.field-textarea {
resize: vertical;
min-height: 70px;
}
.metadata {
display: flex;
gap: 1.5rem;
margin-bottom: 14px;
}
.meta-item {
font-size: 12px;
color: var(--text-muted);
}
.actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 8px;
}
.save-btn {
background: var(--accent-indigo) !important;
color: #fff !important;
border-color: var(--accent-indigo) !important;
&:hover {
background: #3730A3 !important;
}
}
@@ -1,23 +0,0 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BenchmarkCardFull } from './benchmark-card-full';
describe('BenchmarkCardFull', () => {
let component: BenchmarkCardFull;
let fixture: ComponentFixture<BenchmarkCardFull>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [BenchmarkCardFull]
})
.compileComponents();
fixture = TestBed.createComponent(BenchmarkCardFull);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
@@ -1,181 +0,0 @@
import { Component, inject, signal, OnDestroy } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms';
import { DatePipe } from '@angular/common';
import { Subscription } from 'rxjs';
import { StudentService } from '../../../shared/services/student.service';
import { BenchmarkDto } from '../../../shared/classes/benchmark.dto';
@Component({
selector: 'app-benchmark-card-full',
imports: [FormsModule, DatePipe],
templateUrl: './benchmark-card-full.html',
styleUrl: './benchmark-card-full.scss',
})
export class BenchmarkCardFull implements OnDestroy {
// ************************** Constructor **************************
constructor() {
this.paramSub = this.route.paramMap.subscribe(params => {
this.studentId = params.get('studentId')!;
this.goalId = params.get('goalId')!;
this.benchmarkId = params.get('benchmarkId') ?? null;
this.loadBenchmark();
});
}
// ************************** Declarations *************************
private readonly studentService = inject(StudentService);
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly paramSub: Subscription;
private studentId!: string;
private goalId!: string;
private benchmarkId: string | null = null;
protected readonly loaded = signal(false);
protected readonly isNew = signal(false);
protected readonly errorMessage = signal<string | null>(null);
protected readonly successMessage = signal<string | null>(null);
protected readonly saving = signal(false);
// Form fields
protected benchmarkText = '';
protected shortName = '';
private savedBenchmarkText = '';
private savedShortName = '';
// Read-only metadata
protected goalCategory = '';
protected createdByName = '';
protected createdAt: Date | null = null;
protected updatedAt: Date | null = null;
// ************************** Properties ***************************
// *****************************************************************
// Returns true if the benchmark text has unsaved changes.
// *****************************************************************
hasChanges(): boolean {
return this.benchmarkText !== this.savedBenchmarkText
|| this.shortName !== this.savedShortName;
}
// ************************ Public Methods *************************
// ************************ Event Handlers *************************
// *****************************************************************
// Saves changes or creates a new benchmark.
// *****************************************************************
async onSave() {
this.saving.set(true);
this.errorMessage.set(null);
this.successMessage.set(null);
if (this.isNew()) {
const result = await this.studentService.createBenchmark(this.studentId, {
goalId: this.goalId,
benchmark: this.benchmarkText,
shortName: this.shortName || undefined,
});
this.saving.set(false);
if (result.success) {
this.successMessage.set('Benchmark created.');
this.savedBenchmarkText = this.benchmarkText;
this.savedShortName = this.shortName;
this.studentService.notifyDataChanged();
if (result.payload?.benchmarkId) {
this.router.navigate(['/students', this.studentId, 'goals', this.goalId, 'benchmarks', result.payload.benchmarkId]);
}
} else {
this.errorMessage.set(result.message);
}
} else {
const result = await this.studentService.updateBenchmark(this.studentId, this.benchmarkId!, this.benchmarkText, this.shortName || undefined);
this.saving.set(false);
if (result.success) {
this.savedBenchmarkText = this.benchmarkText;
this.savedShortName = this.shortName;
this.successMessage.set('Changes saved.');
} else {
this.errorMessage.set(result.message);
}
}
}
// *****************************************************************
// Reverts the benchmark text to the last-saved value.
// *****************************************************************
onCancel() {
this.benchmarkText = this.savedBenchmarkText;
this.shortName = this.savedShortName;
this.errorMessage.set(null);
this.successMessage.set(null);
}
onBack() {
this.router.navigate(['/students', this.studentId, 'goals', this.goalId]);
}
ngOnDestroy() {
this.paramSub.unsubscribe();
}
// ********************** Support Procedures ***********************
// *****************************************************************
// Loads existing benchmark data or sets up new-benchmark state.
// *****************************************************************
private loadBenchmark() {
if (!this.benchmarkId) {
this.isNew.set(true);
this.benchmarkText = '';
this.shortName = '';
this.savedBenchmarkText = '';
this.savedShortName = '';
this.loadGoalCategory();
this.loaded.set(true);
return;
}
this.isNew.set(false);
this.studentService.getBenchmarksForStudent(this.studentId).then(result => {
if (!result.success || !result.payload) {
this.errorMessage.set(result.message);
return;
}
const bm = result.payload.benchmarks.find(b => b.benchmarkId === this.benchmarkId);
if (!bm) {
this.errorMessage.set('Benchmark not found.');
return;
}
this.benchmarkText = bm.benchmark;
this.shortName = bm.shortName ?? '';
this.savedBenchmarkText = bm.benchmark;
this.savedShortName = bm.shortName ?? '';
this.goalCategory = bm.goalCategory;
this.createdByName = bm.createdByName;
this.createdAt = bm.createdAt;
this.updatedAt = bm.updatedAt;
this.loaded.set(true);
});
}
// *****************************************************************
// Loads the goal category for a new benchmark.
// *****************************************************************
private loadGoalCategory() {
this.studentService.getGoalsForStudent(this.studentId).then(result => {
if (result.success && result.payload) {
const goal = result.payload.goals.find(g => g.goalId === this.goalId);
this.goalCategory = goal?.category ?? '';
}
});
}
}
@@ -1,11 +1,13 @@
<app-modal-shell title="Edit Benchmark" (closed)="closed.emit()">
<app-modal-shell [title]="modalTitle" (closed)="closed.emit()">
<div class="field">
<label class="field-label">Short Name</label>
<input class="field-input" type="text" [(ngModel)]="shortName" />
<label class="field-label">Benchmark</label>
<textarea class="field-input field-textarea" [(ngModel)]="benchmarkText"
placeholder="Enter benchmark text..."></textarea>
</div>
<div class="field">
<label class="field-label">Description</label>
<textarea class="field-input field-textarea" [(ngModel)]="benchmarkText"></textarea>
<label class="field-label">Short Name</label>
<input class="field-input" type="text" [(ngModel)]="shortName" maxlength="50"
placeholder="Optional" />
</div>
@if (errorMessage()) {
@@ -14,8 +16,8 @@
<div class="modal-actions">
<button class="btn-secondary" (click)="closed.emit()">Cancel</button>
<button class="btn-primary" (click)="onSave()" [disabled]="saving()">
{{ saving() ? 'Saving...' : 'Save' }}
<button class="btn-primary" (click)="onSave()" [disabled]="saving() || !benchmarkText.trim()">
{{ saving() ? 'Saving...' : submitLabel }}
</button>
</div>
</app-modal-shell>
@@ -14,7 +14,10 @@ export class EditBenchmarkModal {
private readonly studentService = inject(StudentService);
readonly studentId = input.required<string>();
readonly benchmark = input.required<BenchmarkDto>();
readonly goalId = input.required<string>();
/** null for new benchmark, populated for edit */
readonly benchmark = input<BenchmarkDto | null>(null);
readonly saved = output<void>();
readonly closed = output<void>();
@@ -24,24 +27,52 @@ export class EditBenchmarkModal {
protected shortName = '';
protected benchmarkText = '';
protected get isEditMode(): boolean {
return !!this.benchmark();
}
protected get modalTitle(): string {
return this.isEditMode ? 'Edit Benchmark' : 'Add Benchmark';
}
protected get submitLabel(): string {
return this.isEditMode ? 'Save' : 'Add Benchmark';
}
ngOnInit() {
const b = this.benchmark();
if (b) {
this.shortName = b.shortName ?? '';
this.benchmarkText = b.benchmark;
}
}
async onSave() {
if (!this.shortName.trim()) return;
if (!this.benchmarkText.trim()) return;
this.saving.set(true);
this.errorMessage.set(null);
if (this.isEditMode) {
const result = await this.studentService.updateBenchmark(
this.studentId(),
this.benchmark().benchmarkId,
this.benchmark()!.benchmarkId,
this.benchmarkText,
this.shortName,
this.shortName || undefined,
);
this.saving.set(false);
if (result.success) {
this.studentService.notifyDataChanged();
this.saved.emit();
} else {
this.errorMessage.set(result.message);
}
} else {
const result = await this.studentService.createBenchmark(this.studentId(), {
goalId: this.goalId(),
benchmark: this.benchmarkText,
shortName: this.shortName || undefined,
});
this.saving.set(false);
if (result.success) {
@@ -51,4 +82,5 @@ export class EditBenchmarkModal {
this.errorMessage.set(result.message);
}
}
}
}
@@ -12,7 +12,8 @@
(closed)="showGoalModal.set(null)" />
}
@if (showEditBenchmarkModal()) {
<app-edit-benchmark-modal [studentId]="studentId()!" [benchmark]="showEditBenchmarkModal()!"
<app-edit-benchmark-modal [studentId]="studentId()!" [goalId]="selectedGoal()!.goalId"
[benchmark]="showEditBenchmarkModal() === 'new' ? null : $any(showEditBenchmarkModal())"
(saved)="onEditBenchmarkSaved()" (closed)="showEditBenchmarkModal.set(null)" />
}
@if (showEditEventModal()) {
@@ -73,7 +73,7 @@ export class Workspace {
// Modal states
protected readonly showGoalModal = signal<StudentGoalItem | 'add' | null>(null);
protected readonly showEditBenchmarkModal = signal<BenchmarkDto | null>(null);
protected readonly showEditBenchmarkModal = signal<BenchmarkDto | 'new' | null>(null);
protected readonly showEditEventModal = signal<ProgressEventDto | null | 'new'>(null);
// ************************** Properties ***************************
@@ -148,8 +148,7 @@ export class Workspace {
}
onAddBenchmark() {
// Navigate to the new benchmark route (still uses the old page for creation)
this.router.navigate(['/students', this.studentId(), 'goals', this.selectedGoal()!.goalId, 'benchmarks', 'new']);
this.showEditBenchmarkModal.set('new');
}
onNewEvent() {
@@ -3,8 +3,6 @@ import { Home } from './pages/home/home';
import { Workspace } from './components/workspace/workspace';
import { Reports } from './components/reports/reports';
import { StudentProgressReport } from './components/student-progress-report/student-progress-report';
import { BenchmarkCardFull } from './components/benchmark-card-full/benchmark-card-full';
export default [
{
path: '',
@@ -14,8 +12,6 @@ export default [
{ path: 'students', component: Workspace },
{ path: 'students/:studentId', component: Workspace },
{ path: 'students/:studentId/goals/:goalId', component: Workspace },
// Benchmark creation still uses the dedicated page (no create-benchmark modal yet)
{ path: 'students/:studentId/goals/:goalId/benchmarks/new', component: BenchmarkCardFull },
{ path: 'reports', component: Reports },
{ path: 'reports/student-progress', component: StudentProgressReport },
],