mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 11:07:41 +00:00
Desktop Update
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
<div class="card">
|
<div class="card clickable" (click)="onCardClick()">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<span class="category-badge">{{ goal().category }}</span>
|
<span class="category-badge">{{ goal().category }}</span>
|
||||||
<span class="event-count">{{ goal().progressEventCount }} events</span>
|
<span class="event-count">{{ goal().progressEventCount }} events</span>
|
||||||
|
|||||||
@@ -11,6 +11,16 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.625rem;
|
gap: 0.625rem;
|
||||||
|
height: 130px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.clickable:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
.card-header {
|
||||||
@@ -38,6 +48,9 @@
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #111;
|
color: #111;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
@@ -45,4 +58,8 @@
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
color: #555;
|
color: #555;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component, input } from '@angular/core';
|
import { Component, inject, input } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { StudentGoalItem } from '../../../shared/classes/student-goal';
|
import { StudentGoalItem } from '../../../shared/classes/student-goal';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -13,6 +14,9 @@ export class GoalCard {
|
|||||||
|
|
||||||
// ************************** Declarations *************************
|
// ************************** Declarations *************************
|
||||||
|
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
|
||||||
readonly goal = input.required<StudentGoalItem>();
|
readonly goal = input.required<StudentGoalItem>();
|
||||||
|
|
||||||
// ************************** Properties ***************************
|
// ************************** Properties ***************************
|
||||||
@@ -21,5 +25,13 @@ export class GoalCard {
|
|||||||
|
|
||||||
// ************************ Event Handlers *************************
|
// ************************ Event Handlers *************************
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Navigates to the progress events page for this goal.
|
||||||
|
// *****************************************************************
|
||||||
|
onCardClick() {
|
||||||
|
const studentId = this.route.snapshot.paramMap.get('studentId')!;
|
||||||
|
this.router.navigate(['/students', studentId, 'goals', this.goal().goalId, 'progress']);
|
||||||
|
}
|
||||||
|
|
||||||
// ********************** Support Procedures ***********************
|
// ********************** Support Procedures ***********************
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,28 @@
|
|||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<button class="toolbar-btn back-btn" (click)="onBack()">← Students</button>
|
<button class="toolbar-btn back-btn" (click)="onBack()">← Students</button>
|
||||||
@if (studentIdentifier()) {
|
|
||||||
<span class="student-label">{{ studentIdentifier() }}</span>
|
|
||||||
}
|
|
||||||
<span class="spacer"></span>
|
<span class="spacer"></span>
|
||||||
<button class="toolbar-btn" (click)="onAddGoal()">+ Add a Goal</button>
|
<button class="toolbar-btn" (click)="onAddGoal()">+ Add a Goal</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if (studentIdentifier()) {
|
||||||
|
<h2 class="section-header">Goals for {{ studentIdentifier() }}</h2>
|
||||||
|
}
|
||||||
|
|
||||||
@if (showAddModal()) {
|
@if (showAddModal()) {
|
||||||
<app-add-goal-modal
|
<app-add-goal-modal [studentId]="studentId" [existingGoals]="goals()" (goalCreated)="onGoalCreated($event)"
|
||||||
[studentId]="studentId"
|
(cancelled)="onModalCancelled()" />
|
||||||
[existingGoals]="goals()"
|
|
||||||
(goalCreated)="onGoalCreated($event)"
|
|
||||||
(cancelled)="onModalCancelled()"
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (errorMessage()) {
|
@if (errorMessage()) {
|
||||||
<p class="error">{{ errorMessage() }}</p>
|
<p class="error">{{ errorMessage() }}</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
@if (goals().length === 0 && !errorMessage()) {
|
@if (goals().length === 0 && !errorMessage()) {
|
||||||
<p class="empty-state">No goals yet. Click <strong>+ Add a Goal</strong> to get started.</p>
|
<p class="empty-state">No goals yet. Click <strong>+ Add a Goal</strong> to get started.</p>
|
||||||
} @else {
|
} @else {
|
||||||
<div class="card-grid">
|
<div class="card-grid">
|
||||||
@for (goal of goals(); track goal.goalId) {
|
@for (goal of goals(); track goal.goalId) {
|
||||||
<app-goal-card [goal]="goal" />
|
<app-goal-card [goal]="goal" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -36,10 +36,11 @@
|
|||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.student-label {
|
.section-header {
|
||||||
font-size: 0.9375rem;
|
font-size: 1.125rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="card">
|
||||||
|
<p class="content">{{ event().content }}</p>
|
||||||
|
<div class="action-icons">
|
||||||
|
<button class="icon-btn" title="Edit">✎</button>
|
||||||
|
<button class="icon-btn" title="Delete">🗑</button>
|
||||||
|
</div>
|
||||||
|
<span class="author">{{ event().createdByName }}</span>
|
||||||
|
<span class="date">{{ event().createdAt | date:'MMM d, y' }}</span>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
gap: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-icons {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 1;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-self: start;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.date {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: #888;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #888;
|
||||||
|
padding: 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-btn:hover {
|
||||||
|
color: #4f46e5;
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ProgressItem } from './progress-item';
|
||||||
|
|
||||||
|
describe('ProgressItem', () => {
|
||||||
|
let component: ProgressItem;
|
||||||
|
let fixture: ComponentFixture<ProgressItem>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ProgressItem]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ProgressItem);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
import { DatePipe } from '@angular/common';
|
||||||
|
import { ProgressEventDto } from '../../../shared/classes/progress-event.dto';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-progress-item',
|
||||||
|
imports: [DatePipe],
|
||||||
|
templateUrl: './progress-item.html',
|
||||||
|
styleUrl: './progress-item.scss',
|
||||||
|
})
|
||||||
|
export class ProgressItem {
|
||||||
|
|
||||||
|
// ************************** Constructor **************************
|
||||||
|
|
||||||
|
// ************************** Declarations *************************
|
||||||
|
|
||||||
|
readonly event = input.required<ProgressEventDto>();
|
||||||
|
|
||||||
|
// ************************** Properties ***************************
|
||||||
|
|
||||||
|
// ************************ Public Methods *************************
|
||||||
|
|
||||||
|
// ************************ Event Handlers *************************
|
||||||
|
|
||||||
|
// ********************** Support Procedures ***********************
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<div class="toolbar">
|
||||||
|
<button class="toolbar-btn back-btn" (click)="onBack()">← Goals</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (studentIdentifier() && goalTitle()) {
|
||||||
|
<div class="header-row">
|
||||||
|
<h2 class="section-header">
|
||||||
|
{{ events().length }} Progress Events for {{ studentIdentifier() }} for goal: {{ goalTitle() }}
|
||||||
|
@if (isFiltered()) {
|
||||||
|
<span class="filter-count">(showing {{ filteredEvents().length }})</span>
|
||||||
|
}
|
||||||
|
</h2>
|
||||||
|
<div class="search-box">
|
||||||
|
<input type="text" class="search-input" placeholder="Search..." [value]="rawSearchText()"
|
||||||
|
(input)="onSearchInput($any($event.target).value)" />
|
||||||
|
@if (rawSearchText()) {
|
||||||
|
<button class="clear-btn" (click)="onClearSearch()">×</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (errorMessage()) {
|
||||||
|
<p class="error">{{ errorMessage() }}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (filteredEvents().length === 0 && !errorMessage()) {
|
||||||
|
<p class="empty-state">No progress events recorded yet.</p>
|
||||||
|
} @else {
|
||||||
|
<div class="event-list">
|
||||||
|
@for (evt of filteredEvents(); track evt.progressEventId) {
|
||||||
|
<app-progress-item [event]="evt" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-right: calc(0.75rem + 17px);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-count {
|
||||||
|
font-weight: 400;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-box {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
padding: 0.375rem 2rem 0.375rem 0.75rem;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
width: 200px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
border-color: #4f46e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.375rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: #888;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 0 0.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-btn:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #dc2626;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
margin: 2rem auto;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
flex: 1;
|
||||||
|
padding-right: 0.75rem;
|
||||||
|
}
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { ProgressList } from './progress-list';
|
||||||
|
|
||||||
|
describe('ProgressList', () => {
|
||||||
|
let component: ProgressList;
|
||||||
|
let fixture: ComponentFixture<ProgressList>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [ProgressList]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ProgressList);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import { Component, computed, inject, signal, OnDestroy } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { debounceTime } from 'rxjs/operators';
|
||||||
|
import { ProgressItem } from '../progress-item/progress-item';
|
||||||
|
import { ProgressEventDto } from '../../../shared/classes/progress-event.dto';
|
||||||
|
import { DummyStudentService } from '../../../shared/services/dummy-student.service';
|
||||||
|
import { StudentService } from '../../../shared/services/student.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-progress-list',
|
||||||
|
imports: [ProgressItem],
|
||||||
|
templateUrl: './progress-list.html',
|
||||||
|
styleUrl: './progress-list.scss',
|
||||||
|
})
|
||||||
|
export class ProgressList implements OnDestroy {
|
||||||
|
|
||||||
|
// ************************** Constructor **************************
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.studentId = this.route.snapshot.paramMap.get('studentId')!;
|
||||||
|
this.goalId = this.route.snapshot.paramMap.get('goalId')!;
|
||||||
|
this.loadEvents();
|
||||||
|
this.loadGoalTitle();
|
||||||
|
|
||||||
|
this.searchInput$.pipe(debounceTime(300)).subscribe(term => {
|
||||||
|
this.searchTerm.set(term);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ************************** Declarations *************************
|
||||||
|
|
||||||
|
private readonly dummyService = inject(DummyStudentService);
|
||||||
|
private readonly studentService = inject(StudentService);
|
||||||
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
|
private readonly studentId: string;
|
||||||
|
private readonly goalId: string;
|
||||||
|
private readonly searchInput$ = new Subject<string>();
|
||||||
|
|
||||||
|
protected readonly studentIdentifier = signal<string | null>(null);
|
||||||
|
protected readonly goalTitle = signal<string | null>(null);
|
||||||
|
protected readonly events = signal<ProgressEventDto[]>([]);
|
||||||
|
protected readonly errorMessage = signal<string | null>(null);
|
||||||
|
protected readonly rawSearchText = signal('');
|
||||||
|
protected readonly searchTerm = signal('');
|
||||||
|
|
||||||
|
// ************************** Properties ***************************
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Returns events filtered by the debounced search term. Matches
|
||||||
|
// against the event content (case-insensitive). Only filters when
|
||||||
|
// the term is at least 2 characters.
|
||||||
|
// *****************************************************************
|
||||||
|
protected readonly filteredEvents = computed(() => {
|
||||||
|
const term = this.searchTerm().trim().toLowerCase();
|
||||||
|
if (term.length < 2) return this.events();
|
||||||
|
return this.events().filter(e => e.content.toLowerCase().includes(term));
|
||||||
|
});
|
||||||
|
|
||||||
|
protected readonly isFiltered = computed(() => {
|
||||||
|
return this.searchTerm().trim().length >= 2 && this.filteredEvents().length !== this.events().length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ************************ Public Methods *************************
|
||||||
|
|
||||||
|
// ************************ Event Handlers *************************
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Navigates back to the goals list for this student.
|
||||||
|
// *****************************************************************
|
||||||
|
onBack() {
|
||||||
|
this.router.navigate(['/students', this.studentId, 'goals']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Pushes the raw input value into the debounce stream.
|
||||||
|
// *****************************************************************
|
||||||
|
onSearchInput(value: string) {
|
||||||
|
this.rawSearchText.set(value);
|
||||||
|
this.searchInput$.next(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Clears the search box and resets the filter.
|
||||||
|
// *****************************************************************
|
||||||
|
onClearSearch() {
|
||||||
|
this.rawSearchText.set('');
|
||||||
|
this.searchTerm.set('');
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy() {
|
||||||
|
this.searchInput$.complete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********************** Support Procedures ***********************
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Loads progress events for the given goal from the dummy service,
|
||||||
|
// sorted newest-first by createdAt.
|
||||||
|
// TODO: Replace DummyStudentService with StudentService
|
||||||
|
// *****************************************************************
|
||||||
|
private loadEvents() {
|
||||||
|
this.dummyService.getProgressEventsForGoal(this.goalId).then(result => {
|
||||||
|
if (!result.success) {
|
||||||
|
this.errorMessage.set(result.message);
|
||||||
|
} else {
|
||||||
|
const sorted = (result.payload ?? [])
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());
|
||||||
|
this.events.set(sorted);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Loads the goal title from the student's goal list so the heading
|
||||||
|
// can display "Progress for <goal title>".
|
||||||
|
// *****************************************************************
|
||||||
|
private loadGoalTitle() {
|
||||||
|
this.studentService.getGoalsForStudent(this.studentId).then(result => {
|
||||||
|
if (result.success && result.payload) {
|
||||||
|
this.studentIdentifier.set(result.payload.studentIdentifier);
|
||||||
|
const goal = result.payload.goals.find(g => g.goalId === this.goalId);
|
||||||
|
this.goalTitle.set(goal?.title ?? null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Routes } from '@angular/router';
|
|||||||
import { Home } from './pages/home/home';
|
import { Home } from './pages/home/home';
|
||||||
import { StudentCardList } from './components/student-card-list/student-card-list';
|
import { StudentCardList } from './components/student-card-list/student-card-list';
|
||||||
import { GoalList } from './components/goal-list/goal-list';
|
import { GoalList } from './components/goal-list/goal-list';
|
||||||
|
import { ProgressList } from './components/progress-list/progress-list';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
@@ -11,6 +12,7 @@ export default [
|
|||||||
{ path: '', redirectTo: 'students', pathMatch: 'full' },
|
{ path: '', redirectTo: 'students', pathMatch: 'full' },
|
||||||
{ path: 'students', component: StudentCardList },
|
{ path: 'students', component: StudentCardList },
|
||||||
{ path: 'students/:studentId/goals', component: GoalList },
|
{ path: 'students/:studentId/goals', component: GoalList },
|
||||||
|
{ path: 'students/:studentId/goals/:goalId/progress', component: ProgressList },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] satisfies Routes;
|
] satisfies Routes;
|
||||||
|
|||||||
+1
-1
@@ -69,7 +69,7 @@
|
|||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
font-size: 0.875rem;
|
font-size: 1rem;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
resize: vertical;
|
resize: vertical;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export interface ProgressEventDto {
|
||||||
|
progressEventId: string;
|
||||||
|
content: string;
|
||||||
|
createdAt: Date;
|
||||||
|
createdByName: string;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { StudentCardDto } from '../classes/student-card.dto';
|
|||||||
import { ApiResult } from '../classes/api-result';
|
import { ApiResult } from '../classes/api-result';
|
||||||
import { StudentGoalSummary, StudentGoalItem } from '../classes/student-goal';
|
import { StudentGoalSummary, StudentGoalItem } from '../classes/student-goal';
|
||||||
import { CreateGoalDto } from '../classes/create-goal.dto';
|
import { CreateGoalDto } from '../classes/create-goal.dto';
|
||||||
|
import { ProgressEventDto } from '../classes/progress-event.dto';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -19,39 +20,39 @@ export class DummyStudentService {
|
|||||||
goals: [
|
goals: [
|
||||||
{ goalId: 'g1', goalParentId: null, title: 'Improve reading comprehension', description: 'Work on main-idea identification and inference skills across fiction and nonfiction texts.', category: 'Academics', progressEventCount: 5 },
|
{ goalId: 'g1', goalParentId: null, title: 'Improve reading comprehension', description: 'Work on main-idea identification and inference skills across fiction and nonfiction texts.', category: 'Academics', progressEventCount: 5 },
|
||||||
{ goalId: 'g2', goalParentId: null, title: 'Complete algebra module', description: 'Finish all units in the algebra course including linear equations and graphing.', category: 'Academics', progressEventCount: 2 },
|
{ goalId: 'g2', goalParentId: null, title: 'Complete algebra module', description: 'Finish all units in the algebra course including linear equations and graphing.', category: 'Academics', progressEventCount: 2 },
|
||||||
{ goalId: 'g3', goalParentId: null, title: 'Weekly journal entries', description: 'Write a reflective journal entry each week to build writing fluency.', category: 'Communication', progressEventCount: 8 },
|
{ goalId: 'g3', goalParentId: null, title: 'Weekly journal entries', description: 'Write a reflective journal entry each week to build writing fluency.', category: 'Communication', progressEventCount: 8 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'2': {
|
'2': {
|
||||||
studentIdentifier: 'M.K',
|
studentIdentifier: 'M.K',
|
||||||
goals: [
|
goals: [
|
||||||
{ goalId: 'g4', goalParentId: null, title: 'Pass certification exam', description: 'Prepare for and pass the industry certification exam by end of quarter.', category: 'Career Readiness', progressEventCount: 3 },
|
{ goalId: 'g4', goalParentId: null, title: 'Pass certification exam', description: 'Prepare for and pass the industry certification exam by end of quarter.', category: 'Career Readiness', progressEventCount: 3 },
|
||||||
{ goalId: 'g5', goalParentId: null, title: 'Attendance above 90%', description: 'Maintain consistent attendance throughout the term.', category: 'Behavior', progressEventCount: 0 },
|
{ goalId: 'g5', goalParentId: null, title: 'Attendance above 90%', description: 'Maintain consistent attendance throughout the term.', category: 'Behavior', progressEventCount: 0 },
|
||||||
{ goalId: 'g6', goalParentId: null, title: 'Complete internship hours', description: 'Log the required 40 hours at the assigned internship site.', category: 'Career Readiness', progressEventCount: 12 },
|
{ goalId: 'g6', goalParentId: null, title: 'Complete internship hours', description: 'Log the required 40 hours at the assigned internship site.', category: 'Career Readiness', progressEventCount: 12 },
|
||||||
{ goalId: 'g7', goalParentId: null, title: 'Portfolio project', description: 'Build a personal portfolio showcasing completed coursework and projects.', category: 'Career Readiness', progressEventCount: 1 },
|
{ goalId: 'g7', goalParentId: null, title: 'Portfolio project', description: 'Build a personal portfolio showcasing completed coursework and projects.', category: 'Career Readiness', progressEventCount: 1 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'3': {
|
'3': {
|
||||||
studentIdentifier: 'A.R',
|
studentIdentifier: 'A.R',
|
||||||
goals: [
|
goals: [
|
||||||
{ goalId: 'g8', goalParentId: null, title: 'GED preparation', description: 'Complete practice tests and study modules for GED math and reading sections.', category: 'Academics', progressEventCount: 6 },
|
{ goalId: 'g8', goalParentId: null, title: 'GED preparation', description: 'Complete practice tests and study modules for GED math and reading sections.', category: 'Academics', progressEventCount: 6 },
|
||||||
{ goalId: 'g9', goalParentId: null, title: 'Resume workshop', description: 'Attend the resume writing workshop and produce a final draft.', category: 'Career Readiness', progressEventCount: 0 },
|
{ goalId: 'g9', goalParentId: null, title: 'Resume workshop', description: 'Attend the resume writing workshop and produce a final draft.', category: 'Career Readiness', progressEventCount: 0 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'4': {
|
'4': {
|
||||||
studentIdentifier: 'T.W',
|
studentIdentifier: 'T.W',
|
||||||
goals: [
|
goals: [
|
||||||
{ goalId: 'g10', goalParentId: null, title: 'Public speaking practice', description: 'Present in front of the class at least once per month.', category: 'Communication', progressEventCount: 4 },
|
{ goalId: 'g10', goalParentId: null, title: 'Public speaking practice', description: 'Present in front of the class at least once per month.', category: 'Communication', progressEventCount: 4 },
|
||||||
{ goalId: 'g11', goalParentId: null, title: 'Math placement improvement', description: 'Move up one placement level in math by the end of the semester.', category: 'Academics', progressEventCount: 7 },
|
{ goalId: 'g11', goalParentId: null, title: 'Math placement improvement', description: 'Move up one placement level in math by the end of the semester.', category: 'Academics', progressEventCount: 7 },
|
||||||
{ goalId: 'g12', goalParentId: null, title: 'Conflict resolution strategies', description: 'Learn and apply at least three de-escalation techniques.', category: 'Behavior', progressEventCount: 2 },
|
{ goalId: 'g12', goalParentId: null, title: 'Conflict resolution strategies', description: 'Learn and apply at least three de-escalation techniques.', category: 'Behavior', progressEventCount: 2 },
|
||||||
{ goalId: 'g13', goalParentId: null, title: 'Daily attendance streak', description: 'Achieve a 30-day unbroken attendance streak.', category: 'Behavior', progressEventCount: 0 },
|
{ goalId: 'g13', goalParentId: null, title: 'Daily attendance streak', description: 'Achieve a 30-day unbroken attendance streak.', category: 'Behavior', progressEventCount: 0 },
|
||||||
{ goalId: 'g14', goalParentId: null, title: 'Job shadow experience', description: 'Complete a job shadow day in a field of interest.', category: 'Career Readiness', progressEventCount: 1 },
|
{ goalId: 'g14', goalParentId: null, title: 'Job shadow experience', description: 'Complete a job shadow day in a field of interest.', category: 'Career Readiness', progressEventCount: 1 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'5': {
|
'5': {
|
||||||
studentIdentifier: 'L.C',
|
studentIdentifier: 'L.C',
|
||||||
goals: [
|
goals: [
|
||||||
{ goalId: 'g15', goalParentId: null, title: 'Improve typing speed', description: 'Reach 40 WPM with 95% accuracy on typing assessments.', category: 'Career Readiness', progressEventCount: 3 },
|
{ goalId: 'g15', goalParentId: null, title: 'Improve typing speed', description: 'Reach 40 WPM with 95% accuracy on typing assessments.', category: 'Career Readiness', progressEventCount: 3 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -65,7 +66,7 @@ export class DummyStudentService {
|
|||||||
// until the API endpoint is available.
|
// until the API endpoint is available.
|
||||||
// *****************************************************************
|
// *****************************************************************
|
||||||
async getMyStudents(): Promise<ApiResult<StudentCardDto[]>> {
|
async getMyStudents(): Promise<ApiResult<StudentCardDto[]>> {
|
||||||
var payload = [
|
var payload = [
|
||||||
{
|
{
|
||||||
studentId: '1',
|
studentId: '1',
|
||||||
identifier: 'J.B',
|
identifier: 'J.B',
|
||||||
@@ -97,8 +98,7 @@ export class DummyStudentService {
|
|||||||
|
|
||||||
async getGoalsForStudent(studentId: string): Promise<ApiResult<StudentGoalSummary | null>> {
|
async getGoalsForStudent(studentId: string): Promise<ApiResult<StudentGoalSummary | null>> {
|
||||||
var goals = this.data[studentId] ?? null;
|
var goals = this.data[studentId] ?? null;
|
||||||
if (goals === null)
|
if (goals === null) {
|
||||||
{
|
|
||||||
return ApiResult.fail('Student not found');
|
return ApiResult.fail('Student not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,6 +129,38 @@ export class DummyStudentService {
|
|||||||
return ApiResult.empty();
|
return ApiResult.empty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Returns hardcoded progress events for a given goal. The real
|
||||||
|
// service will call the API with the goal ID.
|
||||||
|
// TODO: Replace with actual API call
|
||||||
|
// *****************************************************************
|
||||||
|
async getProgressEventsForGoal(goalId: string): Promise<ApiResult<ProgressEventDto[]>> {
|
||||||
|
const events: ProgressEventDto[] = [
|
||||||
|
{ progressEventId: 'pe1', content: 'Student demonstrated strong understanding of the topic during today\'s session. Completed all assigned exercises independently.', createdAt: new Date('2026-02-28T10:30:00'), createdByName: 'Sarah Johnson' },
|
||||||
|
{ progressEventId: 'pe2', content: 'Reviewed previous week\'s material. Student needed some additional guidance but showed improvement by end of session.', createdAt: new Date('2026-02-27T14:15:00'), createdByName: 'Mike Thompson' },
|
||||||
|
{ progressEventId: 'pe3', content: 'Initial assessment completed. Identified key areas for focused practice going forward.', createdAt: new Date('2026-02-26T09:00:00'), createdByName: 'Sarah Johnson' },
|
||||||
|
{ progressEventId: 'pe4', content: 'Practiced problem-solving strategies with real-world scenarios. Student engaged well and asked thoughtful questions.', createdAt: new Date('2026-02-25T11:00:00'), createdByName: 'Lisa Martinez' },
|
||||||
|
{ progressEventId: 'pe5', content: 'Worked on time management skills. Created a weekly planner and discussed prioritization techniques.', createdAt: new Date('2026-02-24T13:45:00'), createdByName: 'Mike Thompson' },
|
||||||
|
{ progressEventId: 'pe6', content: 'Student completed a timed practice exercise. Performance improved compared to last week.', createdAt: new Date('2026-02-21T10:00:00'), createdByName: 'Sarah Johnson' },
|
||||||
|
{ progressEventId: 'pe7', content: 'Discussed long-term objectives and broke them into smaller milestones. Student is motivated and on track.', createdAt: new Date('2026-02-20T15:30:00'), createdByName: 'Lisa Martinez' },
|
||||||
|
{ progressEventId: 'pe8', content: 'Reviewed feedback from previous assignment. Student made corrections independently with minimal prompting.', createdAt: new Date('2026-02-19T09:30:00'), createdByName: 'Mike Thompson' },
|
||||||
|
{ progressEventId: 'pe9', content: 'Collaborative session with peer group. Student contributed actively and helped explain concepts to others.', createdAt: new Date('2026-02-18T14:00:00'), createdByName: 'Sarah Johnson' },
|
||||||
|
{ progressEventId: 'pe10', content: 'Introduced new topic area. Student showed curiosity and took detailed notes for independent review.', createdAt: new Date('2026-02-14T11:15:00'), createdByName: 'Lisa Martinez' },
|
||||||
|
{ progressEventId: 'pe11', content: 'Student struggled with today\'s material but remained focused. Will revisit key concepts next session.', createdAt: new Date('2026-02-13T10:45:00'), createdByName: 'Mike Thompson' },
|
||||||
|
{ progressEventId: 'pe12', content: 'Follow-up on yesterday\'s challenging session. Student showed marked improvement after overnight reflection.', createdAt: new Date('2026-02-12T09:00:00'), createdByName: 'Mike Thompson' },
|
||||||
|
{ progressEventId: 'pe13', content: 'Mid-term progress check. Student is meeting expectations in most areas with room for growth in written expression.', createdAt: new Date('2026-02-11T13:00:00'), createdByName: 'Sarah Johnson' },
|
||||||
|
{ progressEventId: 'pe14', content: 'Worked through a series of practice problems. Accuracy rate was 85%, up from 70% two weeks ago.', createdAt: new Date('2026-02-10T10:30:00'), createdByName: 'Lisa Martinez' },
|
||||||
|
{ progressEventId: 'pe15', content: 'Student presented a short summary of recent learning to the group. Showed confidence and clarity.', createdAt: new Date('2026-02-07T14:30:00'), createdByName: 'Sarah Johnson' },
|
||||||
|
{ progressEventId: 'pe16', content: 'Explored supplementary resources together. Student identified two additional practice tools to use independently.', createdAt: new Date('2026-02-06T11:00:00'), createdByName: 'Mike Thompson' },
|
||||||
|
{ progressEventId: 'pe17', content: 'Reviewed study habits and discussed strategies for staying consistent. Student committed to a daily review routine.', createdAt: new Date('2026-02-05T09:15:00'), createdByName: 'Lisa Martinez' },
|
||||||
|
{ progressEventId: 'pe18', content: 'Hands-on activity session. Student completed the project ahead of schedule with strong attention to detail.', createdAt: new Date('2026-02-04T13:30:00'), createdByName: 'Sarah Johnson' },
|
||||||
|
{ progressEventId: 'pe19', content: 'Addressed gaps identified in the initial assessment. Student showed solid understanding of foundational concepts.', createdAt: new Date('2026-02-03T10:00:00'), createdByName: 'Mike Thompson' },
|
||||||
|
{ progressEventId: 'pe20', content: 'First session of the term. Established rapport and set expectations for the upcoming weeks.', createdAt: new Date('2026-01-31T09:00:00'), createdByName: 'Sarah Johnson' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return ApiResult.ok(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ************************ Event Handlers *************************
|
// ************************ Event Handlers *************************
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user