From 41e7120012e4639ec0f58a93f361567d2813c299 Mon Sep 17 00:00:00 2001 From: ivan-pelly Date: Sun, 1 Mar 2026 18:21:51 -0800 Subject: [PATCH] Latest --- .gitignore | 1 + .../src/app/app.routes.ts | 3 + .../src/app/desktop/pages/home/home.html | 2 + .../src/app/desktop/pages/home/home.scss | 18 +++ .../src/app/desktop/pages/home/home.ts | 12 +- .../components/student-card/student-card.html | 8 ++ .../components/student-card/student-card.scss | 44 ++++++ .../student-card/student-card.spec.ts | 23 +++ .../components/student-card/student-card.ts | 34 +++++ .../src/app/mobile/mobile.routes.ts | 14 +- .../add-progress-event.html | 27 ++++ .../add-progress-event.scss | 132 ++++++++++++++++++ .../add-progress-event.spec.ts | 23 +++ .../add-progress-event/add-progress-event.ts | 85 +++++++++++ .../src/app/mobile/pages/home/home.html | 18 ++- .../src/app/mobile/pages/home/home.scss | 65 +++++++++ .../src/app/mobile/pages/home/home.ts | 41 +++++- .../pages/student-goals/student-goals.html | 21 +++ .../pages/student-goals/student-goals.scss | 93 ++++++++++++ .../pages/student-goals/student-goals.spec.ts | 23 +++ .../pages/student-goals/student-goals.ts | 63 +++++++++ .../app/mobile/pages/students/students.html | 5 + .../app/mobile/pages/students/students.scss | 9 ++ .../mobile/pages/students/students.spec.ts | 23 +++ .../src/app/mobile/pages/students/students.ts | 41 ++++++ .../src/app/shared/classes/api-result.ts | 37 +++++ .../src/app/shared/classes/http-errors.ts | 31 ++++ .../src/app/shared/guards/auth.guard.ts | 19 +++ .../src/app/shared/pages/login/login.html | 99 +++++-------- .../src/app/shared/pages/login/login.ts | 20 ++- .../dummy-mobile-home-meta.service.ts | 42 ++++++ .../dummy-save-progress-event.service.ts | 34 +++++ .../services/dummy-student-goal.service.ts | 94 +++++++++++++ .../app/shared/services/platform.service.ts | 71 +++++++--- .../app/shared/services/student.service.ts | 16 +++ 35 files changed, 1194 insertions(+), 97 deletions(-) create mode 100644 ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.html create mode 100644 ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.scss create mode 100644 ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.spec.ts create mode 100644 ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.ts create mode 100644 ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.html create mode 100644 ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.scss create mode 100644 ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.spec.ts create mode 100644 ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.ts create mode 100644 ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.html create mode 100644 ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.scss create mode 100644 ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.spec.ts create mode 100644 ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.ts create mode 100644 ui/winstudentgoaltracker/src/app/mobile/pages/students/students.html create mode 100644 ui/winstudentgoaltracker/src/app/mobile/pages/students/students.scss create mode 100644 ui/winstudentgoaltracker/src/app/mobile/pages/students/students.spec.ts create mode 100644 ui/winstudentgoaltracker/src/app/mobile/pages/students/students.ts create mode 100644 ui/winstudentgoaltracker/src/app/shared/classes/api-result.ts create mode 100644 ui/winstudentgoaltracker/src/app/shared/classes/http-errors.ts create mode 100644 ui/winstudentgoaltracker/src/app/shared/guards/auth.guard.ts create mode 100644 ui/winstudentgoaltracker/src/app/shared/services/dummy-mobile-home-meta.service.ts create mode 100644 ui/winstudentgoaltracker/src/app/shared/services/dummy-save-progress-event.service.ts create mode 100644 ui/winstudentgoaltracker/src/app/shared/services/dummy-student-goal.service.ts diff --git a/.gitignore b/.gitignore index 02287c1..a6e429c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ /.vs/WinStudentGoalTracker/FileContentIndex /.vs/WinStudentGoalTracker/DesignTimeBuild /.vs/WinStudentGoalTracker/v17 +/.vs diff --git a/ui/winstudentgoaltracker/src/app/app.routes.ts b/ui/winstudentgoaltracker/src/app/app.routes.ts index f955d85..06c3ea9 100644 --- a/ui/winstudentgoaltracker/src/app/app.routes.ts +++ b/ui/winstudentgoaltracker/src/app/app.routes.ts @@ -2,16 +2,19 @@ import { inject } from '@angular/core'; import { Routes } from '@angular/router'; import { Login } from './shared/pages/login/login'; import { PlatformService } from './shared/services/platform.service'; +import { authGuard } from './shared/guards/auth.guard'; export const routes: Routes = [ { path: 'login', component: Login }, { path: '', canMatch: [() => inject(PlatformService).formFactor() === 'mobile'], + canActivate: [authGuard], loadChildren: () => import('./mobile/mobile.routes'), }, { path: '', + canActivate: [authGuard], loadChildren: () => import('./desktop/desktop.routes'), }, ]; diff --git a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.html b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.html index 3ef3bc5..2bd312d 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.html +++ b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.html @@ -3,6 +3,8 @@
WinStudentGoalTracker + +
diff --git a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.scss b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.scss index 7a7f232..776f49d 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.scss +++ b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.scss @@ -42,6 +42,24 @@ color: #333; } +.spacer { + flex: 1; +} + +.logout-btn { + background: none; + border: 1px solid #ddd; + border-radius: 6px; + padding: 0.25rem 0.75rem; + font-size: 0.8125rem; + color: #333; + cursor: pointer; +} + +.logout-btn:hover { + background: #f5f5f5; +} + /* Body: Sidebar + Content */ .body { display: flex; diff --git a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.ts b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.ts index 3c3278c..e37dd5d 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.ts +++ b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.ts @@ -1,5 +1,6 @@ -import { Component, signal } from '@angular/core'; +import { Component, inject, signal } from '@angular/core'; import { RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; +import { Auth } from '../../../shared/services/auth'; @Component({ selector: 'app-home', @@ -13,6 +14,7 @@ export class Home { // ************************** Declarations ************************* + private readonly auth = inject(Auth); protected readonly sidebarExpanded = signal(false); // ************************** Properties *************************** @@ -25,5 +27,13 @@ export class Home { this.sidebarExpanded.update(v => !v); } + // ***************************************************************** + // Logs the user out and sends them back to the login screen. + // ***************************************************************** + onLogout() { + this.auth.logout().subscribe(); + this.auth.forceLogout(); + } + // ********************** Support Procedures *********************** } diff --git a/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.html b/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.html new file mode 100644 index 0000000..d5a96fa --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.html @@ -0,0 +1,8 @@ +
+

{{ student().identifier }}

+ Age: {{ student().age }} +
+ {{ student().goalCount }} goals + {{ student().progressEventCount }} events +
+
\ No newline at end of file diff --git a/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.scss b/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.scss new file mode 100644 index 0000000..de946e9 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.scss @@ -0,0 +1,44 @@ +:host { + display: block; +} + +.card { + display: flex; + flex-direction: column; + align-items: center; + padding: 1.25rem 1rem; + background: #fff; + border-radius: 10px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.07); + cursor: pointer; +} + +.card:active { + background: #f9f9f9; +} + +.identifier { + margin: 0; + font-size: 1.5rem; + font-weight: 700; + color: #333; +} + +.age-badge { + display: inline-block; + margin-top: 0.5rem; + padding: 0.2rem 0.75rem; + font-size: 0.8125rem; + color: #555; + background: #f0f0f0; + border-radius: 12px; +} + +.stats { + display: flex; + gap: 1rem; + margin-top: 0.75rem; + font-size: 0.8125rem; + font-weight: 500; + color: #555; +} \ No newline at end of file diff --git a/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.spec.ts b/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.spec.ts new file mode 100644 index 0000000..93455c4 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StudentCard } from './student-card'; + +describe('StudentCard', () => { + let component: StudentCard; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StudentCard] + }) + .compileComponents(); + + fixture = TestBed.createComponent(StudentCard); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.ts b/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.ts new file mode 100644 index 0000000..7111147 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/components/student-card/student-card.ts @@ -0,0 +1,34 @@ +import { Component, inject, input } from '@angular/core'; +import { Router } from '@angular/router'; +import { StudentCardDto } from '../../../shared/models/dto/student-card.dto'; + +@Component({ + selector: 'app-student-card', + imports: [], + templateUrl: './student-card.html', + styleUrl: './student-card.scss', +}) +export class StudentCard { + + // ************************** Constructor ************************** + + // ************************** Declarations ************************* + + private readonly router = inject(Router); + readonly student = input.required(); + + // ************************** Properties *************************** + + // ************************ Public Methods ************************* + + // ************************ Event Handlers ************************* + + // ***************************************************************** + // Navigates to the goals page for this student. + // ***************************************************************** + onCardClick() { + this.router.navigate(['students', this.student().studentId, 'goals']); + } + + // ********************** Support Procedures *********************** +} diff --git a/ui/winstudentgoaltracker/src/app/mobile/mobile.routes.ts b/ui/winstudentgoaltracker/src/app/mobile/mobile.routes.ts index 168c5c2..fb42714 100644 --- a/ui/winstudentgoaltracker/src/app/mobile/mobile.routes.ts +++ b/ui/winstudentgoaltracker/src/app/mobile/mobile.routes.ts @@ -1,6 +1,18 @@ import { Routes } from '@angular/router'; import { Home } from './pages/home/home'; +import { Students } from './pages/students/students'; +import { StudentGoals } from './pages/student-goals/student-goals'; +import { AddProgressEvent } from './pages/add-progress-event/add-progress-event'; export default [ - { path: '', component: Home }, + { + path: '', + component: Home, + children: [ + { path: '', redirectTo: 'students', pathMatch: 'full' }, + { path: 'students', component: Students }, + { path: 'students/:studentId/goals', component: StudentGoals }, + { path: 'students/:studentId/goals/:goalId/add-event', component: AddProgressEvent }, + ], + }, ] satisfies Routes; diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.html b/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.html new file mode 100644 index 0000000..1ccbb1a --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.html @@ -0,0 +1,27 @@ +
+ + +

{{ goalTitle() }}

+ + + @if (error()) { +

{{ error() }}

+ } + + +
+ + + + + +
+ + + +
\ No newline at end of file diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.scss b/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.scss new file mode 100644 index 0000000..6c0d7cf --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.scss @@ -0,0 +1,132 @@ +:host { + display: flex; + flex-direction: column; + height: 100%; +} + +.page-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1.25rem; +} + +.event-page { + display: flex; + flex-direction: column; + flex: 1; +} + +.back-btn { + background: none; + border: 1px solid #ddd; + border-radius: 16px; + color: #333; + font-size: 0.9375rem; + font-weight: 500; + cursor: pointer; + padding: 0.5rem 1rem; + flex-shrink: 0; +} + +.back-btn:active { + background: #f5f5f5; +} + +.student-name { + margin-left: auto; + font-size: 1.5rem; + font-weight: 700; + color: #333; +} + +.goal-title { + margin: 0 0 1rem; + font-size: 1rem; + font-weight: 500; + color: #555; +} + +/* Form card */ +.form-card { + background: #fff; + border: 1px solid #e5e5e5; + border-radius: 10px; + padding: 1.25rem; + margin-bottom: 1.25rem; +} + +.field-label { + display: block; + font-size: 0.875rem; + font-weight: 600; + color: #333; + margin-bottom: 0.5rem; +} + +.notes-input { + width: 100%; + border: 1px solid #ddd; + border-radius: 8px; + padding: 0.75rem; + font-size: 0.875rem; + font-family: inherit; + resize: vertical; + margin-bottom: 1rem; + box-sizing: border-box; +} + +.notes-input:focus { + outline: none; + border-color: #4f46e5; +} + +.voice-btn { + display: block; + width: 100%; + padding: 0.625rem; + background: #f5f5f5; + border: 1px solid #ddd; + border-radius: 20px; + font-size: 0.875rem; + color: #333; + cursor: pointer; + text-align: center; +} + +.voice-btn:active { + background: #eee; +} + +/* Save button */ +.save-btn { + display: block; + width: 100%; + padding: 0.875rem; + background: #222; + color: #fff; + border: none; + border-radius: 10px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + margin-top: auto; +} + +.save-btn:active { + background: #444; +} + +.save-btn:disabled { + background: #ccc; + cursor: default; +} + +.error { + margin: 0 0 1rem; + padding: 0.75rem 1rem; + background: #fef2f2; + color: #dc2626; + border-radius: 8px; + font-size: 0.875rem; +} \ No newline at end of file diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.spec.ts b/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.spec.ts new file mode 100644 index 0000000..1e4a484 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AddProgressEvent } from './add-progress-event'; + +describe('AddProgressEvent', () => { + let component: AddProgressEvent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [AddProgressEvent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(AddProgressEvent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.ts b/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.ts new file mode 100644 index 0000000..84022e6 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.ts @@ -0,0 +1,85 @@ +import { Component, computed, inject, signal } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FormsModule } from '@angular/forms'; +import { HttpErrorResponse } from '@angular/common/http'; +import { DummySaveProgressEvent } from '../../../shared/services/dummy-save-progress-event.service'; +import { describeHttpError } from '../../../shared/classes/http-errors'; + +@Component({ + selector: 'app-add-progress-event', + imports: [FormsModule], + templateUrl: './add-progress-event.html', + styleUrl: './add-progress-event.scss', +}) +export class AddProgressEvent { + + // ************************** Constructor ************************** + + constructor() { + this.goalTitle.set(this.route.snapshot.queryParamMap.get('goalTitle') ?? ''); + 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') ?? ''; + } + + // ************************** Declarations ************************* + + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly saveService = inject(DummySaveProgressEvent); + + private readonly studentId: string; + private readonly goalId: string; + + protected readonly goalTitle = signal(''); + protected readonly studentIdentifier = signal(''); + protected readonly notes = signal(''); + protected readonly error = signal(null); + protected readonly saving = signal(false); + + // ************************** Properties *************************** + + // ***************************************************************** + // True when there is content to save. + // ***************************************************************** + protected readonly canSave = computed(() => + this.notes().trim().length > 0 && !this.saving(), + ); + + // ************************ Public Methods ************************* + + // ************************ Event Handlers ************************* + + // ***************************************************************** + // Navigates back to the student's goal list. + // ***************************************************************** + onBack() { + this.router.navigate(['students', this.studentId, 'goals']); + } + + // ***************************************************************** + // Saves the progress event. On success, returns to the goal list. + // On failure, displays the error message from the API. + // ***************************************************************** + onSave() { + this.error.set(null); + this.saving.set(true); + + this.saveService.save(this.studentId, this.goalId, this.notes().trim()).subscribe({ + next: (result) => { + this.saving.set(false); + if (result.success) { + this.router.navigate(['students', this.studentId, 'goals']); + } else { + this.error.set(result.message); + } + }, + error: (err: HttpErrorResponse) => { + this.saving.set(false); + this.error.set(describeHttpError(err)); + }, + }); + } + + // ********************** Support Procedures *********************** +} diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/home/home.html b/ui/winstudentgoaltracker/src/app/mobile/pages/home/home.html index a72ce19..2f1dde2 100644 --- a/ui/winstudentgoaltracker/src/app/mobile/pages/home/home.html +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/home/home.html @@ -1 +1,17 @@ -

Mobile home works!

+
+ +
+

{{ meta()?.programName }}

+
+ + +
+ +
+ + +
+ {{ meta()?.userName }} + +
+
\ No newline at end of file diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/home/home.scss b/ui/winstudentgoaltracker/src/app/mobile/pages/home/home.scss index e69de29..41d718a 100644 --- a/ui/winstudentgoaltracker/src/app/mobile/pages/home/home.scss +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/home/home.scss @@ -0,0 +1,65 @@ +:host { + display: block; + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; +} + +.page { + display: flex; + flex-direction: column; + height: 100vh; + background: #f5f5f5; +} + +/* Header */ +.header { + padding: 1rem; + background: #fff; + border-bottom: 1px solid #eee; + flex-shrink: 0; +} + +.program-name { + margin: 0; + font-size: 1.125rem; + font-weight: 600; + color: #333; +} + +/* Content */ +.content { + flex: 1; + overflow-y: auto; + padding: 1rem; +} + +/* Sticky Footer */ +.footer { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.75rem 1rem; + background: #fff; + border-top: 1px solid #eee; + flex-shrink: 0; +} + +.user-name { + font-size: 0.875rem; + font-weight: 500; + color: #333; +} + +.logout-btn { + background: none; + border: 1px solid #ddd; + border-radius: 6px; + padding: 0.375rem 0.75rem; + font-size: 0.8125rem; + color: #333; + cursor: pointer; +} + +.logout-btn:active { + background: #f5f5f5; +} \ No newline at end of file diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/home/home.ts b/ui/winstudentgoaltracker/src/app/mobile/pages/home/home.ts index fe83818..a07f381 100644 --- a/ui/winstudentgoaltracker/src/app/mobile/pages/home/home.ts +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/home/home.ts @@ -1,11 +1,48 @@ -import { Component } from '@angular/core'; +import { Component, inject, signal } from '@angular/core'; +import { RouterOutlet } from '@angular/router'; +import { DummyMobileHomeMeta, MobileHomeMeta } from '../../../shared/services/dummy-mobile-home-meta.service'; +import { Auth } from '../../../shared/services/auth'; @Component({ selector: 'app-home', - imports: [], + imports: [RouterOutlet], templateUrl: './home.html', styleUrl: './home.scss', }) export class Home { + // ************************** Constructor ************************** + + constructor() { + this.loadMeta(); + } + + // ************************** Declarations ************************* + + private readonly metaService = inject(DummyMobileHomeMeta); + private readonly auth = inject(Auth); + + protected readonly meta = signal(null); + + // ************************** Properties *************************** + + // ************************ Public Methods ************************* + + // ************************ Event Handlers ************************* + + // ***************************************************************** + // Logs the user out and sends them back to the login screen. + // ***************************************************************** + onLogout() { + this.auth.logout().subscribe(); + this.auth.forceLogout(); + } + + // ********************** Support Procedures *********************** + + private loadMeta() { + this.metaService.getMeta().subscribe(data => { + this.meta.set(data); + }); + } } diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.html b/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.html new file mode 100644 index 0000000..8853f1b --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.html @@ -0,0 +1,21 @@ +
+ + + + +

Goals

+
+ @for (goal of data()?.goals; track goal.goalId) { +
+ {{ goal.title }} + {{ goal.progressEventCount }} +
+ } + @empty { +
No goals yet.
+ } +
+
\ No newline at end of file diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.scss b/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.scss new file mode 100644 index 0000000..e0cb215 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.scss @@ -0,0 +1,93 @@ +:host { + display: block; +} + +.page-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 1.25rem; +} + +.back-btn { + background: none; + border: 1px solid #ddd; + border-radius: 16px; + color: #333; + font-size: 0.9375rem; + font-weight: 500; + cursor: pointer; + padding: 0.5rem 1rem; + flex-shrink: 0; +} + +.back-btn:active { + background: #f5f5f5; +} + +.student-name { + margin: 0; + margin-left: auto; + font-size: 1.5rem; + font-weight: 700; + color: #333; +} + +/* Goal cards */ +.section-heading { + margin: 0 0 0.75rem; + font-size: 1rem; + font-weight: 600; + color: #333; +} + +.goal-cards { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.goal-card { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 1.25rem; + background: #fff; + border: 1px solid #e5e5e5; + border-radius: 10px; + cursor: pointer; +} + +.goal-card:active { + background: #f9f9f9; +} + +.goal-title { + font-size: 1rem; + font-weight: 500; + color: #333; +} + +.event-count { + flex-shrink: 0; + min-width: 1.75rem; + height: 1.75rem; + display: flex; + align-items: center; + justify-content: center; + background: #f0f0f0; + border-radius: 50%; + font-size: 0.8125rem; + font-weight: 600; + color: #555; +} + +.empty-state { + padding: 1.5rem 1rem; + text-align: center; + color: #888; + font-size: 0.875rem; + background: #fff; + border-radius: 10px; + border: 1px solid #e5e5e5; +} \ No newline at end of file diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.spec.ts b/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.spec.ts new file mode 100644 index 0000000..fbdc2f5 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { StudentGoals } from './student-goals'; + +describe('StudentGoals', () => { + let component: StudentGoals; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [StudentGoals] + }) + .compileComponents(); + + fixture = TestBed.createComponent(StudentGoals); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.ts b/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.ts new file mode 100644 index 0000000..77544af --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.ts @@ -0,0 +1,63 @@ +import { Component, inject, signal } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DummyStudentGoalService, StudentGoalSummary } from '../../../shared/services/dummy-student-goal.service'; + +@Component({ + selector: 'app-student-goals', + imports: [], + templateUrl: './student-goals.html', + styleUrl: './student-goals.scss', +}) +export class StudentGoals { + + // ************************** Constructor ************************** + + constructor() { + this.loadGoals(); + } + + // ************************** Declarations ************************* + + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly goalService = inject(DummyStudentGoalService); + + private readonly studentId = this.route.snapshot.paramMap.get('studentId') ?? ''; + protected readonly data = signal(null); + + // ************************** Properties *************************** + + // ************************ Public Methods ************************* + + // ************************ Event Handlers ************************* + + // ***************************************************************** + // Navigates back to the student list. + // ***************************************************************** + onBack() { + this.router.navigate(['students'], { relativeTo: this.route.parent }); + } + + // ***************************************************************** + // Navigates to the add-progress-event page for the selected goal. + // ***************************************************************** + onGoalClick(goalId: string, goalTitle: string) { + this.router.navigate( + ['students', this.studentId, 'goals', goalId, 'add-event'], + { queryParams: { goalTitle, studentIdentifier: this.data()?.studentIdentifier } }, + ); + } + + // ********************** Support Procedures *********************** + + // ***************************************************************** + // Reads the student ID from the route param and loads their goals. + // ***************************************************************** + private loadGoals() { + if (!this.studentId) return; + + this.goalService.getGoalsForStudent(this.studentId).subscribe(result => { + this.data.set(result); + }); + } +} diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.html b/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.html new file mode 100644 index 0000000..1fd2cc6 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.html @@ -0,0 +1,5 @@ +
+ @for (student of students(); track student.studentId) { + + } +
\ No newline at end of file diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.scss b/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.scss new file mode 100644 index 0000000..6e6e55e --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.scss @@ -0,0 +1,9 @@ +:host { + display: block; +} + +.card-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; +} \ No newline at end of file diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.spec.ts b/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.spec.ts new file mode 100644 index 0000000..af25e3f --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { Students } from './students'; + +describe('Students', () => { + let component: Students; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [Students] + }) + .compileComponents(); + + fixture = TestBed.createComponent(Students); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.ts b/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.ts new file mode 100644 index 0000000..074b1c2 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.ts @@ -0,0 +1,41 @@ +import { Component, inject, signal } from '@angular/core'; +import { StudentCard } from '../../components/student-card/student-card'; +import { StudentService } from '../../../shared/services/student.service'; +import { StudentCardDto } from '../../../shared/models/dto/student-card.dto'; + +@Component({ + selector: 'app-students', + imports: [StudentCard], + templateUrl: './students.html', + styleUrl: './students.scss', +}) +export class Students { + + // ************************** Constructor ************************** + + constructor() { + this.loadStudents(); + } + + // ************************** Declarations ************************* + + private readonly studentService = inject(StudentService); + protected readonly students = signal([]); + + // ************************** Properties *************************** + + // ************************ Public Methods ************************* + + // ************************ Event Handlers ************************* + + // ********************** Support Procedures *********************** + + // ***************************************************************** + // Loads the list of students assigned to the current user. + // ***************************************************************** + private loadStudents() { + this.studentService.getDummyStudentsForUser().subscribe(data => { + this.students.set(data); + }); + } +} diff --git a/ui/winstudentgoaltracker/src/app/shared/classes/api-result.ts b/ui/winstudentgoaltracker/src/app/shared/classes/api-result.ts new file mode 100644 index 0000000..ad6c5c9 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/shared/classes/api-result.ts @@ -0,0 +1,37 @@ +// ***************************************************************** +// Standard wrapper for API responses. On success, the payload +// contains the returned data. On failure, message contains the +// error description from the server. +// ***************************************************************** +export class ApiResult { + success: boolean; + payload: T | null; + message: string; + + private constructor(success: boolean, payload: T | null, message: string) { + this.success = success; + this.payload = payload; + this.message = message; + } + + // ***************************************************************** + // Creates a successful result with the given payload. + // ***************************************************************** + static ok(payload: T): ApiResult { + return new ApiResult(true, payload, ''); + } + + // ***************************************************************** + // Creates a successful result with no payload. + // ***************************************************************** + static empty(): ApiResult { + return new ApiResult(true, null, ''); + } + + // ***************************************************************** + // Creates a failed result with the given error message. + // ***************************************************************** + static fail(message: string): ApiResult { + return new ApiResult(false, null, message); + } +} diff --git a/ui/winstudentgoaltracker/src/app/shared/classes/http-errors.ts b/ui/winstudentgoaltracker/src/app/shared/classes/http-errors.ts new file mode 100644 index 0000000..0e4212b --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/shared/classes/http-errors.ts @@ -0,0 +1,31 @@ +import { HttpErrorResponse } from '@angular/common/http'; + +// ***************************************************************** +// Maps an HttpErrorResponse to a user-friendly diagnostic message. +// Status 0 means the browser never received a response (API +// unreachable, DNS failure, CORS block, etc.). 5xx errors typically +// indicate a backend issue such as a database connection failure. +// ***************************************************************** +export function describeHttpError(error: HttpErrorResponse): string { + // Try to extract a message from the response body first. + const serverMessage = error.error?.message ?? error.error?.Message; + + switch (error.status) { + case 0: + return 'Unable to reach the server. Check that the API is running and accessible.'; + case 400: + return serverMessage ?? 'Bad request (400).'; + case 401: + return serverMessage ?? 'Not authorized (401).'; + case 403: + return serverMessage ?? 'Access denied (403).'; + case 404: + return 'Endpoint not found (404). The API may be running a different version.'; + case 500: + return 'Server error (500). The API encountered an internal failure (possibly a database issue).'; + case 503: + return 'Server unavailable (503). The API may be starting up or overwhelmed.'; + default: + return `Unexpected error (${error.status}).`; + } +} diff --git a/ui/winstudentgoaltracker/src/app/shared/guards/auth.guard.ts b/ui/winstudentgoaltracker/src/app/shared/guards/auth.guard.ts new file mode 100644 index 0000000..31824db --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/shared/guards/auth.guard.ts @@ -0,0 +1,19 @@ +import { inject } from '@angular/core'; +import { Router, UrlTree } from '@angular/router'; +import { Auth } from '../services/auth'; + +// ***************************************************************** +// Route guard that checks if the user is logged in. If not, they +// get redirected to the login page. Used on all routes that require +// an authenticated session (desktop and mobile home, etc.). +// ***************************************************************** +export function authGuard(): boolean | UrlTree { + const auth = inject(Auth); + const router = inject(Router); + + if (auth.isAuthenticated()) { + return true; + } + + return router.createUrlTree(['/login']); +} diff --git a/ui/winstudentgoaltracker/src/app/shared/pages/login/login.html b/ui/winstudentgoaltracker/src/app/shared/pages/login/login.html index a902090..074d3bf 100644 --- a/ui/winstudentgoaltracker/src/app/shared/pages/login/login.html +++ b/ui/winstudentgoaltracker/src/app/shared/pages/login/login.html @@ -1,78 +1,49 @@ @if (!auth.isAuthenticated() && !auth.isSelectingProgram()) { -
-

Login

+
+

Login

- @if (error()) { -

{{ error() }}

- } + @if (error()) { +

{{ error() }}

+ } -
- + + - + - -
-
+ + +
} @if (auth.isSelectingProgram()) { -
-

Select a Program

-

Choose which program to log into.

+
+

Select a Program

+

Choose which program to log into.

- @if (error()) { -

{{ error() }}

+ @if (error()) { +

{{ error() }}

+ } + +
+ @for (program of auth.programs(); track program.programId) { + } - -
- @for (program of auth.programs(); track program.programId) { - - } -
- -
-} - -@if (auth.isAuthenticated()) { -
-

Authenticated

- - @if (auth.user(); as user) { -
-
User ID
-
{{ user.userId }}
- -
Email
-
{{ user.email }}
- -
Program ID
-
{{ user.programId }}
- -
Role
-
{{ user.role }}
-
- } - - -
-} + +
+} \ No newline at end of file diff --git a/ui/winstudentgoaltracker/src/app/shared/pages/login/login.ts b/ui/winstudentgoaltracker/src/app/shared/pages/login/login.ts index abf66a4..99a3227 100644 --- a/ui/winstudentgoaltracker/src/app/shared/pages/login/login.ts +++ b/ui/winstudentgoaltracker/src/app/shared/pages/login/login.ts @@ -1,7 +1,9 @@ import { Component, inject, signal } from '@angular/core'; import { FormsModule } from '@angular/forms'; +import { HttpErrorResponse } from '@angular/common/http'; import { Router } from '@angular/router'; import { Auth } from '../../services/auth'; +import { describeHttpError } from '../../classes/http-errors'; @Component({ selector: 'app-login', @@ -39,9 +41,9 @@ export class Login { this.error.set(res.message); } }, - error: () => { + error: (err: HttpErrorResponse) => { this.loading.set(false); - this.error.set('Unable to reach the server.'); + this.error.set(describeHttpError(err)); }, }); } @@ -53,24 +55,20 @@ export class Login { this.auth.selectProgram(programId).subscribe({ next: (res) => { this.loading.set(false); - if (!res.success) { + if (res.success) { + this.router.navigateByUrl('/'); + } else { this.error.set(res.message); } }, - error: () => { + error: (err: HttpErrorResponse) => { this.loading.set(false); - this.error.set('Unable to reach the server.'); + this.error.set(describeHttpError(err)); }, }); } - onHome() { - this.router.navigateByUrl('/'); - } - onLogout() { - this.auth.logout().subscribe(); - } onBackToLogin() { this.error.set(null); diff --git a/ui/winstudentgoaltracker/src/app/shared/services/dummy-mobile-home-meta.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/dummy-mobile-home-meta.service.ts new file mode 100644 index 0000000..bf5abfa --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/shared/services/dummy-mobile-home-meta.service.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; + +// ***************************************************************** +// TODO: This dummy service should be replaced by MobileHomeMeta, +// which will fetch real data from the API. +// ***************************************************************** + +export interface MobileHomeMeta { + programName: string; // program.name — varchar(255) + userName: string; // user.name — varchar(255) +} + +@Injectable({ + providedIn: 'root', +}) +export class DummyMobileHomeMeta { + + // ************************** Constructor ************************** + + // ************************** Declarations ************************* + + // ************************** Properties *************************** + + // ************************ Public Methods ************************* + + // ***************************************************************** + // TODO: DUMMY DATA — Returns hardcoded program and user info. + // Replace with MobileHomeMeta service that calls + // GET /api/mobile/home-meta (or similar). + // ***************************************************************** + getMeta(): Observable { + return of({ + programName: 'WIN Program', + userName: 'Polly Balsillie', + }); + } + + // ************************ Event Handlers ************************* + + // ********************** Support Procedures *********************** +} diff --git a/ui/winstudentgoaltracker/src/app/shared/services/dummy-save-progress-event.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/dummy-save-progress-event.service.ts new file mode 100644 index 0000000..e4ac9a1 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/shared/services/dummy-save-progress-event.service.ts @@ -0,0 +1,34 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; +import { ApiResult } from '../classes/api-result'; + +// ***************************************************************** +// TODO: This dummy service should be replaced by SaveProgressEvent, +// which will POST real data to the API. +// ***************************************************************** + +@Injectable({ + providedIn: 'root', +}) +export class DummySaveProgressEvent { + + // ************************** Constructor ************************** + + // ************************** Declarations ************************* + + // ************************** Properties *************************** + + // ************************ Public Methods ************************* + + // ***************************************************************** + // TODO: DUMMY — Always returns success. Replace with + // SaveProgressEvent calling POST /api/progress-events + // ***************************************************************** + save(studentId: string, goalId: string, content: string): Observable { + return of(ApiResult.empty()); + } + + // ************************ Event Handlers ************************* + + // ********************** Support Procedures *********************** +} diff --git a/ui/winstudentgoaltracker/src/app/shared/services/dummy-student-goal.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/dummy-student-goal.service.ts new file mode 100644 index 0000000..dc79b85 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/shared/services/dummy-student-goal.service.ts @@ -0,0 +1,94 @@ +import { Injectable } from '@angular/core'; +import { Observable, of } from 'rxjs'; + +// ***************************************************************** +// TODO: This dummy service should be replaced by StudentGoalService, +// which will fetch real data from the API. +// ***************************************************************** + +export interface StudentGoalSummary { + studentIdentifier: string; // student.identifier — varchar(50) + goals: StudentGoalItem[]; +} + +export interface StudentGoalItem { + goalId: string; // goal.id_goal — char(36) + title: string; // goal.title — varchar(255) + description: string; // goal.description — text + category: string; // goal.category — varchar(100) + progressEventCount: number; // count of progress_event rows for this goal +} + +@Injectable({ + providedIn: 'root', +}) +export class DummyStudentGoalService { + + // ************************** Constructor ************************** + + // ************************** Declarations ************************* + + // ***************************************************************** + // TODO: DUMMY DATA — Maps studentId to identifier and goals. + // Replace with StudentGoalService calling + // GET /api/students/:id/goals + // ***************************************************************** + private readonly data: Record = { + '1': { + studentIdentifier: 'J.B', + goals: [ + { goalId: 'g1', title: 'Improve reading comprehension', description: 'Work on main-idea identification and inference skills across fiction and nonfiction texts.', category: 'Academics', progressEventCount: 5 }, + { goalId: 'g2', title: 'Complete algebra module', description: 'Finish all units in the algebra course including linear equations and graphing.', category: 'Academics', progressEventCount: 2 }, + { goalId: 'g3', title: 'Weekly journal entries', description: 'Write a reflective journal entry each week to build writing fluency.', category: 'Communication', progressEventCount: 8 }, + ], + }, + '2': { + studentIdentifier: 'M.K', + goals: [ + { goalId: 'g4', title: 'Pass certification exam', description: 'Prepare for and pass the industry certification exam by end of quarter.', category: 'Career Readiness', progressEventCount: 3 }, + { goalId: 'g5', title: 'Attendance above 90%', description: 'Maintain consistent attendance throughout the term.', category: 'Behavior', progressEventCount: 0 }, + { goalId: 'g6', title: 'Complete internship hours', description: 'Log the required 40 hours at the assigned internship site.', category: 'Career Readiness', progressEventCount: 12 }, + { goalId: 'g7', title: 'Portfolio project', description: 'Build a personal portfolio showcasing completed coursework and projects.', category: 'Career Readiness', progressEventCount: 1 }, + ], + }, + '3': { + studentIdentifier: 'A.R', + goals: [ + { goalId: 'g8', title: 'GED preparation', description: 'Complete practice tests and study modules for GED math and reading sections.', category: 'Academics', progressEventCount: 6 }, + { goalId: 'g9', title: 'Resume workshop', description: 'Attend the resume writing workshop and produce a final draft.', category: 'Career Readiness', progressEventCount: 0 }, + ], + }, + '4': { + studentIdentifier: 'T.W', + goals: [ + { goalId: 'g10', title: 'Public speaking practice', description: 'Present in front of the class at least once per month.', category: 'Communication', progressEventCount: 4 }, + { goalId: 'g11', title: 'Math placement improvement', description: 'Move up one placement level in math by the end of the semester.', category: 'Academics', progressEventCount: 7 }, + { goalId: 'g12', title: 'Conflict resolution strategies', description: 'Learn and apply at least three de-escalation techniques.', category: 'Behavior', progressEventCount: 2 }, + { goalId: 'g13', title: 'Daily attendance streak', description: 'Achieve a 30-day unbroken attendance streak.', category: 'Behavior', progressEventCount: 0 }, + { goalId: 'g14', title: 'Job shadow experience', description: 'Complete a job shadow day in a field of interest.', category: 'Career Readiness', progressEventCount: 1 }, + ], + }, + '5': { + studentIdentifier: 'L.C', + goals: [ + { goalId: 'g15', title: 'Improve typing speed', description: 'Reach 40 WPM with 95% accuracy on typing assessments.', category: 'Career Readiness', progressEventCount: 3 }, + ], + }, + }; + + // ************************** Properties *************************** + + // ************************ Public Methods ************************* + + // ***************************************************************** + // Returns the student's identifier and their list of goals, + // given a student ID. + // ***************************************************************** + getGoalsForStudent(studentId: string): Observable { + return of(this.data[studentId] ?? null); + } + + // ************************ Event Handlers ************************* + + // ********************** Support Procedures *********************** +} diff --git a/ui/winstudentgoaltracker/src/app/shared/services/platform.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/platform.service.ts index 2492644..1806d80 100644 --- a/ui/winstudentgoaltracker/src/app/shared/services/platform.service.ts +++ b/ui/winstudentgoaltracker/src/app/shared/services/platform.service.ts @@ -7,67 +7,104 @@ export type FormFactor = 'mobile' | 'desktop'; providedIn: 'root', }) export class PlatformService { + + // ************************** Constructor ************************** + private readonly router = inject(Router); - // ──── Raw Hardware Signals ──── + // ************************** Declarations ************************* + // ***************************************************************** + // Checks if the device uses a touch screen (like a phone or tablet) + // rather than a mouse or trackpad. + // ***************************************************************** private readonly isCoarsePointer = typeof window !== 'undefined' && window.matchMedia('(pointer: coarse)').matches; + // ***************************************************************** + // Captures the screen width and height so we can figure out what + // kind of device the user is on. Defaults to a large desktop size + // if running on the server. + // ***************************************************************** private readonly screenWidth = typeof window !== 'undefined' ? window.innerWidth : 1920; private readonly screenHeight = typeof window !== 'undefined' ? window.innerHeight : 1080; + // ***************************************************************** + // The shortest and longest edges of the screen, regardless of + // whether the device is held in portrait or landscape. + // ***************************************************************** private readonly minDimension = Math.min(this.screenWidth, this.screenHeight); private readonly maxDimension = Math.max(this.screenWidth, this.screenHeight); - // ──── Override Layer ──── - - /** When non-null, overrides auto-detection. */ + // ***************************************************************** + // Lets the user (or the app) manually force mobile or desktop mode + // instead of relying on automatic detection. When set to null, the + // app just figures it out on its own. + // ***************************************************************** private readonly formFactorOverride = signal(null); - // ──── Public API ──── + // ************************** Properties *************************** - /** The resolved form factor — auto-detected or overridden. */ + // ***************************************************************** + // The final answer: are we showing the "mobile" or "desktop" + // experience? Uses the manual override if one was set, otherwise + // auto-detects based on the device hardware. + // ***************************************************************** readonly formFactor = computed(() => this.formFactorOverride() ?? this.resolveFormFactor(), ); - /** True when the user has manually toggled away from auto-detection. */ + // ***************************************************************** + // True when the current mode was manually chosen by the user, + // false when it was detected automatically. + // ***************************************************************** readonly isOverridden = computed(() => this.formFactorOverride() !== null); - /** - * Switch to a specific form factor at runtime. - * Forces a route re-evaluation so the user immediately sees the other experience. - */ + // ************************ Public Methods ************************* + + // ***************************************************************** + // Switches between mobile and desktop mode on the fly. After + // switching, the page reloads so the correct layout appears + // immediately. + // ***************************************************************** switchTo(target: FormFactor): void { this.formFactorOverride.set(target); this.router.navigateByUrl(this.router.url); } - /** Clear the override and return to auto-detected form factor. */ + // ***************************************************************** + // Clears any manual override and goes back to letting the device + // decide which mode to show. + // ***************************************************************** resetToAuto(): void { this.formFactorOverride.set(null); this.router.navigateByUrl(this.router.url); } - // ──── Device Classification Policy ──── + // ************************ Event Handlers ************************* + // ********************** Support Procedures *********************** + + // ***************************************************************** + // The rules for deciding whether a device gets the mobile or + // desktop experience: + // - Mouse/trackpad users always get desktop + // - Large tablets (like iPad Pro) get desktop + // - Medium tablets held sideways get desktop + // - Everything else (phones, small tablets) gets mobile + // ***************************************************************** private resolveFormFactor(): FormFactor { - // Non-touch devices are always desktop if (!this.isCoarsePointer) return 'desktop'; - // Large tablets (iPad Pro 12.9" portrait = 1024px) → desktop if (this.minDimension >= 1024) return 'desktop'; - // Medium tablets in landscape with sufficient width → desktop if (this.maxDimension >= 1180 && this.screenWidth > this.screenHeight) { return 'desktop'; } - // Phones and small/portrait tablets → mobile return 'mobile'; } } diff --git a/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts index 304a1b7..b5a9475 100644 --- a/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts +++ b/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts @@ -48,6 +48,22 @@ export class StudentService { ]); } + // ***************************************************************** + // TODO: DUMMY DATA — Replace with getStudentsPerUser, which will + // call GET /api/users/:id/students to return real data. + // Returns students assigned to the current user with their + // identifier, age, goal count, and progress event count. + // ***************************************************************** + getDummyStudentsForUser(): Observable { + return of([ + { studentId: '1', identifier: 'J.B', age: 21, lastEntryDate: '2026-02-21', goalCount: 3, progressEventCount: 5 }, + { studentId: '2', identifier: 'M.K', age: 19, lastEntryDate: '2026-02-25', goalCount: 4, progressEventCount: 8 }, + { studentId: '3', identifier: 'A.R', age: 22, lastEntryDate: null, goalCount: 2, progressEventCount: 0 }, + { studentId: '4', identifier: 'T.W', age: 20, lastEntryDate: '2026-02-18', goalCount: 5, progressEventCount: 12 }, + { studentId: '5', identifier: 'L.C', age: 18, lastEntryDate: '2026-02-27', goalCount: 1, progressEventCount: 2 }, + ]); + } + // ************************ Event Handlers ************************* // ********************** Support Procedures ***********************