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 @@
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!
+
+
+
+
+
+
+
+
+
+
+
+
\ 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 ***********************