modal consolidation

This commit is contained in:
2026-04-08 15:48:28 -07:00
parent 9e1bef0ee1
commit e2834c1d5b
9 changed files with 130 additions and 164 deletions
@@ -1,56 +0,0 @@
import { Component, inject, input, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ModalShell } from '../modal-shell/modal-shell';
import { CreateGoalDto } from '../../../shared/classes/create-goal.dto';
import { StudentGoalItem } from '../../../shared/classes/student-goal';
import { StudentService } from '../../../shared/services/student.service';
@Component({
selector: 'app-add-goal-modal',
imports: [FormsModule, ModalShell],
templateUrl: './add-goal-modal.html',
styleUrl: './add-goal-modal.scss',
})
export class AddGoalModal {
private readonly studentService = inject(StudentService);
readonly studentId = input.required<string>();
readonly existingGoals = input.required<StudentGoalItem[]>();
readonly nextIepDate = input<string | null>();
readonly goalCreated = output<StudentGoalItem>();
readonly cancelled = output<void>();
protected readonly isSubmitting = signal(false);
protected readonly errorMessage = signal<string | null>(null);
protected form: CreateGoalDto = {
description: '',
category: '',
baseline: '',
goalParentId: null,
targetCompletionDate: null,
};
ngOnInit() {
const iepDate = this.nextIepDate?.();
if (iepDate) {
this.form.targetCompletionDate = iepDate;
}
}
async onSubmit() {
if (!this.form.category.trim()) return;
this.errorMessage.set(null);
this.isSubmitting.set(true);
const result = await this.studentService.createGoal(this.studentId(), this.form);
this.isSubmitting.set(false);
if (!result.success) {
this.errorMessage.set(result.message);
return;
}
this.goalCreated.emit(result.payload!);
}
}
@@ -1,29 +0,0 @@
<app-modal-shell title="Edit Goal" (closed)="closed.emit()">
<div class="field">
<label class="field-label">Category</label>
<input class="field-input" type="text" [(ngModel)]="category" placeholder="e.g. Reading, Math, Behavior…" />
</div>
<div class="field">
<label class="field-label">Description</label>
<textarea class="field-input field-textarea" [(ngModel)]="description"></textarea>
</div>
<div class="field">
<label class="field-label">Baseline</label>
<textarea class="field-input field-textarea" [(ngModel)]="baseline" placeholder="Enter baseline..."></textarea>
</div>
<div class="field">
<label class="field-label">Due Date</label>
<input class="field-input" type="date" [(ngModel)]="targetCompletionDate" />
</div>
@if (errorMessage()) {
<p class="error">{{ errorMessage() }}</p>
}
<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>
</div>
</app-modal-shell>
@@ -1 +0,0 @@
/* Inherits all styles from modal-shell via ::ng-deep */
@@ -1,58 +0,0 @@
import { Component, inject, input, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ModalShell } from '../modal-shell/modal-shell';
import { StudentService } from '../../../shared/services/student.service';
import { StudentGoalItem } from '../../../shared/classes/student-goal';
@Component({
selector: 'app-edit-goal-modal',
imports: [FormsModule, ModalShell],
templateUrl: './edit-goal-modal.html',
styleUrl: './edit-goal-modal.scss',
})
export class EditGoalModal {
private readonly studentService = inject(StudentService);
readonly studentId = input.required<string>();
readonly goal = input.required<StudentGoalItem>();
readonly saved = output<void>();
readonly closed = output<void>();
protected readonly saving = signal(false);
protected readonly errorMessage = signal<string | null>(null);
protected category = '';
protected description = '';
protected baseline = '';
protected targetCompletionDate: string | null = null;
ngOnInit() {
const g = this.goal();
this.category = g.category;
this.description = g.description;
this.baseline = g.baseline;
this.targetCompletionDate = g.targetCompletionDate ? g.targetCompletionDate.substring(0, 10) : null;
}
async onSave() {
if (!this.category.trim() || !this.description.trim()) return;
this.saving.set(true);
this.errorMessage.set(null);
const result = await this.studentService.updateGoal(this.studentId(), this.goal().goalId, {
category: this.category,
description: this.description,
baseline: this.baseline,
targetCompletionDate: this.targetCompletionDate,
});
this.saving.set(false);
if (result.success) {
this.studentService.notifyDataChanged();
this.saved.emit();
} else {
this.errorMessage.set(result.message);
}
}
}
@@ -1,4 +1,4 @@
<app-modal-shell title="Add Goal" (closed)="cancelled.emit()"> <app-modal-shell [title]="modalTitle" (closed)="closed.emit()">
<div class="field"> <div class="field">
<label class="field-label">Category</label> <label class="field-label">Category</label>
<input class="field-input" type="text" [(ngModel)]="form.category" <input class="field-input" type="text" [(ngModel)]="form.category"
@@ -24,9 +24,9 @@
} }
<div class="modal-actions"> <div class="modal-actions">
<button class="btn-secondary" (click)="cancelled.emit()">Cancel</button> <button class="btn-secondary" (click)="closed.emit()">Cancel</button>
<button class="btn-primary" (click)="onSubmit()" [disabled]="isSubmitting() || !form.category.trim()"> <button class="btn-primary" (click)="onSubmit()" [disabled]="isSubmitting() || !form.category.trim()">
{{ isSubmitting() ? 'Saving...' : 'Add Goal' }} {{ isSubmitting() ? 'Saving...' : submitLabel }}
</button> </button>
</div> </div>
</app-modal-shell> </app-modal-shell>
@@ -0,0 +1,113 @@
import { Component, inject, input, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ModalShell } from '../modal-shell/modal-shell';
import { CreateGoalDto } from '../../../shared/classes/create-goal.dto';
import { StudentGoalItem } from '../../../shared/classes/student-goal';
import { StudentService } from '../../../shared/services/student.service';
@Component({
selector: 'app-goal-modal',
imports: [FormsModule, ModalShell],
templateUrl: './goal-modal.html',
styleUrl: './goal-modal.scss',
})
export class GoalModal {
private readonly studentService = inject(StudentService);
/** Required: the student this goal belongs to. */
readonly studentId = input.required<string>();
/** Optional: when provided the modal operates in edit mode. */
readonly goal = input<StudentGoalItem | null>(null);
/** Optional: used to pre-fill the target completion date in add mode. */
readonly nextIepDate = input<string | null>(null);
/** Emits the newly created goal (add mode). */
readonly goalCreated = output<StudentGoalItem>();
/** Emits when an existing goal has been saved (edit mode). */
readonly saved = output<void>();
/** Emits when the modal is dismissed without saving. */
readonly closed = output<void>();
protected readonly isSubmitting = signal(false);
protected readonly errorMessage = signal<string | null>(null);
protected form: CreateGoalDto = {
description: '',
category: '',
baseline: '',
goalParentId: null,
targetCompletionDate: null,
};
protected get isEditMode(): boolean {
return !!this.goal();
}
protected get modalTitle(): string {
return this.isEditMode ? 'Edit Goal' : 'Add Goal';
}
protected get submitLabel(): string {
return this.isEditMode ? 'Save' : 'Add Goal';
}
ngOnInit() {
const existing = this.goal();
if (existing) {
// Edit mode — populate form from the existing goal
this.form.category = existing.category;
this.form.description = existing.description;
this.form.baseline = existing.baseline;
this.form.goalParentId = existing.goalParentId;
this.form.targetCompletionDate = existing.targetCompletionDate
? existing.targetCompletionDate.substring(0, 10)
: null;
} else {
// Add mode — pre-fill target date from IEP if available
const iepDate = this.nextIepDate?.();
if (iepDate) {
this.form.targetCompletionDate = iepDate;
}
}
}
async onSubmit() {
if (!this.form.category.trim()) return;
this.errorMessage.set(null);
this.isSubmitting.set(true);
if (this.isEditMode) {
const result = await this.studentService.updateGoal(
this.studentId(),
this.goal()!.goalId,
{
category: this.form.category,
description: this.form.description,
baseline: this.form.baseline,
targetCompletionDate: this.form.targetCompletionDate,
},
);
this.isSubmitting.set(false);
if (result.success) {
this.studentService.notifyDataChanged();
this.saved.emit();
} else {
this.errorMessage.set(result.message);
}
} else {
const result = await this.studentService.createGoal(this.studentId(), this.form);
this.isSubmitting.set(false);
if (result.success) {
this.goalCreated.emit(result.payload!);
} else {
this.errorMessage.set(result.message);
}
}
}
}
@@ -4,13 +4,12 @@
</div> </div>
} @else { } @else {
<!-- Modals --> <!-- Modals -->
@if (showAddGoalModal()) { @if (showGoalModal()) {
<app-add-goal-modal [studentId]="studentId()!" [existingGoals]="goals()" [nextIepDate]="nextIepDate()" <app-goal-modal [studentId]="studentId()!"
(goalCreated)="onGoalCreated($event)" (cancelled)="showAddGoalModal.set(false)" /> [goal]="showGoalModal() === 'add' ? null : $any(showGoalModal())"
} [nextIepDate]="nextIepDate()"
@if (showEditGoalModal() && selectedGoal()) { (goalCreated)="onGoalCreated($event)" (saved)="onGoalSaved()"
<app-edit-goal-modal [studentId]="studentId()!" [goal]="selectedGoal()!" (saved)="onEditGoalSaved()" (closed)="showGoalModal.set(null)" />
(closed)="showEditGoalModal.set(false)" />
} }
@if (showEditBenchmarkModal()) { @if (showEditBenchmarkModal()) {
<app-edit-benchmark-modal [studentId]="studentId()!" [benchmark]="showEditBenchmarkModal()!" <app-edit-benchmark-modal [studentId]="studentId()!" [benchmark]="showEditBenchmarkModal()!"
@@ -5,16 +5,15 @@ import { StudentCardDto } from '../../../shared/classes/student-card.dto';
import { StudentGoalItem } from '../../../shared/classes/student-goal'; import { StudentGoalItem } from '../../../shared/classes/student-goal';
import { BenchmarkDto } from '../../../shared/classes/benchmark.dto'; import { BenchmarkDto } from '../../../shared/classes/benchmark.dto';
import { ProgressEventDto } from '../../../shared/classes/progress-event.dto'; import { ProgressEventDto } from '../../../shared/classes/progress-event.dto';
import { EditGoalModal } from '../edit-goal-modal/edit-goal-modal'; import { GoalModal } from '../goal-modal/goal-modal';
import { EditBenchmarkModal } from '../edit-benchmark-modal/edit-benchmark-modal'; import { EditBenchmarkModal } from '../edit-benchmark-modal/edit-benchmark-modal';
import { EditEventModal } from '../edit-event-modal/edit-event-modal'; import { EditEventModal } from '../edit-event-modal/edit-event-modal';
import { AddGoalModal } from '../add-goal-modal/add-goal-modal';
type TabView = 'benchmarks' | 'progress'; type TabView = 'benchmarks' | 'progress';
@Component({ @Component({
selector: 'app-workspace', selector: 'app-workspace',
imports: [EditGoalModal, EditBenchmarkModal, EditEventModal, AddGoalModal], imports: [GoalModal, EditBenchmarkModal, EditEventModal],
templateUrl: './workspace.html', templateUrl: './workspace.html',
styleUrl: './workspace.scss', styleUrl: './workspace.scss',
}) })
@@ -67,8 +66,7 @@ export class Workspace {
protected readonly activeTab = signal<TabView>('benchmarks'); protected readonly activeTab = signal<TabView>('benchmarks');
// Modal states // Modal states
protected readonly showAddGoalModal = signal(false); protected readonly showGoalModal = signal<StudentGoalItem | 'add' | null>(null);
protected readonly showEditGoalModal = signal(false);
protected readonly showEditBenchmarkModal = signal<BenchmarkDto | null>(null); protected readonly showEditBenchmarkModal = signal<BenchmarkDto | null>(null);
protected readonly showEditEventModal = signal<ProgressEventDto | null | 'new'>(null); protected readonly showEditEventModal = signal<ProgressEventDto | null | 'new'>(null);
@@ -113,20 +111,20 @@ export class Workspace {
// Modal handlers // Modal handlers
onEditGoal() { onEditGoal() {
this.showEditGoalModal.set(true); this.showGoalModal.set(this.selectedGoal()!);
} }
onEditGoalSaved() { onGoalSaved() {
this.showEditGoalModal.set(false); this.showGoalModal.set(null);
this.loadStudentData(this.studentId()!); this.loadStudentData(this.studentId()!);
} }
onAddGoal() { onAddGoal() {
this.showAddGoalModal.set(true); this.showGoalModal.set('add');
} }
onGoalCreated(goal: StudentGoalItem) { onGoalCreated(goal: StudentGoalItem) {
this.showAddGoalModal.set(false); this.showGoalModal.set(null);
this.studentService.notifyDataChanged(); this.studentService.notifyDataChanged();
this.loadStudentData(this.studentId()!).then(() => { this.loadStudentData(this.studentId()!).then(() => {
this.selectedGoalId.set(goal.goalId); this.selectedGoalId.set(goal.goalId);