mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 02:57:36 +00:00
Added Goals fields
This commit is contained in:
+12
@@ -42,6 +42,17 @@
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="targetCompletionDate">Target Completion Date</label>
|
||||
<input
|
||||
id="targetCompletionDate"
|
||||
type="date"
|
||||
[(ngModel)]="form.targetCompletionDate"
|
||||
name="targetCompletionDate"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Parent goal dropdown hidden — may not be needed
|
||||
@if (parentGoalOptions().length > 0) {
|
||||
<div class="field">
|
||||
<label for="goalParentId">Parent Goal <span class="optional">(optional)</span></label>
|
||||
@@ -53,6 +64,7 @@
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
-->
|
||||
|
||||
@if (errorMessage()) {
|
||||
<p class="error">{{ errorMessage() }}</p>
|
||||
|
||||
@@ -20,6 +20,7 @@ export class AddGoalModal {
|
||||
|
||||
readonly studentId = input.required<string>();
|
||||
readonly existingGoals = input.required<StudentGoalItem[]>();
|
||||
readonly nextIepDate = input<string | null>();
|
||||
readonly goalCreated = output<StudentGoalItem>();
|
||||
readonly cancelled = output<void>();
|
||||
|
||||
@@ -35,8 +36,19 @@ export class AddGoalModal {
|
||||
category: '',
|
||||
baseline: '',
|
||||
goalParentId: null,
|
||||
targetCompletionDate: null,
|
||||
};
|
||||
|
||||
// *****************************************************************
|
||||
// Pre-fills targetCompletionDate from the student's nextIepDate.
|
||||
// *****************************************************************
|
||||
ngOnInit() {
|
||||
const iepDate = this.nextIepDate?.();
|
||||
if (iepDate) {
|
||||
this.form.targetCompletionDate = iepDate;
|
||||
}
|
||||
}
|
||||
|
||||
// ************************** Properties ***************************
|
||||
|
||||
// ************************ Public Methods *************************
|
||||
|
||||
+4
-4
@@ -8,10 +8,6 @@
|
||||
<p class="error">{{ errorMessage() }}</p>
|
||||
}
|
||||
|
||||
@if (successMessage()) {
|
||||
<p class="success">{{ successMessage() }}</p>
|
||||
}
|
||||
|
||||
@if (loaded()) {
|
||||
<div class="detail-card">
|
||||
<div class="field">
|
||||
@@ -45,4 +41,8 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (successMessage()) {
|
||||
<p class="success">{{ successMessage() }}</p>
|
||||
}
|
||||
+66
-30
@@ -4,40 +4,76 @@
|
||||
<span class="spacer"></span>
|
||||
</div>
|
||||
|
||||
@if (loaded()) {
|
||||
<div class="detail-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">Goal: {{ category }}</span>
|
||||
@if (targetCompletionDate) {
|
||||
<span class="card-title">Target: {{ targetCompletionDate | date:'mediumDate' }}</span>
|
||||
} @else {
|
||||
<span class="card-title">No target date</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<div class="field">
|
||||
<label class="field-label" for="category">Category</label>
|
||||
<input id="category" class="field-input" type="text" [(ngModel)]="category" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="description">Description</label>
|
||||
<textarea id="description" class="field-input field-textarea" [(ngModel)]="description" rows="4"
|
||||
placeholder="Enter description..."></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="baseline">Baseline</label>
|
||||
<textarea id="baseline" class="field-input field-textarea" [(ngModel)]="baseline" rows="3"
|
||||
placeholder="Enter baseline..."></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="targetCompletionDate">Target Completion Date</label>
|
||||
<input id="targetCompletionDate" class="field-input" type="date" [(ngModel)]="targetCompletionDate" />
|
||||
</div>
|
||||
|
||||
@if (closeDate !== null) {
|
||||
<div class="close-section">
|
||||
<div class="field">
|
||||
<label class="field-label" for="closeDate">Close Date</label>
|
||||
<input id="closeDate" class="field-input" type="date" [(ngModel)]="closeDate" />
|
||||
</div>
|
||||
<div class="field field-row">
|
||||
<label class="field-label" for="achieved">Achieved</label>
|
||||
<input id="achieved" type="checkbox" [ngModel]="achieved ?? false" (ngModelChange)="achieved = $event" />
|
||||
</div>
|
||||
@if (achieved === false) {
|
||||
<div class="field">
|
||||
<label class="field-label" for="closeNotes">Close Notes <span class="required">*</span></label>
|
||||
<textarea id="closeNotes" class="field-input field-textarea" [(ngModel)]="closeNotes" rows="3"
|
||||
placeholder="Explain why the goal was not achieved..." required></textarea>
|
||||
</div>
|
||||
}
|
||||
</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>
|
||||
|
||||
<div class="card-footer">
|
||||
<button class="footer-btn btn-green" (click)="onProgressEvents()">Progress Events</button>
|
||||
<button class="footer-btn btn-blue" (click)="onBenchmarks()">Benchmarks</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (errorMessage()) {
|
||||
<p class="error">{{ errorMessage() }}</p>
|
||||
}
|
||||
|
||||
@if (successMessage()) {
|
||||
<p class="success">{{ successMessage() }}</p>
|
||||
}
|
||||
|
||||
@if (loaded()) {
|
||||
<div class="detail-card">
|
||||
<div class="field">
|
||||
<label class="field-label" for="category">Category</label>
|
||||
<input id="category" class="field-input" type="text" [(ngModel)]="category" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="description">Description</label>
|
||||
<textarea id="description" class="field-input field-textarea" [(ngModel)]="description" rows="4"
|
||||
placeholder="Enter description..."></textarea>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="baseline">Baseline</label>
|
||||
<textarea id="baseline" class="field-input field-textarea" [(ngModel)]="baseline" rows="3"
|
||||
placeholder="Enter baseline..."></textarea>
|
||||
</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 class="card-footer">
|
||||
<a class="detail-link" (click)="onProgressEvents()">Progress Events</a>
|
||||
<a class="detail-link" (click)="onBenchmarks()">Benchmarks</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
+70
-11
@@ -58,40 +58,77 @@
|
||||
.error {
|
||||
font-size: 0.875rem;
|
||||
color: #dc2626;
|
||||
margin: 0 0 1rem;
|
||||
margin: 1rem 0 0;
|
||||
}
|
||||
|
||||
.success {
|
||||
font-size: 0.875rem;
|
||||
color: #16a34a;
|
||||
margin: 0 0 1rem;
|
||||
margin: 1rem 0 0;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
max-width: 600px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.detail-link {
|
||||
font-size: 0.875rem;
|
||||
color: #4f46e5;
|
||||
.footer-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: transparent;
|
||||
border: 1px solid;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.detail-link:hover {
|
||||
color: #4338ca;
|
||||
.btn-green {
|
||||
color: #16a34a;
|
||||
border-color: #16a34a;
|
||||
}
|
||||
|
||||
.btn-green:hover {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.btn-blue {
|
||||
color: #2563eb;
|
||||
border-color: #2563eb;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn-blue:hover {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.field {
|
||||
@@ -113,6 +150,7 @@
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
font-size: 0.9375rem;
|
||||
outline: none;
|
||||
}
|
||||
@@ -146,4 +184,25 @@
|
||||
|
||||
.save-btn:hover {
|
||||
background: #4338ca;
|
||||
}
|
||||
|
||||
.close-section {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.field-row {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.field-row input[type="checkbox"] {
|
||||
width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #dc2626;
|
||||
}
|
||||
+44
-2
@@ -1,13 +1,14 @@
|
||||
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 { StudentGoalItem } from '../../../shared/classes/student-goal';
|
||||
|
||||
@Component({
|
||||
selector: 'app-goal-card-full',
|
||||
imports: [FormsModule],
|
||||
imports: [FormsModule, DatePipe],
|
||||
templateUrl: './goal-card-full.html',
|
||||
styleUrl: './goal-card-full.scss',
|
||||
})
|
||||
@@ -42,6 +43,10 @@ export class GoalCardFull implements OnDestroy {
|
||||
protected description = '';
|
||||
protected category = '';
|
||||
protected baseline = '';
|
||||
protected targetCompletionDate: string | null = null;
|
||||
protected closeDate: string | null = null;
|
||||
protected achieved: boolean | null = null;
|
||||
protected closeNotes: string | null = null;
|
||||
|
||||
// Read-only metadata
|
||||
protected progressEventCount = 0;
|
||||
@@ -51,6 +56,10 @@ export class GoalCardFull implements OnDestroy {
|
||||
private savedDescription = '';
|
||||
private savedCategory = '';
|
||||
private savedBaseline = '';
|
||||
private savedTargetCompletionDate: string | null = null;
|
||||
private savedCloseDate: string | null = null;
|
||||
private savedAchieved: boolean | null = null;
|
||||
private savedCloseNotes: string | null = null;
|
||||
|
||||
// ************************** Properties ***************************
|
||||
|
||||
@@ -60,7 +69,11 @@ export class GoalCardFull implements OnDestroy {
|
||||
hasChanges(): boolean {
|
||||
return this.description !== this.savedDescription
|
||||
|| this.category !== this.savedCategory
|
||||
|| this.baseline !== this.savedBaseline;
|
||||
|| this.baseline !== this.savedBaseline
|
||||
|| this.targetCompletionDate !== this.savedTargetCompletionDate
|
||||
|| this.closeDate !== this.savedCloseDate
|
||||
|| this.achieved !== this.savedAchieved
|
||||
|| this.closeNotes !== this.savedCloseNotes;
|
||||
}
|
||||
|
||||
// ************************ Public Methods *************************
|
||||
@@ -79,6 +92,10 @@ export class GoalCardFull implements OnDestroy {
|
||||
description: this.description,
|
||||
category: this.category,
|
||||
baseline: this.baseline,
|
||||
targetCompletionDate: this.targetCompletionDate,
|
||||
closeDate: this.closeDate,
|
||||
achieved: this.achieved,
|
||||
closeNotes: this.closeNotes,
|
||||
});
|
||||
|
||||
this.saving.set(false);
|
||||
@@ -87,7 +104,12 @@ export class GoalCardFull implements OnDestroy {
|
||||
this.savedDescription = this.description;
|
||||
this.savedCategory = this.category;
|
||||
this.savedBaseline = this.baseline;
|
||||
this.savedTargetCompletionDate = this.targetCompletionDate;
|
||||
this.savedCloseDate = this.closeDate;
|
||||
this.savedAchieved = this.achieved;
|
||||
this.savedCloseNotes = this.closeNotes;
|
||||
this.successMessage.set('Changes saved.');
|
||||
this.studentService.notifyDataChanged();
|
||||
} else {
|
||||
this.errorMessage.set(result.message);
|
||||
}
|
||||
@@ -100,6 +122,10 @@ export class GoalCardFull implements OnDestroy {
|
||||
this.description = this.savedDescription;
|
||||
this.category = this.savedCategory;
|
||||
this.baseline = this.savedBaseline;
|
||||
this.targetCompletionDate = this.savedTargetCompletionDate;
|
||||
this.closeDate = this.savedCloseDate;
|
||||
this.achieved = this.savedAchieved;
|
||||
this.closeNotes = this.savedCloseNotes;
|
||||
this.errorMessage.set(null);
|
||||
this.successMessage.set(null);
|
||||
}
|
||||
@@ -122,6 +148,14 @@ export class GoalCardFull implements OnDestroy {
|
||||
|
||||
// ********************** Support Procedures ***********************
|
||||
|
||||
// *****************************************************************
|
||||
// Normalizes an API date string to YYYY-MM-DD for <input type="date">.
|
||||
// *****************************************************************
|
||||
private toDateInput(value: string | null): string | null {
|
||||
if (!value) return null;
|
||||
return value.substring(0, 10);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Loads the goal by finding it in the student's goal list.
|
||||
// *****************************************************************
|
||||
@@ -142,12 +176,20 @@ export class GoalCardFull implements OnDestroy {
|
||||
this.description = goal.description;
|
||||
this.category = goal.category;
|
||||
this.baseline = goal.baseline;
|
||||
this.targetCompletionDate = this.toDateInput(goal.targetCompletionDate);
|
||||
this.closeDate = this.toDateInput(goal.closeDate);
|
||||
this.achieved = goal.achieved;
|
||||
this.closeNotes = goal.closeNotes;
|
||||
this.progressEventCount = goal.progressEventCount;
|
||||
this.benchmarkCount = goal.benchmarkCount;
|
||||
|
||||
this.savedDescription = goal.description;
|
||||
this.savedCategory = goal.category;
|
||||
this.savedBaseline = goal.baseline;
|
||||
this.savedTargetCompletionDate = this.toDateInput(goal.targetCompletionDate);
|
||||
this.savedCloseDate = this.toDateInput(goal.closeDate);
|
||||
this.savedAchieved = goal.achieved;
|
||||
this.savedCloseNotes = goal.closeNotes;
|
||||
this.loaded.set(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,14 +1,24 @@
|
||||
<div class="card" (click)="onCardClick()">
|
||||
<div class="card-header">
|
||||
<span class="category-badge">{{ goal().category }}</span>
|
||||
<span class="event-count">{{ goal().progressEventCount }} events</span>
|
||||
<span class="card-title">Goal: {{ goal().category }}</span>
|
||||
@if (goal().closeDate !== null) {
|
||||
<span class="closed-badge" [class.achieved]="goal().achieved === true">
|
||||
{{ goal().achieved === true ? 'Closed ✓' : 'Closed ✗' }}
|
||||
</span>
|
||||
}
|
||||
@if (goal().targetCompletionDate) {
|
||||
<span class="event-count">Target: {{ goal().targetCompletionDate | date:'mediumDate' }}</span>
|
||||
} @else {
|
||||
<span class="event-count">No target date</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<h3 class="title">{{ goal().category }}</h3>
|
||||
<p class="description">{{ goal().description }}</p>
|
||||
<div class="card-body">
|
||||
<p class="description">{{ goal().description }}</p>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<a class="footer-link" (click)="$event.stopPropagation(); onBenchmarksClick()">Benchmarks</a>
|
||||
<a class="footer-link" (click)="$event.stopPropagation(); onProgressEventsClick()">Progress Events</a>
|
||||
<button class="footer-btn btn-green" (click)="$event.stopPropagation(); onProgressEventsClick()">Progress Events</button>
|
||||
<button class="footer-btn btn-blue" (click)="$event.stopPropagation(); onBenchmarksClick()">Benchmarks</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6,64 +6,37 @@
|
||||
.card {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||
padding: 1.25rem 1.5rem;
|
||||
border: 1px solid #ddd;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.625rem;
|
||||
cursor: pointer;
|
||||
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
margin: 0 -1.5rem -1rem;
|
||||
padding: 0.5rem 1.5rem 0.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.footer-link {
|
||||
font-size: 0.875rem;
|
||||
color: #4f46e5;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer-link:hover {
|
||||
color: #4338ca;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.category-badge {
|
||||
padding: 0.2rem 0.6rem;
|
||||
background: #eef2ff;
|
||||
color: #4f46e5;
|
||||
border-radius: 999px;
|
||||
font-size: 0.75rem;
|
||||
.card-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.event-count {
|
||||
font-size: 0.8125rem;
|
||||
color: #888;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #111;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.card-body {
|
||||
padding: 1.25rem 1.5rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
@@ -72,7 +45,61 @@
|
||||
color: #555;
|
||||
line-height: 1.5;
|
||||
display: -webkit-box;
|
||||
line-clamp: 2;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.footer-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: transparent;
|
||||
border: 1px solid;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-green {
|
||||
color: #16a34a;
|
||||
border-color: #16a34a;
|
||||
}
|
||||
|
||||
.btn-green:hover {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.btn-blue {
|
||||
color: #2563eb;
|
||||
border-color: #2563eb;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.btn-blue:hover {
|
||||
background: #eff6ff;
|
||||
}
|
||||
|
||||
.closed-badge {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
.closed-badge.achieved {
|
||||
background: #f0fdf4;
|
||||
color: #16a34a;
|
||||
border-color: #bbf7d0;
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Component, inject, input } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { StudentGoalItem } from '../../../shared/classes/student-goal';
|
||||
|
||||
@Component({
|
||||
selector: 'app-goal-card',
|
||||
imports: [],
|
||||
imports: [DatePipe],
|
||||
templateUrl: './goal-card.html',
|
||||
styleUrl: './goal-card.scss',
|
||||
})
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
}
|
||||
|
||||
@if (showAddModal()) {
|
||||
<app-add-goal-modal [studentId]="studentId" [existingGoals]="goals()" (goalCreated)="onGoalCreated($event)"
|
||||
<app-add-goal-modal [studentId]="studentId" [existingGoals]="goals()" [nextIepDate]="nextIepDate()" (goalCreated)="onGoalCreated($event)"
|
||||
(cancelled)="onModalCancelled()" />
|
||||
}
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
font-size: 1.125rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin: 0 0 0.5rem;
|
||||
margin: 0 0 1.25rem;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
@@ -78,6 +78,7 @@
|
||||
.card-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-content: start;
|
||||
gap: 1rem;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
|
||||
@@ -32,6 +32,7 @@ export class GoalList implements OnDestroy {
|
||||
|
||||
protected studentId!: string;
|
||||
protected readonly studentIdentifier = signal<string | null>(null);
|
||||
protected readonly nextIepDate = signal<string | null>(null);
|
||||
protected readonly goals = signal<StudentGoalItem[]>([]);
|
||||
protected readonly showAddModal = signal(false);
|
||||
protected readonly errorMessage = signal<string | null>(null);
|
||||
@@ -71,6 +72,13 @@ export class GoalList implements OnDestroy {
|
||||
// Loads goals for the student from the service.
|
||||
// *****************************************************************
|
||||
private loadGoals() {
|
||||
this.studentService.getStudentById(this.studentId).then(studentResult => {
|
||||
if (studentResult.success && studentResult.payload) {
|
||||
const iep = studentResult.payload.nextIepDate;
|
||||
this.nextIepDate.set(iep ? String(iep).substring(0, 10) : null);
|
||||
}
|
||||
});
|
||||
|
||||
this.studentService.getGoalsForStudent(this.studentId).then(data => {
|
||||
if (!data.success) {
|
||||
this.errorMessage.set(data.message);
|
||||
|
||||
+57
-1
@@ -1 +1,57 @@
|
||||
<p>progress-edit works!</p>
|
||||
<div class="toolbar">
|
||||
<button class="toolbar-btn back-btn" (click)="onBack()">↑ Progress Events</button>
|
||||
<span class="toolbar-title">{{ isNew() ? 'New Progress Event' : 'Edit Progress Event' }}</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="content">Notes</label>
|
||||
<textarea id="content" class="field-input field-textarea" [(ngModel)]="content" rows="5"
|
||||
placeholder="Enter progress event notes..."></textarea>
|
||||
</div>
|
||||
|
||||
@if (benchmarkItems().length > 0) {
|
||||
<div class="field">
|
||||
<span class="field-label">Associated Benchmarks</span>
|
||||
<div class="benchmark-checks">
|
||||
@for (bm of benchmarkItems(); track bm.benchmarkId) {
|
||||
<label class="check-item">
|
||||
<input type="checkbox" [checked]="bm.checked" (change)="onToggleBenchmark(bm.benchmarkId)" />
|
||||
{{ bm.label }}
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!isNew()) {
|
||||
<div class="metadata">
|
||||
@if (createdByName) {
|
||||
<span class="meta-item">Created by: {{ createdByName }}</span>
|
||||
}
|
||||
@if (createdAt) {
|
||||
<span class="meta-item">Created: {{ createdAt | 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>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
gap: 0.75rem;
|
||||
height: 40px;
|
||||
padding-right: 0.5rem;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #ddd;
|
||||
margin-bottom: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolbar-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: transparent;
|
||||
color: #4f46e5;
|
||||
border: 1px solid #4f46e5;
|
||||
border-radius: 6px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar-btn:hover {
|
||||
background: #eef2ff;
|
||||
}
|
||||
|
||||
.toolbar-btn:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.toolbar-title {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.error {
|
||||
font-size: 0.875rem;
|
||||
color: #dc2626;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.success {
|
||||
font-size: 0.875rem;
|
||||
color: #16a34a;
|
||||
margin: 0 0 1rem;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.field-label {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #666;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.025em;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.field-input {
|
||||
padding: 0.375rem 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9375rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.field-input:focus {
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
.field-textarea {
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
min-height: 5rem;
|
||||
}
|
||||
|
||||
.benchmark-checks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.check-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
color: #333;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.check-item input[type="checkbox"] {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
accent-color: #4f46e5;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
font-size: 0.8125rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.actions .toolbar-btn {
|
||||
min-width: 6rem;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
background: #4f46e5;
|
||||
color: #fff;
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
.save-btn:hover {
|
||||
background: #4338ca;
|
||||
}
|
||||
|
||||
+220
-3
@@ -1,11 +1,228 @@
|
||||
import { Component } from '@angular/core';
|
||||
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';
|
||||
|
||||
interface BenchmarkCheckItem {
|
||||
benchmarkId: string;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-progress-edit',
|
||||
imports: [],
|
||||
imports: [FormsModule, DatePipe],
|
||||
templateUrl: './progress-edit.html',
|
||||
styleUrl: './progress-edit.scss',
|
||||
})
|
||||
export class ProgressEdit {
|
||||
export class ProgressEdit implements OnDestroy {
|
||||
|
||||
// ************************** Constructor **************************
|
||||
|
||||
constructor() {
|
||||
this.paramSub = this.route.paramMap.subscribe(params => {
|
||||
this.studentId = params.get('studentId')!;
|
||||
this.goalId = params.get('goalId')!;
|
||||
this.progressEventId = params.get('progressEventId') ?? null;
|
||||
this.loadData();
|
||||
});
|
||||
}
|
||||
|
||||
// ************************** 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 progressEventId: 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 content = '';
|
||||
private savedContent = '';
|
||||
|
||||
// Benchmark checkboxes
|
||||
protected benchmarkItems = signal<BenchmarkCheckItem[]>([]);
|
||||
private savedBenchmarkSelections: Set<string> = new Set();
|
||||
|
||||
// Read-only metadata
|
||||
protected goalCategory = '';
|
||||
protected createdByName = '';
|
||||
protected createdAt: Date | null = null;
|
||||
|
||||
// ************************** Properties ***************************
|
||||
|
||||
// *****************************************************************
|
||||
// Returns true if the form has unsaved changes.
|
||||
// *****************************************************************
|
||||
hasChanges(): boolean {
|
||||
if (this.content !== this.savedContent) return true;
|
||||
const current = new Set(this.benchmarkItems().filter(b => b.checked).map(b => b.benchmarkId));
|
||||
if (current.size !== this.savedBenchmarkSelections.size) return true;
|
||||
for (const id of current) {
|
||||
if (!this.savedBenchmarkSelections.has(id)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ************************ Public Methods *************************
|
||||
|
||||
// ************************ Event Handlers *************************
|
||||
|
||||
// *****************************************************************
|
||||
// Saves the progress event (create or update) and any benchmark
|
||||
// associations based on the checked checkboxes.
|
||||
// *****************************************************************
|
||||
async onSave() {
|
||||
this.saving.set(true);
|
||||
this.errorMessage.set(null);
|
||||
this.successMessage.set(null);
|
||||
|
||||
const checkedIds = this.benchmarkItems()
|
||||
.filter(b => b.checked)
|
||||
.map(b => b.benchmarkId);
|
||||
|
||||
if (this.isNew()) {
|
||||
const result = await this.studentService.addProgressEvent(
|
||||
this.studentId, this.goalId, this.content.trim(),
|
||||
checkedIds.length > 0 ? checkedIds : undefined
|
||||
);
|
||||
this.saving.set(false);
|
||||
if (result.success) {
|
||||
this.successMessage.set('Progress event created.');
|
||||
this.savedContent = this.content;
|
||||
this.savedBenchmarkSelections = new Set(checkedIds);
|
||||
this.studentService.notifyDataChanged();
|
||||
if (result.payload?.progressEventId) {
|
||||
this.router.navigate(['/students', this.studentId, 'goals', this.goalId, 'progress', result.payload.progressEventId]);
|
||||
}
|
||||
} else {
|
||||
this.errorMessage.set(result.message);
|
||||
}
|
||||
} else {
|
||||
const result = await this.studentService.updateProgressEvent(
|
||||
this.studentId, this.progressEventId!, this.content.trim(), checkedIds
|
||||
);
|
||||
this.saving.set(false);
|
||||
if (result.success) {
|
||||
this.savedContent = this.content;
|
||||
this.savedBenchmarkSelections = new Set(checkedIds);
|
||||
this.successMessage.set('Changes saved.');
|
||||
} else {
|
||||
this.errorMessage.set(result.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Reverts the form to the last-saved state.
|
||||
// *****************************************************************
|
||||
onCancel() {
|
||||
this.content = this.savedContent;
|
||||
this.benchmarkItems.update(items =>
|
||||
items.map(b => ({ ...b, checked: this.savedBenchmarkSelections.has(b.benchmarkId) }))
|
||||
);
|
||||
this.errorMessage.set(null);
|
||||
this.successMessage.set(null);
|
||||
}
|
||||
|
||||
onBack() {
|
||||
this.router.navigate(['/students', this.studentId, 'goals', this.goalId, 'progress']);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Toggles a benchmark checkbox.
|
||||
// *****************************************************************
|
||||
onToggleBenchmark(benchmarkId: string) {
|
||||
this.benchmarkItems.update(items =>
|
||||
items.map(b => b.benchmarkId === benchmarkId ? { ...b, checked: !b.checked } : b)
|
||||
);
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.paramSub.unsubscribe();
|
||||
}
|
||||
|
||||
// ********************** Support Procedures ***********************
|
||||
|
||||
// *****************************************************************
|
||||
// Loads all data needed for the form: goal category, benchmarks,
|
||||
// and (for edit mode) the existing event content + associations.
|
||||
// *****************************************************************
|
||||
private async loadData() {
|
||||
this.loaded.set(false);
|
||||
|
||||
// Load goal category
|
||||
const goalsResult = await this.studentService.getGoalsForStudent(this.studentId);
|
||||
if (goalsResult.success && goalsResult.payload) {
|
||||
const goal = goalsResult.payload.goals.find(g => g.goalId === this.goalId);
|
||||
this.goalCategory = goal?.category ?? '';
|
||||
}
|
||||
|
||||
// Load benchmarks for this goal
|
||||
const bmResult = await this.studentService.getBenchmarksForStudent(this.studentId);
|
||||
const goalBenchmarks = (bmResult.success && bmResult.payload)
|
||||
? bmResult.payload.benchmarks.filter(b => b.goalId === this.goalId)
|
||||
: [];
|
||||
|
||||
if (!this.progressEventId) {
|
||||
// New event mode
|
||||
this.isNew.set(true);
|
||||
this.content = '';
|
||||
this.savedContent = '';
|
||||
this.savedBenchmarkSelections = new Set();
|
||||
this.benchmarkItems.set(goalBenchmarks.map(b => ({
|
||||
benchmarkId: b.benchmarkId,
|
||||
label: b.shortName || b.benchmark,
|
||||
checked: false,
|
||||
})));
|
||||
this.loaded.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Edit mode — load existing event
|
||||
this.isNew.set(false);
|
||||
const eventsResult = await this.studentService.getProgressEventsForGoal(this.goalId);
|
||||
if (!eventsResult.success || !eventsResult.payload) {
|
||||
this.errorMessage.set(eventsResult.message);
|
||||
this.loaded.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const event = eventsResult.payload.find(e => e.progressEventId === this.progressEventId);
|
||||
if (!event) {
|
||||
this.errorMessage.set('Progress event not found.');
|
||||
this.loaded.set(true);
|
||||
return;
|
||||
}
|
||||
|
||||
this.content = event.content;
|
||||
this.savedContent = event.content;
|
||||
this.createdByName = event.createdByName;
|
||||
this.createdAt = event.createdAt;
|
||||
|
||||
// Load existing benchmark associations
|
||||
const assocResult = await this.studentService.getProgressEventBenchmarks(this.progressEventId!);
|
||||
const associatedIds = new Set(assocResult.success && assocResult.payload ? assocResult.payload : []);
|
||||
this.savedBenchmarkSelections = new Set(associatedIds);
|
||||
|
||||
this.benchmarkItems.set(goalBenchmarks.map(b => ({
|
||||
benchmarkId: b.benchmarkId,
|
||||
label: b.shortName || b.benchmark,
|
||||
checked: associatedIds.has(b.benchmarkId),
|
||||
})));
|
||||
|
||||
this.loaded.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<div class="card">
|
||||
<p class="content">{{ event().content }}</p>
|
||||
<div class="action-icons">
|
||||
<button class="icon-btn" title="Edit">✎</button>
|
||||
<button class="edit-btn" (click)="onEdit()">Edit</button>
|
||||
<!-- <button class="icon-btn" title="Delete">🗑</button> -->
|
||||
</div>
|
||||
<span class="author">{{ event().createdByName }}</span>
|
||||
|
||||
+12
-10
@@ -47,15 +47,17 @@
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
// .icon-btn {
|
||||
// background: none;
|
||||
// border: none;
|
||||
// cursor: pointer;
|
||||
// font-size: 1rem;
|
||||
// color: #888;
|
||||
// padding: 0.125rem;
|
||||
// }
|
||||
|
||||
.icon-btn:hover {
|
||||
.edit-btn {
|
||||
padding: 0.25rem 0.625rem;
|
||||
background: transparent;
|
||||
color: #4f46e5;
|
||||
border: 1px solid #4f46e5;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-btn:hover {
|
||||
background: #eef2ff;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, input } from '@angular/core';
|
||||
import { Component, inject, input } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ProgressEventDto } from '../../../shared/classes/progress-event.dto';
|
||||
|
||||
@Component({
|
||||
@@ -14,6 +15,8 @@ export class ProgressItem {
|
||||
|
||||
// ************************** Declarations *************************
|
||||
|
||||
private readonly router = inject(Router);
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
readonly event = input.required<ProgressEventDto>();
|
||||
|
||||
// ************************** Properties ***************************
|
||||
@@ -22,5 +25,9 @@ export class ProgressItem {
|
||||
|
||||
// ************************ Event Handlers *************************
|
||||
|
||||
onEdit() {
|
||||
this.router.navigate([this.event().progressEventId], { relativeTo: this.route });
|
||||
}
|
||||
|
||||
// ********************** Support Procedures ***********************
|
||||
}
|
||||
|
||||
@@ -67,8 +67,7 @@ export class ProgressList implements OnDestroy {
|
||||
// ************************ Event Handlers *************************
|
||||
|
||||
onAddProgressEvent() {
|
||||
this.showAddModal.set(true);
|
||||
// TODO: Wire up add-progress-event modal component
|
||||
this.router.navigate(['/students', this.studentId, 'goals', this.goalId, 'progress', 'new']);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
|
||||
+2
@@ -2,6 +2,8 @@
|
||||
<div class="node-row" [style.padding-left]="indent()">
|
||||
@if (hasToggle(node)) {
|
||||
<span class="toggle-indicator" (click)="onToggle(node, $event)">{{ node.expanded ? '−' : '+' }}</span>
|
||||
} @else {
|
||||
<span class="toggle-placeholder"></span>
|
||||
}
|
||||
@if (node.routerLink) {
|
||||
<a class="node-label" [routerLink]="node.routerLink" routerLinkActive="active"
|
||||
|
||||
+5
@@ -34,6 +34,11 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toggle-placeholder {
|
||||
width: calc(1rem + 2px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.node-label {
|
||||
flex: 1;
|
||||
font-size: 0.8125rem;
|
||||
|
||||
+19
-13
@@ -12,24 +12,30 @@
|
||||
|
||||
@if (loaded()) {
|
||||
<div class="detail-card">
|
||||
<div class="field">
|
||||
<label class="field-label" for="identifier">Name</label>
|
||||
<input id="identifier" class="field-input" type="text" [(ngModel)]="identifier" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="nextIepDate">Next IEP Date</label>
|
||||
<input id="nextIepDate" class="field-input" type="date" [(ngModel)]="nextIepDate" />
|
||||
<div class="detail-card-header">
|
||||
<span class="detail-card-title">Student</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 class="detail-card-body">
|
||||
<div class="field">
|
||||
<label class="field-label" for="identifier">Name</label>
|
||||
<input id="identifier" class="field-input" type="text" [(ngModel)]="identifier" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="field-label" for="nextIepDate">Next IEP Date</label>
|
||||
<input id="nextIepDate" class="field-input" type="date" [(ngModel)]="nextIepDate" />
|
||||
</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>
|
||||
|
||||
<div class="card-footer">
|
||||
<a class="detail-link" (click)="onGoals()">Goals</a>
|
||||
<button class="footer-btn btn-dark-green" (click)="onGoals()">Goals</button>
|
||||
<span class="spacer"></span>
|
||||
@if (successMessage()) {
|
||||
<span class="success-label" [class.fade-out]="fading()">{{ successMessage() }}</span>
|
||||
|
||||
+34
-9
@@ -67,8 +67,26 @@
|
||||
background: #fff;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
max-width: 480px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.detail-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.625rem 1.25rem;
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.detail-card-title {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.detail-card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
@@ -106,20 +124,27 @@
|
||||
.card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 1rem -1.5rem -1rem;
|
||||
padding: 0.5rem 1.5rem 0.5rem;
|
||||
padding: 0.5rem 1.5rem;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.detail-link {
|
||||
font-size: 0.875rem;
|
||||
color: #4f46e5;
|
||||
.footer-btn {
|
||||
padding: 0.375rem 0.75rem;
|
||||
background: transparent;
|
||||
border: 1px solid;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.detail-link:hover {
|
||||
color: #4338ca;
|
||||
.btn-dark-green {
|
||||
color: #15803d;
|
||||
border-color: #15803d;
|
||||
}
|
||||
|
||||
.btn-dark-green:hover {
|
||||
background: #f0fdf4;
|
||||
}
|
||||
|
||||
.success-label {
|
||||
|
||||
@@ -15,5 +15,6 @@
|
||||
<div class="stats">
|
||||
<span>Goals: {{ student().goalCount }}</span>
|
||||
<span>Events: {{ student().progressEventCount }}</span>
|
||||
<span>Benchmarks: {{ student().benchmarkCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7,6 +7,7 @@ import { GoalCardFull } from './components/goal-card-full/goal-card-full';
|
||||
import { ProgressList } from './components/progress-list/progress-list';
|
||||
import { BenchmarkList } from './components/benchmark-list/benchmark-list';
|
||||
import { BenchmarkCardFull } from './components/benchmark-card-full/benchmark-card-full';
|
||||
import { ProgressEdit } from './components/progress-edit/progress-edit';
|
||||
|
||||
export default [
|
||||
{
|
||||
@@ -19,6 +20,8 @@ export default [
|
||||
{ path: 'students/:studentId/goals', component: GoalList },
|
||||
{ path: 'students/:studentId/goals/:goalId', component: GoalCardFull },
|
||||
{ path: 'students/:studentId/goals/:goalId/progress', component: ProgressList },
|
||||
{ path: 'students/:studentId/goals/:goalId/progress/new', component: ProgressEdit },
|
||||
{ path: 'students/:studentId/goals/:goalId/progress/:progressEventId', component: ProgressEdit },
|
||||
{ path: 'students/:studentId/goals/:goalId/benchmarks', component: BenchmarkList },
|
||||
{ path: 'students/:studentId/goals/:goalId/benchmarks/new', component: BenchmarkCardFull },
|
||||
{ path: 'students/:studentId/goals/:goalId/benchmarks/:benchmarkId', component: BenchmarkCardFull },
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
<div class="tile" [class.checked]="checked()" (click)="onTap()">
|
||||
<span class="check-indicator">{{ checked() ? '✓' : '' }}</span>
|
||||
<span class="tile-label">{{ label() }}</span>
|
||||
</div>
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
.tile {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.625rem 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.tile:active {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.tile.checked {
|
||||
border-color: #4f46e5;
|
||||
background: #eef2ff;
|
||||
}
|
||||
|
||||
.check-indicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1.375rem;
|
||||
height: 1.375rem;
|
||||
border: 2px solid #ccc;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tile.checked .check-indicator {
|
||||
background: #4f46e5;
|
||||
border-color: #4f46e5;
|
||||
}
|
||||
|
||||
.tile-label {
|
||||
font-size: 0.9375rem;
|
||||
color: #333;
|
||||
line-height: 1.3;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ToggleBenchmark } from './toggle-benchmark';
|
||||
|
||||
describe('ToggleBenchmark', () => {
|
||||
let component: ToggleBenchmark;
|
||||
let fixture: ComponentFixture<ToggleBenchmark>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ToggleBenchmark]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ToggleBenchmark);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-toggle-benchmark',
|
||||
imports: [],
|
||||
templateUrl: './toggle-benchmark.html',
|
||||
styleUrl: './toggle-benchmark.scss',
|
||||
})
|
||||
export class ToggleBenchmark {
|
||||
|
||||
// ************************** Declarations *************************
|
||||
|
||||
readonly label = input.required<string>();
|
||||
readonly checked = input.required<boolean>();
|
||||
readonly toggled = output<void>();
|
||||
|
||||
// ************************ Event Handlers *************************
|
||||
|
||||
onTap() {
|
||||
this.toggled.emit();
|
||||
}
|
||||
}
|
||||
+12
-3
@@ -15,11 +15,20 @@
|
||||
<div class="form-card">
|
||||
<label class="field-label">Progress event notes</label>
|
||||
<textarea class="notes-input" placeholder="Type your message here." rows="5" [(ngModel)]="notes"></textarea>
|
||||
|
||||
<label class="field-label">Voice note</label>
|
||||
<button class="voice-btn">🎙 Record voice note</button>
|
||||
</div>
|
||||
|
||||
@if (benchmarkItems().length > 0) {
|
||||
<label class="field-label">Associated Benchmarks</label>
|
||||
<div class="benchmark-scroll">
|
||||
@for (bm of benchmarkItems(); track bm.benchmarkId) {
|
||||
<app-toggle-benchmark
|
||||
[label]="bm.label"
|
||||
[checked]="bm.checked"
|
||||
(toggled)="onToggleBenchmark(bm.benchmarkId)" />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Save button -->
|
||||
<button class="save-btn" [disabled]="!canSave()" (click)="onSave()">
|
||||
{{ saving() ? 'Saving...' : 'Save' }}
|
||||
|
||||
+11
@@ -129,4 +129,15 @@
|
||||
color: #dc2626;
|
||||
border-radius: 8px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Scrollable benchmark region */
|
||||
.benchmark-scroll {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
+45
-5
@@ -3,12 +3,19 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { HttpErrorResponse } from '@angular/common/http';
|
||||
import { describeHttpError } from '../../../shared/classes/http-errors';
|
||||
import { DummyStudentService } from '../../../shared/services/dummy-student.service';
|
||||
import { StudentService } from '../../../shared/services/student.service';
|
||||
|
||||
interface BenchmarkCheckItem {
|
||||
benchmarkId: string;
|
||||
label: string;
|
||||
checked: boolean;
|
||||
}
|
||||
|
||||
import { ToggleBenchmark } from '../../components/toggle-benchmark/toggle-benchmark';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-progress-event',
|
||||
imports: [FormsModule],
|
||||
imports: [FormsModule, ToggleBenchmark],
|
||||
templateUrl: './add-progress-event.html',
|
||||
styleUrl: './add-progress-event.scss',
|
||||
})
|
||||
@@ -21,6 +28,7 @@ export class AddProgressEvent {
|
||||
this.studentIdentifier.set(this.route.snapshot.queryParamMap.get('studentIdentifier') ?? '');
|
||||
this.studentId = this.route.snapshot.paramMap.get('studentId') ?? '';
|
||||
this.goalId = this.route.snapshot.paramMap.get('goalId') ?? '';
|
||||
this.loadBenchmarks();
|
||||
}
|
||||
|
||||
// ************************** Declarations *************************
|
||||
@@ -37,6 +45,7 @@ export class AddProgressEvent {
|
||||
protected readonly notes = signal('');
|
||||
protected readonly error = signal<string | null>(null);
|
||||
protected readonly saving = signal(false);
|
||||
protected readonly benchmarkItems = signal<BenchmarkCheckItem[]>([]);
|
||||
|
||||
// ************************** Properties ***************************
|
||||
|
||||
@@ -59,15 +68,31 @@ export class AddProgressEvent {
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Saves the progress event. On success, returns to the goal list.
|
||||
// On failure, displays the error message from the API.
|
||||
// Toggles a benchmark checkbox.
|
||||
// *****************************************************************
|
||||
onToggleBenchmark(benchmarkId: string) {
|
||||
this.benchmarkItems.update(items =>
|
||||
items.map(b => b.benchmarkId === benchmarkId ? { ...b, checked: !b.checked } : b)
|
||||
);
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Saves the progress event with optional benchmark associations.
|
||||
// On success, returns to the goal list.
|
||||
// *****************************************************************
|
||||
async onSave() {
|
||||
this.error.set(null);
|
||||
this.saving.set(true);
|
||||
|
||||
const checkedIds = this.benchmarkItems()
|
||||
.filter(b => b.checked)
|
||||
.map(b => b.benchmarkId);
|
||||
|
||||
try {
|
||||
const result = await this.studentService.addProgressEvent(this.studentId, this.goalId, this.notes().trim());
|
||||
const result = await this.studentService.addProgressEvent(
|
||||
this.studentId, this.goalId, this.notes().trim(),
|
||||
checkedIds.length > 0 ? checkedIds : undefined
|
||||
);
|
||||
this.saving.set(false);
|
||||
if (result.success) {
|
||||
this.router.navigate(['students', this.studentId, 'goals']);
|
||||
@@ -81,4 +106,19 @@ export class AddProgressEvent {
|
||||
}
|
||||
|
||||
// ********************** Support Procedures ***********************
|
||||
|
||||
// *****************************************************************
|
||||
// Loads benchmarks for the current goal to populate checkboxes.
|
||||
// *****************************************************************
|
||||
private async loadBenchmarks() {
|
||||
const result = await this.studentService.getBenchmarksForStudent(this.studentId);
|
||||
if (result.success && result.payload) {
|
||||
const goalBenchmarks = result.payload.benchmarks.filter(b => b.goalId === this.goalId);
|
||||
this.benchmarkItems.set(goalBenchmarks.map(b => ({
|
||||
benchmarkId: b.benchmarkId,
|
||||
label: b.shortName || b.benchmark,
|
||||
checked: false,
|
||||
})));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,5 @@ export interface CreateGoalDto {
|
||||
category: string;
|
||||
baseline: string;
|
||||
goalParentId: string | null;
|
||||
targetCompletionDate: string | null;
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ export interface StudentCardDto {
|
||||
lastEntryDate: Date | null;
|
||||
goalCount: number;
|
||||
progressEventCount: number;
|
||||
benchmarkCount: number;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,10 @@ export interface StudentGoalItem {
|
||||
description: string; // goal.description — text
|
||||
category: string; // goal.category — varchar(100)
|
||||
baseline: string; // goal.baseline — text
|
||||
targetCompletionDate: string | null; // goal.target_completion_date — date
|
||||
closeDate: string | null; // goal.close_date — date
|
||||
achieved: boolean | null; // goal.achieved — tinyint(1), null until closed
|
||||
closeNotes: string | null; // goal.close_notes — text
|
||||
progressEventCount: number; // count of progress_event rows for this goal
|
||||
benchmarkCount: number; // count of benchmark rows for this goal
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ const STORAGE_KEYS = {
|
||||
JWT: 'auth_jwt',
|
||||
REFRESH_TOKEN: 'auth_refresh_token',
|
||||
SESSION_TOKEN: 'auth_session_token',
|
||||
PROGRAM_NAME: 'auth_program_name',
|
||||
SCHOOL_DISTRICT_NAME: 'auth_school_district_name',
|
||||
} as const;
|
||||
|
||||
// Refresh the JWT this many seconds before it actually expires.
|
||||
@@ -34,8 +36,8 @@ export class Auth {
|
||||
private readonly _sessionToken = signal<string | null>(this.loadSessionToken());
|
||||
private readonly _programs = signal<UserProgramSummary[]>([]);
|
||||
private readonly _isRefreshing = signal(false);
|
||||
private readonly _programName = signal<string>('');
|
||||
private readonly _schoolDistrictName = signal<string>('');
|
||||
private readonly _programName = signal<string>(localStorage.getItem(STORAGE_KEYS.PROGRAM_NAME) ?? '');
|
||||
private readonly _schoolDistrictName = signal<string>(localStorage.getItem(STORAGE_KEYS.SCHOOL_DISTRICT_NAME) ?? '');
|
||||
|
||||
/** The currently authenticated user, parsed from the JWT. Null when logged out. */
|
||||
readonly user = computed<AuthUser | null>(() => {
|
||||
@@ -198,6 +200,8 @@ export class Auth {
|
||||
// Store program context for header display
|
||||
this._programName.set(data.programName);
|
||||
this._schoolDistrictName.set(data.schoolDistrictName);
|
||||
localStorage.setItem(STORAGE_KEYS.PROGRAM_NAME, data.programName);
|
||||
localStorage.setItem(STORAGE_KEYS.SCHOOL_DISTRICT_NAME, data.schoolDistrictName);
|
||||
|
||||
// Clear phase-1 artefacts
|
||||
localStorage.removeItem(STORAGE_KEYS.SESSION_TOKEN);
|
||||
@@ -243,6 +247,8 @@ export class Auth {
|
||||
this._isRefreshing.set(false);
|
||||
this._programName.set('');
|
||||
this._schoolDistrictName.set('');
|
||||
localStorage.removeItem(STORAGE_KEYS.PROGRAM_NAME);
|
||||
localStorage.removeItem(STORAGE_KEYS.SCHOOL_DISTRICT_NAME);
|
||||
}
|
||||
|
||||
private loadSessionToken(): string | null {
|
||||
|
||||
@@ -19,41 +19,41 @@ export class DummyStudentService {
|
||||
'1': {
|
||||
studentIdentifier: 'J.B',
|
||||
goals: [
|
||||
{ goalId: 'g1', goalParentId: null, description: 'Work on main-idea identification and inference skills across fiction and nonfiction texts.', category: 'Academics', baseline: '', progressEventCount: 5, benchmarkCount: 2 },
|
||||
{ goalId: 'g2', goalParentId: null, description: 'Finish all units in the algebra course including linear equations and graphing.', category: 'Academics', baseline: '', progressEventCount: 2, benchmarkCount: 0 },
|
||||
{ goalId: 'g3', goalParentId: null, description: 'Write a reflective journal entry each week to build writing fluency.', category: 'Communication', baseline: '', progressEventCount: 8, benchmarkCount: 1 },
|
||||
{ goalId: 'g1', goalParentId: null, description: 'Work on main-idea identification and inference skills across fiction and nonfiction texts.', category: 'Academics', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 5, benchmarkCount: 2 },
|
||||
{ goalId: 'g2', goalParentId: null, description: 'Finish all units in the algebra course including linear equations and graphing.', category: 'Academics', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 2, benchmarkCount: 0 },
|
||||
{ goalId: 'g3', goalParentId: null, description: 'Write a reflective journal entry each week to build writing fluency.', category: 'Communication', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 8, benchmarkCount: 1 },
|
||||
],
|
||||
},
|
||||
'2': {
|
||||
studentIdentifier: 'M.K',
|
||||
goals: [
|
||||
{ goalId: 'g4', goalParentId: null, description: 'Prepare for and pass the industry certification exam by end of quarter.', category: 'Career Readiness', baseline: '', progressEventCount: 3, benchmarkCount: 0 },
|
||||
{ goalId: 'g5', goalParentId: null, description: 'Maintain consistent attendance throughout the term.', category: 'Behavior', baseline: '', progressEventCount: 0, benchmarkCount: 0 },
|
||||
{ goalId: 'g6', goalParentId: null, description: 'Log the required 40 hours at the assigned internship site.', category: 'Career Readiness', baseline: '', progressEventCount: 12, benchmarkCount: 0 },
|
||||
{ goalId: 'g7', goalParentId: null, description: 'Build a personal portfolio showcasing completed coursework and projects.', category: 'Career Readiness', baseline: '', progressEventCount: 1, benchmarkCount: 0 },
|
||||
{ goalId: 'g4', goalParentId: null, description: 'Prepare for and pass the industry certification exam by end of quarter.', category: 'Career Readiness', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 3, benchmarkCount: 0 },
|
||||
{ goalId: 'g5', goalParentId: null, description: 'Maintain consistent attendance throughout the term.', category: 'Behavior', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 0, benchmarkCount: 0 },
|
||||
{ goalId: 'g6', goalParentId: null, description: 'Log the required 40 hours at the assigned internship site.', category: 'Career Readiness', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 12, benchmarkCount: 0 },
|
||||
{ goalId: 'g7', goalParentId: null, description: 'Build a personal portfolio showcasing completed coursework and projects.', category: 'Career Readiness', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 1, benchmarkCount: 0 },
|
||||
],
|
||||
},
|
||||
'3': {
|
||||
studentIdentifier: 'A.R',
|
||||
goals: [
|
||||
{ goalId: 'g8', goalParentId: null, description: 'Complete practice tests and study modules for GED math and reading sections.', category: 'Academics', baseline: '', progressEventCount: 6, benchmarkCount: 0 },
|
||||
{ goalId: 'g9', goalParentId: null, description: 'Attend the resume writing workshop and produce a final draft.', category: 'Career Readiness', baseline: '', progressEventCount: 0, benchmarkCount: 0 },
|
||||
{ goalId: 'g8', goalParentId: null, description: 'Complete practice tests and study modules for GED math and reading sections.', category: 'Academics', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 6, benchmarkCount: 0 },
|
||||
{ goalId: 'g9', goalParentId: null, description: 'Attend the resume writing workshop and produce a final draft.', category: 'Career Readiness', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 0, benchmarkCount: 0 },
|
||||
],
|
||||
},
|
||||
'4': {
|
||||
studentIdentifier: 'T.W',
|
||||
goals: [
|
||||
{ goalId: 'g10', goalParentId: null, description: 'Present in front of the class at least once per month.', category: 'Communication', baseline: '', progressEventCount: 4, benchmarkCount: 0 },
|
||||
{ goalId: 'g11', goalParentId: null, description: 'Move up one placement level in math by the end of the semester.', category: 'Academics', baseline: '', progressEventCount: 7, benchmarkCount: 0 },
|
||||
{ goalId: 'g12', goalParentId: null, description: 'Learn and apply at least three de-escalation techniques.', category: 'Behavior', baseline: '', progressEventCount: 2, benchmarkCount: 0 },
|
||||
{ goalId: 'g13', goalParentId: null, description: 'Achieve a 30-day unbroken attendance streak.', category: 'Behavior', baseline: '', progressEventCount: 0, benchmarkCount: 0 },
|
||||
{ goalId: 'g14', goalParentId: null, description: 'Complete a job shadow day in a field of interest.', category: 'Career Readiness', baseline: '', progressEventCount: 1, benchmarkCount: 0 },
|
||||
{ goalId: 'g10', goalParentId: null, description: 'Present in front of the class at least once per month.', category: 'Communication', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 4, benchmarkCount: 0 },
|
||||
{ goalId: 'g11', goalParentId: null, description: 'Move up one placement level in math by the end of the semester.', category: 'Academics', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 7, benchmarkCount: 0 },
|
||||
{ goalId: 'g12', goalParentId: null, description: 'Learn and apply at least three de-escalation techniques.', category: 'Behavior', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 2, benchmarkCount: 0 },
|
||||
{ goalId: 'g13', goalParentId: null, description: 'Achieve a 30-day unbroken attendance streak.', category: 'Behavior', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 0, benchmarkCount: 0 },
|
||||
{ goalId: 'g14', goalParentId: null, description: 'Complete a job shadow day in a field of interest.', category: 'Career Readiness', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 1, benchmarkCount: 0 },
|
||||
],
|
||||
},
|
||||
'5': {
|
||||
studentIdentifier: 'L.C',
|
||||
goals: [
|
||||
{ goalId: 'g15', goalParentId: null, description: 'Reach 40 WPM with 95% accuracy on typing assessments.', category: 'Career Readiness', baseline: '', progressEventCount: 3, benchmarkCount: 0 },
|
||||
{ goalId: 'g15', goalParentId: null, description: 'Reach 40 WPM with 95% accuracy on typing assessments.', category: 'Career Readiness', baseline: '', targetCompletionDate: null, closeDate: null, achieved: null, closeNotes: null, progressEventCount: 3, benchmarkCount: 0 },
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -75,6 +75,7 @@ export class DummyStudentService {
|
||||
lastEntryDate: new Date('2026-02-21'),
|
||||
goalCount: 3,
|
||||
progressEventCount: 5,
|
||||
benchmarkCount: 3,
|
||||
},
|
||||
{
|
||||
studentId: '2',
|
||||
@@ -83,6 +84,7 @@ export class DummyStudentService {
|
||||
lastEntryDate: new Date('2026-02-25'),
|
||||
goalCount: 4,
|
||||
progressEventCount: 8,
|
||||
benchmarkCount: 0,
|
||||
},
|
||||
{
|
||||
studentId: '3',
|
||||
@@ -91,6 +93,7 @@ export class DummyStudentService {
|
||||
lastEntryDate: null,
|
||||
goalCount: 2,
|
||||
progressEventCount: 0,
|
||||
benchmarkCount: 0,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -119,6 +122,10 @@ export class DummyStudentService {
|
||||
description: data.description,
|
||||
category: data.category,
|
||||
baseline: data.baseline,
|
||||
targetCompletionDate: data.targetCompletionDate,
|
||||
closeDate: null,
|
||||
achieved: null,
|
||||
closeNotes: null,
|
||||
progressEventCount: 0,
|
||||
benchmarkCount: 0,
|
||||
};
|
||||
|
||||
@@ -114,10 +114,30 @@ export class StudentService {
|
||||
}
|
||||
}
|
||||
|
||||
async addProgressEvent(studentId: string, goalId: string, content: string): Promise<ApiResult> {
|
||||
// *****************************************************************
|
||||
// Creates a new progress event, optionally with benchmark
|
||||
// associations. Returns the new progress event ID on success.
|
||||
// *****************************************************************
|
||||
async addProgressEvent(studentId: string, goalId: string, content: string, benchmarkIds?: string[]): Promise<ApiResult<any>> {
|
||||
try {
|
||||
const result = await firstValueFrom(
|
||||
this.http.post<ResponseResult<void>>(`${this.base}/api/Student/${studentId}/progress-event`, { goalId, content })
|
||||
this.http.post<ResponseResult<any>>(`${this.base}/api/Student/${studentId}/progress-event`, { goalId, content, benchmarkIds })
|
||||
);
|
||||
return result.success
|
||||
? ApiResult.ok(result.data)
|
||||
: ApiResult.fail(result.message);
|
||||
} catch (error) {
|
||||
return ApiResult.fail(describeHttpError(error as HttpErrorResponse));
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Updates a progress event's content and benchmark associations.
|
||||
// *****************************************************************
|
||||
async updateProgressEvent(studentId: string, progressEventId: string, content: string, benchmarkIds?: string[]): Promise<ApiResult> {
|
||||
try {
|
||||
const result = await firstValueFrom(
|
||||
this.http.put<ResponseResult<void>>(`${this.base}/api/Student/${studentId}/progress-events/${progressEventId}`, { content, benchmarkIds })
|
||||
);
|
||||
return result.success
|
||||
? ApiResult.empty()
|
||||
@@ -127,6 +147,22 @@ export class StudentService {
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Returns benchmark IDs associated with a progress event.
|
||||
// *****************************************************************
|
||||
async getProgressEventBenchmarks(progressEventId: string): Promise<ApiResult<string[]>> {
|
||||
try {
|
||||
const result = await firstValueFrom(
|
||||
this.http.get<ResponseResult<string[]>>(`${this.base}/api/Student/progress-events/${progressEventId}/benchmarks`)
|
||||
);
|
||||
return result.success
|
||||
? ApiResult.ok(result.data ?? [])
|
||||
: ApiResult.fail(result.message);
|
||||
} catch (error) {
|
||||
return ApiResult.fail(describeHttpError(error as HttpErrorResponse));
|
||||
}
|
||||
}
|
||||
|
||||
// *****************************************************************
|
||||
// Returns progress events for a given student goal.
|
||||
// *****************************************************************
|
||||
@@ -230,7 +266,15 @@ export class StudentService {
|
||||
// *****************************************************************
|
||||
// Updates a goal's description, category, and baseline.
|
||||
// *****************************************************************
|
||||
async updateGoal(studentId: string, goalId: string, data: { description?: string; category?: string; baseline?: string }): Promise<ApiResult<any>> {
|
||||
async updateGoal(studentId: string, goalId: string, data: {
|
||||
description?: string;
|
||||
category?: string;
|
||||
baseline?: string;
|
||||
targetCompletionDate?: string | null;
|
||||
closeDate?: string | null;
|
||||
achieved?: boolean | null;
|
||||
closeNotes?: string | null;
|
||||
}): Promise<ApiResult<any>> {
|
||||
try {
|
||||
const result = await firstValueFrom(
|
||||
this.http.put<ResponseResult<any>>(`${this.base}/api/Student/${studentId}/goals/${goalId}`, data)
|
||||
|
||||
@@ -6,4 +6,8 @@ body {
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
input[type="date"] {
|
||||
font-family: inherit;
|
||||
}
|
||||
Reference in New Issue
Block a user