mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 05:17:41 +00:00
Latest
This commit is contained in:
@@ -204,6 +204,7 @@ public class AuthController : BaseController
|
|||||||
UserId = programUser.IdUser,
|
UserId = programUser.IdUser,
|
||||||
Email = programUser.Email!,
|
Email = programUser.Email!,
|
||||||
ProgramName = programUser.ProgramName!,
|
ProgramName = programUser.ProgramName!,
|
||||||
|
SchoolDistrictName = programUser.SchoolDistrictName ?? "",
|
||||||
Jwt = accessToken,
|
Jwt = accessToken,
|
||||||
RefreshToken = fullRefreshToken,
|
RefreshToken = fullRefreshToken,
|
||||||
Role = programUser.RoleInternalName,
|
Role = programUser.RoleInternalName,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ public class dbProgramUser
|
|||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public required Guid IdProgram { get; set; }
|
public required Guid IdProgram { get; set; }
|
||||||
public string? ProgramName { get; set; }
|
public string? ProgramName { get; set; }
|
||||||
|
public string? SchoolDistrictName { get; set; }
|
||||||
public required string RoleInternalName { get; set; }
|
public required string RoleInternalName { get; set; }
|
||||||
public required string RoleDisplayName { get; set; }
|
public required string RoleDisplayName { get; set; }
|
||||||
public string? Status { get; set; }
|
public string? Status { get; set; }
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public class SelectProgramResponse
|
|||||||
public Guid UserId { get; set; }
|
public Guid UserId { get; set; }
|
||||||
public required string Email { get; set; }
|
public required string Email { get; set; }
|
||||||
public required string ProgramName { get; set; }
|
public required string ProgramName { get; set; }
|
||||||
|
public required string SchoolDistrictName { get; set; }
|
||||||
public required string Jwt { get; set; }
|
public required string Jwt { get; set; }
|
||||||
public required string RefreshToken { get; set; }
|
public required string RefreshToken { get; set; }
|
||||||
public required string Role { get; set; }
|
public required string Role { get; set; }
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ BEGIN
|
|||||||
u.name,
|
u.name,
|
||||||
up.id_program,
|
up.id_program,
|
||||||
p.name AS program_name,
|
p.name AS program_name,
|
||||||
|
sd.name AS school_district_name,
|
||||||
r.internal_name AS role_internal_name,
|
r.internal_name AS role_internal_name,
|
||||||
r.name AS role_display_name,
|
r.name AS role_display_name,
|
||||||
up.status
|
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 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 role r ON up.id_role = r.id_role
|
||||||
JOIN program p ON up.id_program = p.id_program
|
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
|
WHERE u.id_user = p_id_user
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
END;;
|
END;;
|
||||||
|
|||||||
@@ -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());
|
||||||
+7
@@ -11,11 +11,18 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (displayMode() === 'card') {
|
@if (displayMode() === 'card') {
|
||||||
|
@if (loaded() && students().length === 0) {
|
||||||
|
<div class="empty-state">
|
||||||
|
<p>You don't have any students entered yet.</p>
|
||||||
|
<p>Click <strong>+ Add a Student</strong> in the upper right to get started.</p>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
<div class="card-grid">
|
<div class="card-grid">
|
||||||
@for (student of students(); track student.studentId) {
|
@for (student of students(); track student.studentId) {
|
||||||
<app-student-card [student]="student" />
|
<app-student-card [student]="student" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
} @else {
|
} @else {
|
||||||
<!-- List mode — to be implemented -->
|
<!-- List mode — to be implemented -->
|
||||||
<p>List view coming soon.</p>
|
<p>List view coming soon.</p>
|
||||||
|
|||||||
+10
@@ -46,3 +46,13 @@
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
flex: 1;
|
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;
|
||||||
|
}
|
||||||
+2
@@ -27,6 +27,7 @@ export class StudentCardList {
|
|||||||
protected readonly students = signal<StudentCardDto[]>([]);
|
protected readonly students = signal<StudentCardDto[]>([]);
|
||||||
protected readonly displayMode = signal<DisplayMode>('card');
|
protected readonly displayMode = signal<DisplayMode>('card');
|
||||||
protected readonly showAddModal = signal(false);
|
protected readonly showAddModal = signal(false);
|
||||||
|
protected readonly loaded = signal(false);
|
||||||
|
|
||||||
public errorMessage = signal<String | null>(null);
|
public errorMessage = signal<String | null>(null);
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ export class StudentCardList {
|
|||||||
this.students.set(this.sortByIdentifier(data.payload || []))
|
this.students.set(this.sortByIdentifier(data.payload || []))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loaded.set(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<button class="menu-toggle" (click)="onToggleSidebar()">☰</button>
|
<button class="menu-toggle" (click)="onToggleSidebar()">☰</button>
|
||||||
<span class="header-title">WinStudentGoalTracker</span>
|
<span class="header-title">{{ auth.schoolDistrictName() }} — {{ auth.programName() }}</span>
|
||||||
<span class="spacer"></span>
|
|
||||||
<button class="logout-btn" (click)="onLogout()">Log Out</button>
|
<button class="logout-btn" (click)="onLogout()">Log Out</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -37,15 +37,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.header-title {
|
.header-title {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: #333;
|
color: #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.spacer {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logout-btn {
|
.logout-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export class Home implements OnDestroy {
|
|||||||
|
|
||||||
// ************************** Declarations *************************
|
// ************************** Declarations *************************
|
||||||
|
|
||||||
private readonly auth = inject(Auth);
|
protected readonly auth = inject(Auth);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
private readonly studentService = inject(StudentService);
|
private readonly studentService = inject(StudentService);
|
||||||
private readonly routeSub: Subscription;
|
private readonly routeSub: Subscription;
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ export interface SelectProgramResponse {
|
|||||||
userId: string;
|
userId: string;
|
||||||
email: string;
|
email: string;
|
||||||
programName: string;
|
programName: string;
|
||||||
|
schoolDistrictName: string;
|
||||||
jwt: string;
|
jwt: string;
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ export class Auth {
|
|||||||
private readonly _sessionToken = signal<string | null>(this.loadSessionToken());
|
private readonly _sessionToken = signal<string | null>(this.loadSessionToken());
|
||||||
private readonly _programs = signal<UserProgramSummary[]>([]);
|
private readonly _programs = signal<UserProgramSummary[]>([]);
|
||||||
private readonly _isRefreshing = signal(false);
|
private readonly _isRefreshing = signal(false);
|
||||||
|
private readonly _programName = signal<string>('');
|
||||||
|
private readonly _schoolDistrictName = signal<string>('');
|
||||||
|
|
||||||
/** The currently authenticated user, parsed from the JWT. Null when logged out. */
|
/** The currently authenticated user, parsed from the JWT. Null when logged out. */
|
||||||
readonly user = computed<AuthUser | null>(() => {
|
readonly user = computed<AuthUser | null>(() => {
|
||||||
@@ -50,6 +52,12 @@ export class Auth {
|
|||||||
/** Programs returned by phase 1 for the user to choose from. */
|
/** Programs returned by phase 1 for the user to choose from. */
|
||||||
readonly programs = this._programs.asReadonly();
|
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. */
|
/** Emits when a token refresh fails and the user is forced to re-login. */
|
||||||
readonly sessionExpired$ = new Subject<void>();
|
readonly sessionExpired$ = new Subject<void>();
|
||||||
|
|
||||||
@@ -187,6 +195,10 @@ export class Auth {
|
|||||||
private handleFullAuth(data: SelectProgramResponse): void {
|
private handleFullAuth(data: SelectProgramResponse): void {
|
||||||
this.storeTokens(data.jwt, data.refreshToken);
|
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
|
// Clear phase-1 artefacts
|
||||||
localStorage.removeItem(STORAGE_KEYS.SESSION_TOKEN);
|
localStorage.removeItem(STORAGE_KEYS.SESSION_TOKEN);
|
||||||
this._sessionToken.set(null);
|
this._sessionToken.set(null);
|
||||||
@@ -229,6 +241,8 @@ export class Auth {
|
|||||||
this._sessionToken.set(null);
|
this._sessionToken.set(null);
|
||||||
this._programs.set([]);
|
this._programs.set([]);
|
||||||
this._isRefreshing.set(false);
|
this._isRefreshing.set(false);
|
||||||
|
this._programName.set('');
|
||||||
|
this._schoolDistrictName.set('');
|
||||||
}
|
}
|
||||||
|
|
||||||
private loadSessionToken(): string | null {
|
private loadSessionToken(): string | null {
|
||||||
|
|||||||
Reference in New Issue
Block a user