mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 06:27:37 +00:00
consolodiated benchmark moal
This commit is contained in:
-48
@@ -1,48 +0,0 @@
|
||||
<div class="toolbar">
|
||||
<button class="toolbar-btn back-btn" (click)="onBack()">↑ 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>
|
||||
}
|
||||
-120
@@ -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;
|
||||
}
|
||||
}
|
||||
-23
@@ -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();
|
||||
});
|
||||
});
|
||||
-181
@@ -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 ?? '';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -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>
|
||||
|
||||
+48
-16
@@ -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,31 +27,60 @@ 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();
|
||||
this.shortName = b.shortName ?? '';
|
||||
this.benchmarkText = b.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);
|
||||
|
||||
const result = await this.studentService.updateBenchmark(
|
||||
this.studentId(),
|
||||
this.benchmark().benchmarkId,
|
||||
this.benchmarkText,
|
||||
this.shortName,
|
||||
);
|
||||
if (this.isEditMode) {
|
||||
const result = await this.studentService.updateBenchmark(
|
||||
this.studentId(),
|
||||
this.benchmark()!.benchmarkId,
|
||||
this.benchmarkText,
|
||||
this.shortName || undefined,
|
||||
);
|
||||
this.saving.set(false);
|
||||
|
||||
this.saving.set(false);
|
||||
|
||||
if (result.success) {
|
||||
this.studentService.notifyDataChanged();
|
||||
this.saved.emit();
|
||||
if (result.success) {
|
||||
this.studentService.notifyDataChanged();
|
||||
this.saved.emit();
|
||||
} else {
|
||||
this.errorMessage.set(result.message);
|
||||
}
|
||||
} else {
|
||||
this.errorMessage.set(result.message);
|
||||
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.studentService.notifyDataChanged();
|
||||
this.saved.emit();
|
||||
} else {
|
||||
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 },
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user