diff --git a/api/src/Controllers/AuthController.cs b/api/src/Controllers/AuthController.cs index 0158f44..909bebb 100644 --- a/api/src/Controllers/AuthController.cs +++ b/api/src/Controllers/AuthController.cs @@ -204,6 +204,7 @@ public class AuthController : BaseController UserId = programUser.IdUser, Email = programUser.Email!, ProgramName = programUser.ProgramName!, + SchoolDistrictName = programUser.SchoolDistrictName ?? "", Jwt = accessToken, RefreshToken = fullRefreshToken, Role = programUser.RoleInternalName, diff --git a/api/src/DataAccess/Models/DatabaseObjects/dbProgramUser.cs b/api/src/DataAccess/Models/DatabaseObjects/dbProgramUser.cs index 7ed8f72..4166884 100644 --- a/api/src/DataAccess/Models/DatabaseObjects/dbProgramUser.cs +++ b/api/src/DataAccess/Models/DatabaseObjects/dbProgramUser.cs @@ -7,6 +7,7 @@ public class dbProgramUser public string? Name { get; set; } public required Guid IdProgram { get; set; } public string? ProgramName { get; set; } + public string? SchoolDistrictName { get; set; } public required string RoleInternalName { get; set; } public required string RoleDisplayName { get; set; } public string? Status { get; set; } diff --git a/api/src/Models/ResponseTypes/SelectProgramResponse.cs b/api/src/Models/ResponseTypes/SelectProgramResponse.cs index 5104b7c..4e59183 100644 --- a/api/src/Models/ResponseTypes/SelectProgramResponse.cs +++ b/api/src/Models/ResponseTypes/SelectProgramResponse.cs @@ -5,6 +5,7 @@ public class SelectProgramResponse public Guid UserId { get; set; } public required string Email { get; set; } public required string ProgramName { get; set; } + public required string SchoolDistrictName { get; set; } public required string Jwt { get; set; } public required string RefreshToken { get; set; } public required string Role { get; set; } diff --git a/db/Objects/procedures/sp_User_GetById_WithProgram.sql b/db/Objects/procedures/sp_User_GetById_WithProgram.sql index cbe8926..43c8c36 100644 --- a/db/Objects/procedures/sp_User_GetById_WithProgram.sql +++ b/db/Objects/procedures/sp_User_GetById_WithProgram.sql @@ -10,6 +10,7 @@ BEGIN u.name, up.id_program, p.name AS program_name, + sd.name AS school_district_name, r.internal_name AS role_internal_name, r.name AS role_display_name, up.status @@ -17,6 +18,7 @@ BEGIN JOIN user_program up ON u.id_user = up.id_user AND up.id_program = p_id_program JOIN role r ON up.id_role = r.id_role JOIN program p ON up.id_program = p.id_program + LEFT JOIN school_district sd ON p.id_school_district = sd.id_school_district WHERE u.id_user = p_id_user LIMIT 1; END;; diff --git a/db/Scripts/seed_10_peer_reviewer_programs.sql b/db/Scripts/seed_10_peer_reviewer_programs.sql new file mode 100644 index 0000000..48abebd --- /dev/null +++ b/db/Scripts/seed_10_peer_reviewer_programs.sql @@ -0,0 +1,57 @@ +-- ===================================================================== +-- Seed 10 Programs with Peer Reviewer users under the existing +-- school district: a1b2c3d4-0001-4000-a000-000000000001 +-- +-- Each program gets: +-- 1. A program record +-- 2. A user with the provided password hash/salt +-- 3. A user_program link with the Teacher role +-- +-- Designed to run in TablePlus against a MySQL database. +-- ===================================================================== + +-- ------------------------- +-- 1. PROGRAMS +-- ------------------------- +INSERT INTO program (id_program, id_school_district, name, description, created_at) VALUES +('b2c3d4e5-1001-4000-a000-000000000001', 'a1b2c3d4-0001-4000-a000-000000000001', 'Program 1', 'This is program 1', UTC_TIMESTAMP()), +('b2c3d4e5-1002-4000-a000-000000000001', 'a1b2c3d4-0001-4000-a000-000000000001', 'Program 2', 'This is program 2', UTC_TIMESTAMP()), +('b2c3d4e5-1003-4000-a000-000000000001', 'a1b2c3d4-0001-4000-a000-000000000001', 'Program 3', 'This is program 3', UTC_TIMESTAMP()), +('b2c3d4e5-1004-4000-a000-000000000001', 'a1b2c3d4-0001-4000-a000-000000000001', 'Program 4', 'This is program 4', UTC_TIMESTAMP()), +('b2c3d4e5-1005-4000-a000-000000000001', 'a1b2c3d4-0001-4000-a000-000000000001', 'Program 5', 'This is program 5', UTC_TIMESTAMP()), +('b2c3d4e5-1006-4000-a000-000000000001', 'a1b2c3d4-0001-4000-a000-000000000001', 'Program 6', 'This is program 6', UTC_TIMESTAMP()), +('b2c3d4e5-1007-4000-a000-000000000001', 'a1b2c3d4-0001-4000-a000-000000000001', 'Program 7', 'This is program 7', UTC_TIMESTAMP()), +('b2c3d4e5-1008-4000-a000-000000000001', 'a1b2c3d4-0001-4000-a000-000000000001', 'Program 8', 'This is program 8', UTC_TIMESTAMP()), +('b2c3d4e5-1009-4000-a000-000000000001', 'a1b2c3d4-0001-4000-a000-000000000001', 'Program 9', 'This is program 9', UTC_TIMESTAMP()), +('b2c3d4e5-1010-4000-a000-000000000001', 'a1b2c3d4-0001-4000-a000-000000000001', 'Program 10', 'This is program 10', UTC_TIMESTAMP()); + +-- ------------------------- +-- 2. USERS +-- ------------------------- +INSERT INTO user (id_user, email, name, password_hash, password_salt, created_at) VALUES +('d4e5f6a7-1001-4000-a000-000000000001', 'peer_reviewer_1@gmail.com', 'Peer Reviewer 1', 'FEp+zRcVsX3wAtwriCh2WCnz4DfIZ/vw3M+Ke30VneM=', 'giXYhXeRNxh0OuQ3KQzLcA==', UTC_TIMESTAMP()), +('d4e5f6a7-1002-4000-a000-000000000001', 'peer_reviewer_2@gmail.com', 'Peer Reviewer 2', 'FEp+zRcVsX3wAtwriCh2WCnz4DfIZ/vw3M+Ke30VneM=', 'giXYhXeRNxh0OuQ3KQzLcA==', UTC_TIMESTAMP()), +('d4e5f6a7-1003-4000-a000-000000000001', 'peer_reviewer_3@gmail.com', 'Peer Reviewer 3', 'FEp+zRcVsX3wAtwriCh2WCnz4DfIZ/vw3M+Ke30VneM=', 'giXYhXeRNxh0OuQ3KQzLcA==', UTC_TIMESTAMP()), +('d4e5f6a7-1004-4000-a000-000000000001', 'peer_reviewer_4@gmail.com', 'Peer Reviewer 4', 'FEp+zRcVsX3wAtwriCh2WCnz4DfIZ/vw3M+Ke30VneM=', 'giXYhXeRNxh0OuQ3KQzLcA==', UTC_TIMESTAMP()), +('d4e5f6a7-1005-4000-a000-000000000001', 'peer_reviewer_5@gmail.com', 'Peer Reviewer 5', 'FEp+zRcVsX3wAtwriCh2WCnz4DfIZ/vw3M+Ke30VneM=', 'giXYhXeRNxh0OuQ3KQzLcA==', UTC_TIMESTAMP()), +('d4e5f6a7-1006-4000-a000-000000000001', 'peer_reviewer_6@gmail.com', 'Peer Reviewer 6', 'FEp+zRcVsX3wAtwriCh2WCnz4DfIZ/vw3M+Ke30VneM=', 'giXYhXeRNxh0OuQ3KQzLcA==', UTC_TIMESTAMP()), +('d4e5f6a7-1007-4000-a000-000000000001', 'peer_reviewer_7@gmail.com', 'Peer Reviewer 7', 'FEp+zRcVsX3wAtwriCh2WCnz4DfIZ/vw3M+Ke30VneM=', 'giXYhXeRNxh0OuQ3KQzLcA==', UTC_TIMESTAMP()), +('d4e5f6a7-1008-4000-a000-000000000001', 'peer_reviewer_8@gmail.com', 'Peer Reviewer 8', 'FEp+zRcVsX3wAtwriCh2WCnz4DfIZ/vw3M+Ke30VneM=', 'giXYhXeRNxh0OuQ3KQzLcA==', UTC_TIMESTAMP()), +('d4e5f6a7-1009-4000-a000-000000000001', 'peer_reviewer_9@gmail.com', 'Peer Reviewer 9', 'FEp+zRcVsX3wAtwriCh2WCnz4DfIZ/vw3M+Ke30VneM=', 'giXYhXeRNxh0OuQ3KQzLcA==', UTC_TIMESTAMP()), +('d4e5f6a7-1010-4000-a000-000000000001', 'peer_reviewer_10@gmail.com', 'Peer Reviewer 10', 'FEp+zRcVsX3wAtwriCh2WCnz4DfIZ/vw3M+Ke30VneM=', 'giXYhXeRNxh0OuQ3KQzLcA==', UTC_TIMESTAMP()); + +-- ------------------------- +-- 3. USER_PROGRAM (Teacher role) +-- Teacher role ID: c3d4e5f6-0004-4000-a000-000000000001 +-- ------------------------- +INSERT INTO user_program (id_user_program, id_user, id_program, id_role, is_primary, status, joined_at) VALUES +('e5f6a7b8-1001-4000-a000-000000000001', 'd4e5f6a7-1001-4000-a000-000000000001', 'b2c3d4e5-1001-4000-a000-000000000001', 'c3d4e5f6-0004-4000-a000-000000000001', 1, 'active', UTC_TIMESTAMP()), +('e5f6a7b8-1002-4000-a000-000000000001', 'd4e5f6a7-1002-4000-a000-000000000001', 'b2c3d4e5-1002-4000-a000-000000000001', 'c3d4e5f6-0004-4000-a000-000000000001', 1, 'active', UTC_TIMESTAMP()), +('e5f6a7b8-1003-4000-a000-000000000001', 'd4e5f6a7-1003-4000-a000-000000000001', 'b2c3d4e5-1003-4000-a000-000000000001', 'c3d4e5f6-0004-4000-a000-000000000001', 1, 'active', UTC_TIMESTAMP()), +('e5f6a7b8-1004-4000-a000-000000000001', 'd4e5f6a7-1004-4000-a000-000000000001', 'b2c3d4e5-1004-4000-a000-000000000001', 'c3d4e5f6-0004-4000-a000-000000000001', 1, 'active', UTC_TIMESTAMP()), +('e5f6a7b8-1005-4000-a000-000000000001', 'd4e5f6a7-1005-4000-a000-000000000001', 'b2c3d4e5-1005-4000-a000-000000000001', 'c3d4e5f6-0004-4000-a000-000000000001', 1, 'active', UTC_TIMESTAMP()), +('e5f6a7b8-1006-4000-a000-000000000001', 'd4e5f6a7-1006-4000-a000-000000000001', 'b2c3d4e5-1006-4000-a000-000000000001', 'c3d4e5f6-0004-4000-a000-000000000001', 1, 'active', UTC_TIMESTAMP()), +('e5f6a7b8-1007-4000-a000-000000000001', 'd4e5f6a7-1007-4000-a000-000000000001', 'b2c3d4e5-1007-4000-a000-000000000001', 'c3d4e5f6-0004-4000-a000-000000000001', 1, 'active', UTC_TIMESTAMP()), +('e5f6a7b8-1008-4000-a000-000000000001', 'd4e5f6a7-1008-4000-a000-000000000001', 'b2c3d4e5-1008-4000-a000-000000000001', 'c3d4e5f6-0004-4000-a000-000000000001', 1, 'active', UTC_TIMESTAMP()), +('e5f6a7b8-1009-4000-a000-000000000001', 'd4e5f6a7-1009-4000-a000-000000000001', 'b2c3d4e5-1009-4000-a000-000000000001', 'c3d4e5f6-0004-4000-a000-000000000001', 1, 'active', UTC_TIMESTAMP()), +('e5f6a7b8-1010-4000-a000-000000000001', 'd4e5f6a7-1010-4000-a000-000000000001', 'b2c3d4e5-1010-4000-a000-000000000001', 'c3d4e5f6-0004-4000-a000-000000000001', 1, 'active', UTC_TIMESTAMP()); diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.html b/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.html index 48ebc2d..95ecd99 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.html +++ b/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.html @@ -11,11 +11,18 @@ } @if (displayMode() === 'card') { -
- @for (student of students(); track student.studentId) { - + @if (loaded() && students().length === 0) { +
+

You don't have any students entered yet.

+

Click + Add a Student in the upper right to get started.

+
+ } @else { +
+ @for (student of students(); track student.studentId) { + + } +
} -
} @else {

List view coming soon.

diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.scss b/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.scss index 29aa341..2f9f576 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.scss +++ b/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.scss @@ -45,4 +45,14 @@ gap: 1rem; overflow-y: auto; flex: 1; +} + +.empty-state { + text-align: center; + padding: 3rem 1.5rem; + color: #555; + font-size: 0.9375rem; + background: #fff; + border-radius: 8px; + border: 1px solid #ddd; } \ No newline at end of file diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.ts b/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.ts index 902d4fb..c012e7c 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.ts +++ b/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.ts @@ -27,6 +27,7 @@ export class StudentCardList { protected readonly students = signal([]); protected readonly displayMode = signal('card'); protected readonly showAddModal = signal(false); + protected readonly loaded = signal(false); public errorMessage = signal(null); @@ -73,6 +74,7 @@ export class StudentCardList { this.students.set(this.sortByIdentifier(data.payload || [])) } + this.loaded.set(true); }); } } diff --git a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.html b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.html index 449c5ca..4e0f528 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.html +++ b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.html @@ -2,8 +2,7 @@
- WinStudentGoalTracker - + {{ auth.schoolDistrictName() }} — {{ auth.programName() }}
diff --git a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.scss b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.scss index 8044d65..23b8e68 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.scss +++ b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.scss @@ -37,15 +37,13 @@ } .header-title { + flex: 1; + text-align: center; font-weight: 600; font-size: 1rem; color: #333; } -.spacer { - flex: 1; -} - .logout-btn { background: none; border: 1px solid #ddd; diff --git a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.ts b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.ts index 4026348..ad82dbb 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.ts +++ b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.ts @@ -41,7 +41,7 @@ export class Home implements OnDestroy { // ************************** Declarations ************************* - private readonly auth = inject(Auth); + protected readonly auth = inject(Auth); private readonly router = inject(Router); private readonly studentService = inject(StudentService); private readonly routeSub: Subscription; diff --git a/ui/winstudentgoaltracker/src/app/shared/classes/auth.models.ts b/ui/winstudentgoaltracker/src/app/shared/classes/auth.models.ts index f4f7cbc..0352587 100644 --- a/ui/winstudentgoaltracker/src/app/shared/classes/auth.models.ts +++ b/ui/winstudentgoaltracker/src/app/shared/classes/auth.models.ts @@ -33,6 +33,7 @@ export interface SelectProgramResponse { userId: string; email: string; programName: string; + schoolDistrictName: string; jwt: string; refreshToken: string; role: string; diff --git a/ui/winstudentgoaltracker/src/app/shared/services/auth.ts b/ui/winstudentgoaltracker/src/app/shared/services/auth.ts index c5b97a3..5172f65 100644 --- a/ui/winstudentgoaltracker/src/app/shared/services/auth.ts +++ b/ui/winstudentgoaltracker/src/app/shared/services/auth.ts @@ -34,6 +34,8 @@ export class Auth { private readonly _sessionToken = signal(this.loadSessionToken()); private readonly _programs = signal([]); private readonly _isRefreshing = signal(false); + private readonly _programName = signal(''); + private readonly _schoolDistrictName = signal(''); /** The currently authenticated user, parsed from the JWT. Null when logged out. */ readonly user = computed(() => { @@ -50,6 +52,12 @@ export class Auth { /** Programs returned by phase 1 for the user to choose from. */ readonly programs = this._programs.asReadonly(); + /** The name of the currently selected program. */ + readonly programName = this._programName.asReadonly(); + + /** The name of the school district for the currently selected program. */ + readonly schoolDistrictName = this._schoolDistrictName.asReadonly(); + /** Emits when a token refresh fails and the user is forced to re-login. */ readonly sessionExpired$ = new Subject(); @@ -187,6 +195,10 @@ export class Auth { private handleFullAuth(data: SelectProgramResponse): void { this.storeTokens(data.jwt, data.refreshToken); + // Store program context for header display + this._programName.set(data.programName); + this._schoolDistrictName.set(data.schoolDistrictName); + // Clear phase-1 artefacts localStorage.removeItem(STORAGE_KEYS.SESSION_TOKEN); this._sessionToken.set(null); @@ -229,6 +241,8 @@ export class Auth { this._sessionToken.set(null); this._programs.set([]); this._isRefreshing.set(false); + this._programName.set(''); + this._schoolDistrictName.set(''); } private loadSessionToken(): string | null {