Renamed some of the goal fields to align with business logic

This commit is contained in:
ivan-pelly
2026-03-14 16:30:17 -07:00
parent 7f91e2e557
commit 4d9b83c327
50 changed files with 279 additions and 149 deletions
+3 -2
View File
@@ -1,10 +1,11 @@
{ {
"$schema": "https://json.schemastore.org/launchsettings.json", "$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": { "profiles": {
"http": { "http": {
"commandName": "Project", "commandName": "Project",
"dotnetRunMessages": true, "dotnetRunMessages": true,
"launchBrowser": false, "launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:5123", "applicationUrl": "http://localhost:5123",
"environmentVariables": { "environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
+47
View File
@@ -405,4 +405,51 @@ public class AuthController : BaseController
Message = "Logged out successfully." Message = "Logged out successfully."
}); });
} }
// *****************************************************************
// Sets the password hash and salt for an existing user.
// Accepts a user ID and plaintext password, hashes it, and stores
// the result in the user table.
// *****************************************************************
[HttpPost("SetPassword")]
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<ResponseResult<object>>> SetPassword([FromBody] SetPasswordDto dto)
{
if (string.IsNullOrWhiteSpace(dto.UserId) || string.IsNullOrWhiteSpace(dto.Password))
{
return BadRequest(new ResponseResult<object>
{
Success = false,
Message = "User ID and password are required."
});
}
if (!Guid.TryParse(dto.UserId, out Guid userId))
{
return BadRequest(new ResponseResult<object>
{
Success = false,
Message = "Invalid user ID format."
});
}
var (hash, salt) = PasswordHasher.HashPassword(dto.Password);
var updated = await _userRepo.SetPasswordAsync(userId, hash, salt);
if (!updated)
{
return Ok(new ResponseResult<object>
{
Success = false,
Message = "User not found."
});
}
return Ok(new ResponseResult<object>
{
Success = true,
Message = "Password set successfully."
});
}
} }
@@ -2,8 +2,8 @@ namespace WinStudentGoalTracker.DataAccess;
public class CreateGoalDto public class CreateGoalDto
{ {
public string? Title { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public string? Category { get; set; } public string? Category { get; set; }
public string? Baseline { get; set; }
public Guid? GoalParentId { get; set; } public Guid? GoalParentId { get; set; }
} }
@@ -0,0 +1,7 @@
namespace WinStudentGoalTracker.DataAccess;
public class SetPasswordDto
{
public string? UserId { get; set; }
public string? Password { get; set; }
}
@@ -2,7 +2,7 @@ namespace WinStudentGoalTracker.DataAccess;
public class UpdateGoalDto public class UpdateGoalDto
{ {
public string? Title { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public string? Category { get; set; } public string? Category { get; set; }
public string? Baseline { get; set; }
} }
@@ -5,7 +5,7 @@ public class dbStudentBenchmarkRow
public string? StudentIdentifier { get; set; } public string? StudentIdentifier { get; set; }
public required Guid BenchmarkId { get; set; } public required Guid BenchmarkId { get; set; }
public required Guid GoalId { get; set; } public required Guid GoalId { get; set; }
public string? GoalTitle { get; set; } public string? GoalCategory { get; set; }
public string? Benchmark { get; set; } public string? Benchmark { get; set; }
public string? CreatedByName { get; set; } public string? CreatedByName { get; set; }
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
@@ -5,9 +5,9 @@ public class dbStudentGoalRow
public string? StudentIdentifier { get; set; } public string? StudentIdentifier { get; set; }
public required Guid GoalId { get; set; } public required Guid GoalId { get; set; }
public Guid? GoalParentId { get; set; } public Guid? GoalParentId { get; set; }
public string? Title { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public string? Category { get; set; } public string? Category { get; set; }
public string? Baseline { get; set; }
public int ProgressEventCount { get; set; } public int ProgressEventCount { get; set; }
public int BenchmarkCount { get; set; } public int BenchmarkCount { get; set; }
} }
@@ -144,9 +144,9 @@ public class StudentRepository
p_id_goal_parent = dto.GoalParentId?.ToString(), p_id_goal_parent = dto.GoalParentId?.ToString(),
p_id_student = idStudent.ToString(), p_id_student = idStudent.ToString(),
p_id_user_created = userId.ToString(), p_id_user_created = userId.ToString(),
p_title = dto.Title,
p_description = dto.Description, p_description = dto.Description,
p_category = dto.Category p_category = dto.Category,
p_baseline = dto.Baseline
}, },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
@@ -156,9 +156,9 @@ public class StudentRepository
{ {
GoalId = newGoalId, GoalId = newGoalId,
GoalParentId = dto.GoalParentId, GoalParentId = dto.GoalParentId,
Title = dto.Title,
Description = dto.Description, Description = dto.Description,
Category = dto.Category, Category = dto.Category,
Baseline = dto.Baseline,
ProgressEventCount = 0 ProgressEventCount = 0
}; };
} }
@@ -191,9 +191,9 @@ public class StudentRepository
{ {
GoalId = r.GoalId, GoalId = r.GoalId,
GoalParentId = r.GoalParentId, GoalParentId = r.GoalParentId,
Title = r.Title,
Description = r.Description, Description = r.Description,
Category = r.Category, Category = r.Category,
Baseline = r.Baseline,
ProgressEventCount = r.ProgressEventCount, ProgressEventCount = r.ProgressEventCount,
BenchmarkCount = r.BenchmarkCount BenchmarkCount = r.BenchmarkCount
}).ToList() }).ToList()
@@ -201,7 +201,7 @@ public class StudentRepository
} }
// ***************************************************************** // *****************************************************************
// Updates a goal's title, description, and category. // Updates a goal's description, category, and baseline.
// ***************************************************************** // *****************************************************************
public async Task<bool> UpdateGoalAsync(Guid goalId, UpdateGoalDto dto) public async Task<bool> UpdateGoalAsync(Guid goalId, UpdateGoalDto dto)
{ {
@@ -214,9 +214,9 @@ public class StudentRepository
p_id_goal_parent = (string?)null, p_id_goal_parent = (string?)null,
p_id_student = (string?)null, p_id_student = (string?)null,
p_id_user_created = (string?)null, p_id_user_created = (string?)null,
p_title = dto.Title,
p_description = dto.Description, p_description = dto.Description,
p_category = dto.Category p_category = dto.Category,
p_baseline = dto.Baseline
}, },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
return rowsAffected > 0; return rowsAffected > 0;
@@ -254,7 +254,7 @@ public class StudentRepository
{ {
BenchmarkId = r.BenchmarkId, BenchmarkId = r.BenchmarkId,
GoalId = r.GoalId, GoalId = r.GoalId,
GoalTitle = r.GoalTitle, GoalCategory = r.GoalCategory,
Benchmark = r.Benchmark, Benchmark = r.Benchmark,
CreatedByName = r.CreatedByName, CreatedByName = r.CreatedByName,
CreatedAt = r.CreatedAt, CreatedAt = r.CreatedAt,
@@ -43,4 +43,23 @@ public class UserRepository
new { p_id_user = idUser.ToString() }, new { p_id_user = idUser.ToString() },
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
} }
// *****************************************************************
// Updates the password hash and salt for the given user.
// Returns true if the user was found and updated.
// *****************************************************************
public async Task<bool> SetPasswordAsync(Guid userId, string passwordHash, string passwordSalt)
{
using var db = Connection;
var rowsAffected = await db.QuerySingleOrDefaultAsync<int>(
"sp_User_SetPassword",
new
{
p_id_user = userId.ToString(),
p_password_hash = passwordHash,
p_password_salt = passwordSalt
},
commandType: CommandType.StoredProcedure);
return rowsAffected > 0;
}
} }
@@ -4,7 +4,7 @@ public class StudentBenchmarkItem
{ {
public Guid BenchmarkId { get; set; } public Guid BenchmarkId { get; set; }
public Guid GoalId { get; set; } public Guid GoalId { get; set; }
public string? GoalTitle { get; set; } public string? GoalCategory { get; set; }
public string? Benchmark { get; set; } public string? Benchmark { get; set; }
public string? CreatedByName { get; set; } public string? CreatedByName { get; set; }
public DateTime CreatedAt { get; set; } public DateTime CreatedAt { get; set; }
@@ -4,9 +4,9 @@ public class StudentGoalItem
{ {
public Guid GoalId { get; set; } public Guid GoalId { get; set; }
public Guid? GoalParentId { get; set; } public Guid? GoalParentId { get; set; }
public string? Title { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public string? Category { get; set; } public string? Category { get; set; }
public string? Baseline { get; set; }
public int ProgressEventCount { get; set; } public int ProgressEventCount { get; set; }
public int BenchmarkCount { get; set; } public int BenchmarkCount { get; set; }
} }
@@ -5,7 +5,7 @@ BEGIN
s.`identifier` AS `studentIdentifier`, s.`identifier` AS `studentIdentifier`,
b.`id_benchmark` AS `benchmarkId`, b.`id_benchmark` AS `benchmarkId`,
b.`id_goal` AS `goalId`, b.`id_goal` AS `goalId`,
g.`title` AS `goalTitle`, g.`category` AS `goalCategory`,
b.`benchmark` AS `benchmark`, b.`benchmark` AS `benchmark`,
u.`name` AS `createdByName`, u.`name` AS `createdByName`,
b.`created_at` AS `createdAt`, b.`created_at` AS `createdAt`,
+1 -1
View File
@@ -5,9 +5,9 @@ BEGIN
goalId, goalId,
goalParentId, goalParentId,
studentId, studentId,
title,
description, description,
category, category,
baseline,
progressEventCount progressEventCount
FROM v_goal_card FROM v_goal_card
ORDER BY goalId; ORDER BY goalId;
+1 -1
View File
@@ -5,9 +5,9 @@ BEGIN
goalId, goalId,
goalParentId, goalParentId,
studentId, studentId,
title,
description, description,
category, category,
baseline,
progressEventCount progressEventCount
FROM v_goal_card FROM v_goal_card
WHERE goalId = p_id_goal WHERE goalId = p_id_goal
@@ -5,9 +5,9 @@ BEGIN
s.`identifier` AS `studentIdentifier`, s.`identifier` AS `studentIdentifier`,
vc.`goalId`, vc.`goalId`,
vc.`goalParentId`, vc.`goalParentId`,
vc.`title`,
vc.`description`, vc.`description`,
vc.`category`, vc.`category`,
vc.`baseline`,
vc.`progressEventCount`, vc.`progressEventCount`,
vc.`benchmarkCount` vc.`benchmarkCount`
FROM `v_goal_card` vc FROM `v_goal_card` vc
+5 -5
View File
@@ -4,9 +4,9 @@ CREATE DEFINER=`root`@`%` PROCEDURE `sp_Goal_Insert`(
IN p_id_goal_parent CHAR(36), IN p_id_goal_parent CHAR(36),
IN p_id_student CHAR(36), IN p_id_student CHAR(36),
IN p_id_user_created CHAR(36), IN p_id_user_created CHAR(36),
IN p_title VARCHAR(255),
IN p_description TEXT, IN p_description TEXT,
IN p_category VARCHAR(100) IN p_category VARCHAR(255),
IN p_baseline TEXT
) )
BEGIN BEGIN
INSERT INTO goal INSERT INTO goal
@@ -15,9 +15,9 @@ BEGIN
id_goal_parent, id_goal_parent,
id_student, id_student,
id_user_created, id_user_created,
title,
description, description,
category, category,
baseline,
created_at, created_at,
updated_at updated_at
) )
@@ -27,9 +27,9 @@ BEGIN
p_id_goal_parent, p_id_goal_parent,
p_id_student, p_id_student,
p_id_user_created, p_id_user_created,
p_title,
p_description, p_description,
p_category, p_category,
p_baseline,
UTC_TIMESTAMP(), UTC_TIMESTAMP(),
UTC_TIMESTAMP() UTC_TIMESTAMP()
); );
@@ -38,9 +38,9 @@ BEGIN
id_goal_parent, id_goal_parent,
id_student, id_student,
id_user_created, id_user_created,
title,
description, description,
category, category,
baseline,
created_at, created_at,
updated_at updated_at
FROM goal FROM goal
+3 -3
View File
@@ -4,9 +4,9 @@ CREATE DEFINER=`root`@`%` PROCEDURE `sp_Goal_Update`(
IN p_id_goal_parent CHAR(36), IN p_id_goal_parent CHAR(36),
IN p_id_student CHAR(36), IN p_id_student CHAR(36),
IN p_id_user_created CHAR(36), IN p_id_user_created CHAR(36),
IN p_title VARCHAR(255),
IN p_description TEXT, IN p_description TEXT,
IN p_category VARCHAR(100) IN p_category VARCHAR(255),
IN p_baseline TEXT
) )
BEGIN BEGIN
UPDATE goal UPDATE goal
@@ -14,9 +14,9 @@ BEGIN
id_goal_parent = COALESCE(p_id_goal_parent, id_goal_parent), id_goal_parent = COALESCE(p_id_goal_parent, id_goal_parent),
id_student = COALESCE(p_id_student, id_student), id_student = COALESCE(p_id_student, id_student),
id_user_created = COALESCE(p_id_user_created, id_user_created), id_user_created = COALESCE(p_id_user_created, id_user_created),
title = COALESCE(p_title, title),
description = COALESCE(p_description, description), description = COALESCE(p_description, description),
category = COALESCE(p_category, category), category = COALESCE(p_category, category),
baseline = COALESCE(p_baseline, baseline),
updated_at = UTC_TIMESTAMP() updated_at = UTC_TIMESTAMP()
WHERE id_goal = p_id_goal; WHERE id_goal = p_id_goal;
SELECT ROW_COUNT() AS rows_affected; SELECT ROW_COUNT() AS rows_affected;
@@ -0,0 +1,15 @@
DELIMITER ;;
CREATE DEFINER=`root`@`%` PROCEDURE `sp_User_SetPassword`(
IN p_id_user CHAR(36),
IN p_password_hash VARCHAR(255),
IN p_password_salt VARCHAR(255)
)
BEGIN
UPDATE user
SET password_hash = p_password_hash,
password_salt = p_password_salt,
password_updated_at = UTC_TIMESTAMP()
WHERE id_user = p_id_user;
SELECT ROW_COUNT() AS rows_affected;
END;;
DELIMITER ;
+2 -2
View File
@@ -3,9 +3,9 @@ CREATE TABLE `goal` (
`id_goal_parent` char(36) DEFAULT NULL, `id_goal_parent` char(36) DEFAULT NULL,
`id_student` char(36) DEFAULT NULL, `id_student` char(36) DEFAULT NULL,
`id_user_created` char(36) DEFAULT NULL, `id_user_created` char(36) DEFAULT NULL,
`title` varchar(255) DEFAULT NULL,
`description` text, `description` text,
`category` varchar(100) DEFAULT NULL, `category` varchar(255) DEFAULT NULL,
`baseline` text,
`created_at` timestamp NULL DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id_goal`), PRIMARY KEY (`id_goal`),
+2 -2
View File
@@ -1,7 +1,7 @@
CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `winstudentgoaltracker`.`v_goal_card` AS CREATE ALGORITHM=UNDEFINED DEFINER=`root`@`%` SQL SECURITY DEFINER VIEW `winstudentgoaltracker`.`v_goal_card` AS
select `g`.`id_goal` AS `goalId`,`g`.`id_goal_parent` AS `goalParentId`,`g`.`id_student` AS `studentId`,`g`.`title` AS `title`,`g`.`description` AS `description`,`g`.`category` AS `category`,count(distinct `pe`.`id_progress_event`) AS `progressEventCount`,count(distinct `b`.`id_benchmark`) AS `benchmarkCount` select `g`.`id_goal` AS `goalId`,`g`.`id_goal_parent` AS `goalParentId`,`g`.`id_student` AS `studentId`,`g`.`description` AS `description`,`g`.`category` AS `category`,`g`.`baseline` AS `baseline`,count(distinct `pe`.`id_progress_event`) AS `progressEventCount`,count(distinct `b`.`id_benchmark`) AS `benchmarkCount`
from ((`winstudentgoaltracker`.`goal` `g` from ((`winstudentgoaltracker`.`goal` `g`
left left
join `winstudentgoaltracker`.`progress_event` `pe` on((`pe`.`id_goal` = `g`.`id_goal`))) join `winstudentgoaltracker`.`progress_event` `pe` on((`pe`.`id_goal` = `g`.`id_goal`)))
left left
join `winstudentgoaltracker`.`benchmark` `b` on((`b`.`id_goal` = `g`.`id_goal`))) group by `g`.`id_goal`,`g`.`id_goal_parent`,`g`.`id_student`,`g`.`title`,`g`.`description`,`g`.`category`; join `winstudentgoaltracker`.`benchmark` `b` on((`b`.`id_goal` = `g`.`id_goal`))) group by `g`.`id_goal`,`g`.`id_goal_parent`,`g`.`id_student`,`g`.`description`,`g`.`category`,`g`.`baseline`;
@@ -8,18 +8,6 @@
<form class="modal-body" (ngSubmit)="onSubmit()" #goalForm="ngForm"> <form class="modal-body" (ngSubmit)="onSubmit()" #goalForm="ngForm">
<div class="field">
<label for="title">Title<span class="required">*</span></label>
<input
id="title"
type="text"
[(ngModel)]="form.title"
name="title"
required
placeholder="e.g. Improve reading comprehension"
/>
</div>
<div class="field"> <div class="field">
<label for="category">Category<span class="required">*</span></label> <label for="category">Category<span class="required">*</span></label>
<input <input
@@ -32,18 +20,6 @@
/> />
</div> </div>
@if (parentGoalOptions().length > 0) {
<div class="field">
<label for="goalParentId">Parent Goal <span class="optional">(optional)</span></label>
<select id="goalParentId" [(ngModel)]="form.goalParentId" name="goalParentId">
<option [ngValue]="null">None</option>
@for (goal of parentGoalOptions(); track goal.goalId) {
<option [ngValue]="goal.goalId">{{ goal.title }}</option>
}
</select>
</div>
}
<div class="field"> <div class="field">
<label for="description">Description</label> <label for="description">Description</label>
<textarea <textarea
@@ -55,6 +31,29 @@
></textarea> ></textarea>
</div> </div>
<div class="field">
<label for="baseline">Baseline</label>
<textarea
id="baseline"
[(ngModel)]="form.baseline"
name="baseline"
rows="3"
placeholder="Enter baseline..."
></textarea>
</div>
@if (parentGoalOptions().length > 0) {
<div class="field">
<label for="goalParentId">Parent Goal <span class="optional">(optional)</span></label>
<select id="goalParentId" [(ngModel)]="form.goalParentId" name="goalParentId">
<option [ngValue]="null">None</option>
@for (goal of parentGoalOptions(); track goal.goalId) {
<option [ngValue]="goal.goalId">{{ goal.category }}</option>
}
</select>
</div>
}
@if (errorMessage()) { @if (errorMessage()) {
<p class="error">{{ errorMessage() }}</p> <p class="error">{{ errorMessage() }}</p>
} }
@@ -31,9 +31,9 @@ export class AddGoalModal {
); );
protected form: CreateGoalDto = { protected form: CreateGoalDto = {
title: '',
description: '', description: '',
category: '', category: '',
baseline: '',
goalParentId: null, goalParentId: null,
}; };
@@ -9,14 +9,14 @@
<form class="modal-body" (ngSubmit)="onSubmit()" #studentForm="ngForm"> <form class="modal-body" (ngSubmit)="onSubmit()" #studentForm="ngForm">
<div class="field"> <div class="field">
<label for="identifier">Identifier</label> <label for="identifier">Student</label>
<input <input
id="identifier" id="identifier"
type="text" type="text"
[(ngModel)]="form.identifier" [(ngModel)]="form.identifier"
name="identifier" name="identifier"
required required
placeholder="e.g. Student123" placeholder="Initials or other non-personally identifiable label"
/> />
</div> </div>
@@ -16,7 +16,7 @@
<div class="detail-card"> <div class="detail-card">
<div class="field"> <div class="field">
<span class="field-label">Goal</span> <span class="field-label">Goal</span>
<span class="field-value">{{ goalTitle }}</span> <span class="field-value">{{ goalCategory }}</span>
</div> </div>
<div class="field"> <div class="field">
<label class="field-label" for="benchmarkText">Benchmark</label> <label class="field-label" for="benchmarkText">Benchmark</label>
@@ -47,7 +47,7 @@ export class BenchmarkCardFull implements OnDestroy {
private savedBenchmarkText = ''; private savedBenchmarkText = '';
// Read-only metadata // Read-only metadata
protected goalTitle = ''; protected goalCategory = '';
protected createdByName = ''; protected createdByName = '';
protected createdAt: Date | null = null; protected createdAt: Date | null = null;
protected updatedAt: Date | null = null; protected updatedAt: Date | null = null;
@@ -127,7 +127,7 @@ export class BenchmarkCardFull implements OnDestroy {
this.isNew.set(true); this.isNew.set(true);
this.benchmarkText = ''; this.benchmarkText = '';
this.savedBenchmarkText = ''; this.savedBenchmarkText = '';
this.loadGoalTitle(); this.loadGoalCategory();
this.loaded.set(true); this.loaded.set(true);
return; return;
} }
@@ -147,7 +147,7 @@ export class BenchmarkCardFull implements OnDestroy {
this.benchmarkText = bm.benchmark; this.benchmarkText = bm.benchmark;
this.savedBenchmarkText = bm.benchmark; this.savedBenchmarkText = bm.benchmark;
this.goalTitle = bm.goalTitle; this.goalCategory = bm.goalCategory;
this.createdByName = bm.createdByName; this.createdByName = bm.createdByName;
this.createdAt = bm.createdAt; this.createdAt = bm.createdAt;
this.updatedAt = bm.updatedAt; this.updatedAt = bm.updatedAt;
@@ -156,13 +156,13 @@ export class BenchmarkCardFull implements OnDestroy {
} }
// ***************************************************************** // *****************************************************************
// Loads the goal title for a new benchmark. // Loads the goal category for a new benchmark.
// ***************************************************************** // *****************************************************************
private loadGoalTitle() { private loadGoalCategory() {
this.studentService.getGoalsForStudent(this.studentId).then(result => { this.studentService.getGoalsForStudent(this.studentId).then(result => {
if (result.success && result.payload) { if (result.success && result.payload) {
const goal = result.payload.goals.find(g => g.goalId === this.goalId); const goal = result.payload.goals.find(g => g.goalId === this.goalId);
this.goalTitle = goal?.title ?? ''; this.goalCategory = goal?.category ?? '';
} }
}); });
} }
@@ -1,6 +1,6 @@
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<span class="goal-badge">{{ benchmark().goalTitle }}</span> <span class="goal-badge">{{ benchmark().goalCategory }}</span>
@if (benchmark().updatedAt) { @if (benchmark().updatedAt) {
<span class="date">Updated: {{ benchmark().updatedAt | date:'M/d/yy' }}</span> <span class="date">Updated: {{ benchmark().updatedAt | date:'M/d/yy' }}</span>
} @else { } @else {
@@ -4,8 +4,10 @@
<button class="toolbar-btn" (click)="onAddBenchmark()">+ Add a Benchmark</button> <button class="toolbar-btn" (click)="onAddBenchmark()">+ Add a Benchmark</button>
</div> </div>
@if (studentIdentifier()) { @if (goalCategory()) {
<h2 class="section-header">Benchmarks for {{ studentIdentifier() }}</h2> <h2 class="section-header">Benchmarks for {{ goalCategory() }}</h2>
} @else {
<h2 class="section-header">Benchmarks</h2>
} }
@if (errorMessage()) { @if (errorMessage()) {
@@ -13,8 +15,11 @@
} }
@if (benchmarks().length === 0 && !errorMessage()) { @if (benchmarks().length === 0 && !errorMessage()) {
<p class="empty-state">No benchmarks yet. Click <a class="empty-link" (click)="onAddBenchmark()">Add a Benchmark</a> to <div class="empty-state">
get started.</p> <p>Benchmarks are milestones on the way to achieving a goal. They are optional in this system.</p>
<p>No benchmarks yet.</p>
<p>Click <strong>+ Add a Benchmark</strong> in the upper right to get started.</p>
</div>
} @else { } @else {
<div class="card-grid"> <div class="card-grid">
@for (bm of benchmarks(); track bm.benchmarkId) { @for (bm of benchmarks(); track bm.benchmarkId) {
@@ -54,16 +54,13 @@
} }
.empty-state { .empty-state {
color: #888;
font-size: 0.9375rem;
margin: 2rem auto;
text-align: center; text-align: center;
} padding: 3rem 1.5rem;
color: #555;
.empty-link { font-size: 0.9375rem;
color: #4f46e5; background: #fff;
text-decoration: underline; border-radius: 8px;
cursor: pointer; border: 1px solid #ddd;
} }
.card-grid { .card-grid {
@@ -18,6 +18,7 @@ export class BenchmarkList {
this.studentId = this.route.snapshot.paramMap.get('studentId')!; this.studentId = this.route.snapshot.paramMap.get('studentId')!;
this.goalId = this.route.snapshot.paramMap.get('goalId') || ''; this.goalId = this.route.snapshot.paramMap.get('goalId') || '';
this.loadBenchmarks(); this.loadBenchmarks();
this.loadGoalCategory();
} }
// ************************** Declarations ************************* // ************************** Declarations *************************
@@ -28,7 +29,7 @@ export class BenchmarkList {
protected readonly studentId: string; protected readonly studentId: string;
protected readonly goalId: string; protected readonly goalId: string;
protected readonly studentIdentifier = signal<string | null>(null); protected readonly goalCategory = signal<string | null>(null);
protected readonly benchmarks = signal<BenchmarkDto[]>([]); protected readonly benchmarks = signal<BenchmarkDto[]>([]);
protected readonly errorMessage = signal<string | null>(null); protected readonly errorMessage = signal<string | null>(null);
@@ -43,22 +44,51 @@ export class BenchmarkList {
} }
onBack() { onBack() {
this.router.navigate(['/students', this.studentId, 'goals', this.goalId]); if (this.goalId) {
this.router.navigate(['/students', this.studentId, 'goals', this.goalId]);
} else {
this.router.navigate(['/students', this.studentId, 'goals']);
}
} }
// ********************** Support Procedures *********************** // ********************** Support Procedures ***********************
// ***************************************************************** // *****************************************************************
// Loads benchmarks for the student from the service. // Loads benchmarks for the student from the service. Also sets
// goalCategory from the first benchmark's data.
// ***************************************************************** // *****************************************************************
private loadBenchmarks() { private loadBenchmarks() {
this.studentService.getBenchmarksForStudent(this.studentId).then(data => { this.studentService.getBenchmarksForStudent(this.studentId).then(data => {
if (!data.success) { if (!data.success) {
this.errorMessage.set(data.message); this.errorMessage.set(data.message);
} else { } else {
this.studentIdentifier.set(data.payload?.studentIdentifier ?? null); const benchmarks = data.payload?.benchmarks ?? [];
this.benchmarks.set(data.payload?.benchmarks ?? []); this.benchmarks.set(benchmarks);
// Set the goal category from benchmark data if available
if (this.goalId && benchmarks.length > 0) {
const match = benchmarks.find(b => b.goalId === this.goalId);
if (match) this.goalCategory.set(match.goalCategory);
}
// If we still don't have a category, load it from the goals API
if (!this.goalCategory()) {
this.loadGoalCategory();
}
} }
}); });
} }
// *****************************************************************
// Loads the goal category from the goals API as a fallback when
// no benchmarks exist yet to extract it from.
// *****************************************************************
private loadGoalCategory() {
if (!this.goalId) return;
this.studentService.getGoalsForStudent(this.studentId).then(result => {
if (!result.success || !result.payload) return;
const goal = result.payload.goals.find(g => g.goalId === this.goalId);
this.goalCategory.set(goal?.category ?? null);
});
}
} }
@@ -1,5 +1,5 @@
<div class="toolbar"> <div class="toolbar">
<button class="toolbar-btn back-btn" (click)="onBack()">&#8593; Student</button> <button class="toolbar-btn back-btn" (click)="onBack()">&#8593; Goals</button>
<span class="toolbar-title">Goal Detail</span> <span class="toolbar-title">Goal Detail</span>
<span class="spacer"></span> <span class="spacer"></span>
</div> </div>
@@ -15,8 +15,8 @@
@if (loaded()) { @if (loaded()) {
<div class="detail-card"> <div class="detail-card">
<div class="field"> <div class="field">
<label class="field-label" for="title">Title</label> <label class="field-label" for="baseline">Baseline</label>
<input id="title" class="field-input" type="text" [(ngModel)]="title" /> <input id="baseline" class="field-input" type="text" [(ngModel)]="baseline" />
</div> </div>
<div class="field"> <div class="field">
<label class="field-label" for="description">Description</label> <label class="field-label" for="description">Description</label>
@@ -39,18 +39,18 @@ export class GoalCardFull implements OnDestroy {
protected readonly saving = signal(false); protected readonly saving = signal(false);
// Form fields // Form fields
protected title = '';
protected description = ''; protected description = '';
protected category = ''; protected category = '';
protected baseline = '';
// Read-only metadata // Read-only metadata
protected progressEventCount = 0; protected progressEventCount = 0;
protected benchmarkCount = 0; protected benchmarkCount = 0;
// Snapshot // Snapshot
private savedTitle = '';
private savedDescription = ''; private savedDescription = '';
private savedCategory = ''; private savedCategory = '';
private savedBaseline = '';
// ************************** Properties *************************** // ************************** Properties ***************************
@@ -58,9 +58,9 @@ export class GoalCardFull implements OnDestroy {
// Returns true if form values differ from the saved snapshot. // Returns true if form values differ from the saved snapshot.
// ***************************************************************** // *****************************************************************
hasChanges(): boolean { hasChanges(): boolean {
return this.title !== this.savedTitle return this.description !== this.savedDescription
|| this.description !== this.savedDescription || this.category !== this.savedCategory
|| this.category !== this.savedCategory; || this.baseline !== this.savedBaseline;
} }
// ************************ Public Methods ************************* // ************************ Public Methods *************************
@@ -76,17 +76,17 @@ export class GoalCardFull implements OnDestroy {
this.successMessage.set(null); this.successMessage.set(null);
const result = await this.studentService.updateGoal(this.studentId, this.goalId, { const result = await this.studentService.updateGoal(this.studentId, this.goalId, {
title: this.title,
description: this.description, description: this.description,
category: this.category, category: this.category,
baseline: this.baseline,
}); });
this.saving.set(false); this.saving.set(false);
if (result.success) { if (result.success) {
this.savedTitle = this.title;
this.savedDescription = this.description; this.savedDescription = this.description;
this.savedCategory = this.category; this.savedCategory = this.category;
this.savedBaseline = this.baseline;
this.successMessage.set('Changes saved.'); this.successMessage.set('Changes saved.');
} else { } else {
this.errorMessage.set(result.message); this.errorMessage.set(result.message);
@@ -97,15 +97,15 @@ export class GoalCardFull implements OnDestroy {
// Reverts form fields to the last-saved snapshot. // Reverts form fields to the last-saved snapshot.
// ***************************************************************** // *****************************************************************
onCancel() { onCancel() {
this.title = this.savedTitle;
this.description = this.savedDescription; this.description = this.savedDescription;
this.category = this.savedCategory; this.category = this.savedCategory;
this.baseline = this.savedBaseline;
this.errorMessage.set(null); this.errorMessage.set(null);
this.successMessage.set(null); this.successMessage.set(null);
} }
onBack() { onBack() {
this.router.navigate(['/students', this.studentId]); this.router.navigate(['/students', this.studentId, 'goals']);
} }
onProgressEvents() { onProgressEvents() {
@@ -139,15 +139,15 @@ export class GoalCardFull implements OnDestroy {
return; return;
} }
this.title = goal.title;
this.description = goal.description; this.description = goal.description;
this.category = goal.category; this.category = goal.category;
this.baseline = goal.baseline;
this.progressEventCount = goal.progressEventCount; this.progressEventCount = goal.progressEventCount;
this.benchmarkCount = goal.benchmarkCount; this.benchmarkCount = goal.benchmarkCount;
this.savedTitle = goal.title;
this.savedDescription = goal.description; this.savedDescription = goal.description;
this.savedCategory = goal.category; this.savedCategory = goal.category;
this.savedBaseline = goal.baseline;
this.loaded.set(true); this.loaded.set(true);
}); });
} }
@@ -4,7 +4,7 @@
<span class="event-count">{{ goal().progressEventCount }} events</span> <span class="event-count">{{ goal().progressEventCount }} events</span>
</div> </div>
<h3 class="title">{{ goal().title }}</h3> <h3 class="title">{{ goal().category }}</h3>
<p class="description">{{ goal().description }}</p> <p class="description">{{ goal().description }}</p>
<div class="card-footer"> <div class="card-footer">
@@ -1,6 +1,6 @@
:host { :host {
display: block; display: block;
width: 300px; width: 450px;
} }
.card { .card {
@@ -22,7 +22,10 @@
} }
@if (goals().length === 0 && !errorMessage()) { @if (goals().length === 0 && !errorMessage()) {
<p class="empty-state">No goals yet. Click <strong>+ Add a Goal</strong> to get started.</p> <div class="empty-state">
<p>No goals yet.</p>
<p>Click <strong>+ Add a Goal</strong> in the upper right to get started.</p>
</div>
} @else { } @else {
<div class="card-grid"> <div class="card-grid">
@for (goal of goals(); track goal.goalId) { @for (goal of goals(); track goal.goalId) {
@@ -66,10 +66,13 @@
} }
.empty-state { .empty-state {
color: #888;
font-size: 0.9375rem;
margin: 2rem auto;
text-align: center; text-align: center;
padding: 3rem 1.5rem;
color: #555;
font-size: 0.9375rem;
background: #fff;
border-radius: 8px;
border: 1px solid #ddd;
} }
.card-grid { .card-grid {
@@ -88,4 +91,5 @@
background: #fff; background: #fff;
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
flex-shrink: 0; flex-shrink: 0;
margin-top: auto;
} }
@@ -49,6 +49,7 @@ export class GoalList implements OnDestroy {
onGoalCreated(goal: StudentGoalItem) { onGoalCreated(goal: StudentGoalItem) {
this.goals.update(list => [...list, goal]); this.goals.update(list => [...list, goal]);
this.showAddModal.set(false); this.showAddModal.set(false);
this.studentService.notifyDataChanged();
} }
onModalCancelled() { onModalCancelled() {
@@ -7,10 +7,10 @@
<!-- <img class="hero-image" src="/slalomcropped.png" alt="Slalom" /> --> <!-- <img class="hero-image" src="/slalomcropped.png" alt="Slalom" /> -->
@if (studentIdentifier() && goalTitle()) { @if (studentIdentifier() && goalCategory()) {
<div class="header-row"> <div class="header-row">
<h2 class="section-header"> <h2 class="section-header">
Student: {{ studentIdentifier() }} &nbsp;&nbsp; Goal: {{ goalTitle() }} Student: {{ studentIdentifier() }} &nbsp;&nbsp; Goal: {{ goalCategory() }}
@if (isFiltered()) { @if (isFiltered()) {
<span class="filter-count">(showing {{ filteredEvents().length }} of {{ events().length }})</span> <span class="filter-count">(showing {{ filteredEvents().length }} of {{ events().length }})</span>
} }
@@ -20,7 +20,7 @@ export class ProgressList implements OnDestroy {
this.studentId = this.route.snapshot.paramMap.get('studentId')!; this.studentId = this.route.snapshot.paramMap.get('studentId')!;
this.goalId = this.route.snapshot.paramMap.get('goalId')!; this.goalId = this.route.snapshot.paramMap.get('goalId')!;
this.loadEvents(); this.loadEvents();
this.loadGoalTitle(); this.loadGoalCategory();
this.searchInput$.pipe(debounceTime(300)).subscribe(term => { this.searchInput$.pipe(debounceTime(300)).subscribe(term => {
this.searchTerm.set(term); this.searchTerm.set(term);
@@ -38,7 +38,7 @@ export class ProgressList implements OnDestroy {
private readonly searchInput$ = new Subject<string>(); private readonly searchInput$ = new Subject<string>();
protected readonly studentIdentifier = signal<string | null>(null); protected readonly studentIdentifier = signal<string | null>(null);
protected readonly goalTitle = signal<string | null>(null); protected readonly goalCategory = signal<string | null>(null);
protected readonly events = signal<ProgressEventDto[]>([]); protected readonly events = signal<ProgressEventDto[]>([]);
protected readonly errorMessage = signal<string | null>(null); protected readonly errorMessage = signal<string | null>(null);
protected readonly rawSearchText = signal(''); protected readonly rawSearchText = signal('');
@@ -118,16 +118,15 @@ export class ProgressList implements OnDestroy {
} }
// ***************************************************************** // *****************************************************************
// Loads the goal title from the student's goal list so the heading // Loads the goal category from the student's goal list so the heading
// can display "Progress for <goal title>". // can display "Progress for <goal category>".
// ***************************************************************** // *****************************************************************
private loadGoalTitle() { private loadGoalCategory() {
this.studentService.getGoalsForStudent(this.studentId).then(result => { this.studentService.getGoalsForStudent(this.studentId).then(result => {
if (result.success && result.payload) { if (!result.success || !result.payload) return;
this.studentIdentifier.set(result.payload.studentIdentifier); this.studentIdentifier.set(result.payload.studentIdentifier);
const goal = result.payload.goals.find(g => g.goalId === this.goalId); const goal = result.payload.goals.find(g => g.goalId === this.goalId);
this.goalTitle.set(goal?.title ?? null); this.goalCategory.set(goal?.category ?? null);
}
}); });
} }
} }
@@ -44,6 +44,7 @@ export class StudentCardList {
onStudentCreated(student: StudentCardDto) { onStudentCreated(student: StudentCardDto) {
this.students.update(list => this.sortByIdentifier([...list, student])); this.students.update(list => this.sortByIdentifier([...list, student]));
this.showAddModal.set(false); this.showAddModal.set(false);
this.studentService.notifyDataChanged();
} }
onModalCancelled() { onModalCancelled() {
@@ -106,6 +106,8 @@
overflow-y: auto; overflow-y: auto;
padding: 1.5rem; padding: 1.5rem;
background: #f5f5f5; background: #f5f5f5;
display: flex;
flex-direction: column;
} }
/* Footer */ /* Footer */
@@ -120,7 +120,7 @@ export class Home implements OnDestroy {
if (!result.success || !result.payload) return []; if (!result.success || !result.payload) return [];
return result.payload.goals.map(goal => ({ return result.payload.goals.map(goal => ({
label: goal.title, label: goal.category,
routerLink: ['/students', studentId, 'goals', goal.goalId], routerLink: ['/students', studentId, 'goals', goal.goalId],
childCount: 2, childCount: 2,
children: [ children: [
@@ -4,7 +4,7 @@
<button class="back-btn" (click)="onBack()">← Back</button> <button class="back-btn" (click)="onBack()">← Back</button>
<span class="student-name">{{ studentIdentifier() }}</span> <span class="student-name">{{ studentIdentifier() }}</span>
</div> </div>
<h2 class="goal-title">{{ goalTitle() }}</h2> <h2 class="goal-title">{{ goalCategory() }}</h2>
<!-- Error message --> <!-- Error message -->
@if (error()) { @if (error()) {
@@ -17,7 +17,7 @@ export class AddProgressEvent {
// ************************** Constructor ************************** // ************************** Constructor **************************
constructor() { constructor() {
this.goalTitle.set(this.route.snapshot.queryParamMap.get('goalTitle') ?? ''); this.goalCategory.set(this.route.snapshot.queryParamMap.get('goalCategory') ?? '');
this.studentIdentifier.set(this.route.snapshot.queryParamMap.get('studentIdentifier') ?? ''); this.studentIdentifier.set(this.route.snapshot.queryParamMap.get('studentIdentifier') ?? '');
this.studentId = this.route.snapshot.paramMap.get('studentId') ?? ''; this.studentId = this.route.snapshot.paramMap.get('studentId') ?? '';
this.goalId = this.route.snapshot.paramMap.get('goalId') ?? ''; this.goalId = this.route.snapshot.paramMap.get('goalId') ?? '';
@@ -32,7 +32,7 @@ export class AddProgressEvent {
private readonly studentId: string; private readonly studentId: string;
private readonly goalId: string; private readonly goalId: string;
protected readonly goalTitle = signal(''); protected readonly goalCategory = signal('');
protected readonly studentIdentifier = signal(''); protected readonly studentIdentifier = signal('');
protected readonly notes = signal(''); protected readonly notes = signal('');
protected readonly error = signal<string | null>(null); protected readonly error = signal<string | null>(null);
@@ -9,8 +9,8 @@
<h2 class="section-heading">Goals</h2> <h2 class="section-heading">Goals</h2>
<div class="goal-cards"> <div class="goal-cards">
@for (goal of data()?.goals; track goal.goalId) { @for (goal of data()?.goals; track goal.goalId) {
<div class="goal-card" (click)="onGoalClick(goal.goalId, goal.title)"> <div class="goal-card" (click)="onGoalClick(goal.goalId, goal.category)">
<span class="goal-title">{{ goal.title }}</span> <span class="goal-title">{{ goal.category }}</span>
<span class="event-count">{{ goal.progressEventCount }}</span> <span class="event-count">{{ goal.progressEventCount }}</span>
</div> </div>
} }
@@ -46,10 +46,10 @@ export class StudentGoals {
// ***************************************************************** // *****************************************************************
// Navigates to the add-progress-event page for the selected goal. // Navigates to the add-progress-event page for the selected goal.
// ***************************************************************** // *****************************************************************
onGoalClick(goalId: string, goalTitle: string) { onGoalClick(goalId: string, goalCategory: string) {
this.router.navigate( this.router.navigate(
['students', this.studentId, 'goals', goalId, 'add-event'], ['students', this.studentId, 'goals', goalId, 'add-event'],
{ queryParams: { goalTitle, studentIdentifier: this.data()?.studentIdentifier } }, { queryParams: { goalCategory, studentIdentifier: this.data()?.studentIdentifier } },
); );
} }
@@ -6,7 +6,7 @@ export interface StudentBenchmarkSummary {
export interface BenchmarkDto { export interface BenchmarkDto {
benchmarkId: string; benchmarkId: string;
goalId: string; goalId: string;
goalTitle: string; goalCategory: string;
benchmark: string; benchmark: string;
createdByName: string; createdByName: string;
createdAt: Date; createdAt: Date;
@@ -1,6 +1,6 @@
export interface CreateGoalDto { export interface CreateGoalDto {
title: string;
description: string; description: string;
category: string; category: string;
baseline: string;
goalParentId: string | null; goalParentId: string | null;
} }
@@ -6,9 +6,9 @@ export interface StudentGoalSummary {
export interface StudentGoalItem { export interface StudentGoalItem {
goalId: string; // goal.id_goal — char(36) goalId: string; // goal.id_goal — char(36)
goalParentId: string | null; goalParentId: string | null;
title: string; // goal.title — varchar(255)
description: string; // goal.description — text description: string; // goal.description — text
category: string; // goal.category — varchar(100) category: string; // goal.category — varchar(100)
baseline: string; // goal.baseline — text
progressEventCount: number; // count of progress_event rows for this goal progressEventCount: number; // count of progress_event rows for this goal
benchmarkCount: number; // count of benchmark rows for this goal benchmarkCount: number; // count of benchmark rows for this goal
} }
@@ -19,41 +19,41 @@ export class DummyStudentService {
'1': { '1': {
studentIdentifier: 'J.B', studentIdentifier: 'J.B',
goals: [ goals: [
{ goalId: 'g1', goalParentId: null, title: 'Improve reading comprehension', description: 'Work on main-idea identification and inference skills across fiction and nonfiction texts.', category: 'Academics', progressEventCount: 5, benchmarkCount: 2 }, { goalId: 'g1', goalParentId: null, description: 'Work on main-idea identification and inference skills across fiction and nonfiction texts.', category: 'Academics', baseline: '', progressEventCount: 5, benchmarkCount: 2 },
{ goalId: 'g2', goalParentId: null, title: 'Complete algebra module', description: 'Finish all units in the algebra course including linear equations and graphing.', category: 'Academics', progressEventCount: 2, benchmarkCount: 0 }, { goalId: 'g2', goalParentId: null, description: 'Finish all units in the algebra course including linear equations and graphing.', category: 'Academics', baseline: '', progressEventCount: 2, benchmarkCount: 0 },
{ goalId: 'g3', goalParentId: null, title: 'Weekly journal entries', description: 'Write a reflective journal entry each week to build writing fluency.', category: 'Communication', progressEventCount: 8, benchmarkCount: 1 }, { goalId: 'g3', goalParentId: null, description: 'Write a reflective journal entry each week to build writing fluency.', category: 'Communication', baseline: '', progressEventCount: 8, benchmarkCount: 1 },
], ],
}, },
'2': { '2': {
studentIdentifier: 'M.K', studentIdentifier: 'M.K',
goals: [ goals: [
{ goalId: 'g4', goalParentId: null, title: 'Pass certification exam', description: 'Prepare for and pass the industry certification exam by end of quarter.', category: 'Career Readiness', progressEventCount: 3, benchmarkCount: 0 }, { goalId: 'g4', goalParentId: null, description: 'Prepare for and pass the industry certification exam by end of quarter.', category: 'Career Readiness', baseline: '', progressEventCount: 3, benchmarkCount: 0 },
{ goalId: 'g5', goalParentId: null, title: 'Attendance above 90%', description: 'Maintain consistent attendance throughout the term.', category: 'Behavior', progressEventCount: 0, benchmarkCount: 0 }, { goalId: 'g5', goalParentId: null, description: 'Maintain consistent attendance throughout the term.', category: 'Behavior', baseline: '', progressEventCount: 0, benchmarkCount: 0 },
{ goalId: 'g6', goalParentId: null, title: 'Complete internship hours', description: 'Log the required 40 hours at the assigned internship site.', category: 'Career Readiness', progressEventCount: 12, benchmarkCount: 0 }, { goalId: 'g6', goalParentId: null, description: 'Log the required 40 hours at the assigned internship site.', category: 'Career Readiness', baseline: '', progressEventCount: 12, benchmarkCount: 0 },
{ goalId: 'g7', goalParentId: null, title: 'Portfolio project', description: 'Build a personal portfolio showcasing completed coursework and projects.', category: 'Career Readiness', progressEventCount: 1, benchmarkCount: 0 }, { goalId: 'g7', goalParentId: null, description: 'Build a personal portfolio showcasing completed coursework and projects.', category: 'Career Readiness', baseline: '', progressEventCount: 1, benchmarkCount: 0 },
], ],
}, },
'3': { '3': {
studentIdentifier: 'A.R', studentIdentifier: 'A.R',
goals: [ goals: [
{ goalId: 'g8', goalParentId: null, title: 'GED preparation', description: 'Complete practice tests and study modules for GED math and reading sections.', category: 'Academics', progressEventCount: 6, benchmarkCount: 0 }, { goalId: 'g8', goalParentId: null, description: 'Complete practice tests and study modules for GED math and reading sections.', category: 'Academics', baseline: '', progressEventCount: 6, benchmarkCount: 0 },
{ goalId: 'g9', goalParentId: null, title: 'Resume workshop', description: 'Attend the resume writing workshop and produce a final draft.', category: 'Career Readiness', progressEventCount: 0, benchmarkCount: 0 }, { goalId: 'g9', goalParentId: null, description: 'Attend the resume writing workshop and produce a final draft.', category: 'Career Readiness', baseline: '', progressEventCount: 0, benchmarkCount: 0 },
], ],
}, },
'4': { '4': {
studentIdentifier: 'T.W', studentIdentifier: 'T.W',
goals: [ goals: [
{ goalId: 'g10', goalParentId: null, title: 'Public speaking practice', description: 'Present in front of the class at least once per month.', category: 'Communication', progressEventCount: 4, benchmarkCount: 0 }, { goalId: 'g10', goalParentId: null, description: 'Present in front of the class at least once per month.', category: 'Communication', baseline: '', progressEventCount: 4, benchmarkCount: 0 },
{ goalId: 'g11', goalParentId: null, title: 'Math placement improvement', description: 'Move up one placement level in math by the end of the semester.', category: 'Academics', progressEventCount: 7, benchmarkCount: 0 }, { goalId: 'g11', goalParentId: null, description: 'Move up one placement level in math by the end of the semester.', category: 'Academics', baseline: '', progressEventCount: 7, benchmarkCount: 0 },
{ goalId: 'g12', goalParentId: null, title: 'Conflict resolution strategies', description: 'Learn and apply at least three de-escalation techniques.', category: 'Behavior', progressEventCount: 2, benchmarkCount: 0 }, { goalId: 'g12', goalParentId: null, description: 'Learn and apply at least three de-escalation techniques.', category: 'Behavior', baseline: '', progressEventCount: 2, benchmarkCount: 0 },
{ goalId: 'g13', goalParentId: null, title: 'Daily attendance streak', description: 'Achieve a 30-day unbroken attendance streak.', category: 'Behavior', progressEventCount: 0, benchmarkCount: 0 }, { goalId: 'g13', goalParentId: null, description: 'Achieve a 30-day unbroken attendance streak.', category: 'Behavior', baseline: '', progressEventCount: 0, benchmarkCount: 0 },
{ goalId: 'g14', goalParentId: null, title: 'Job shadow experience', description: 'Complete a job shadow day in a field of interest.', category: 'Career Readiness', progressEventCount: 1, benchmarkCount: 0 }, { goalId: 'g14', goalParentId: null, description: 'Complete a job shadow day in a field of interest.', category: 'Career Readiness', baseline: '', progressEventCount: 1, benchmarkCount: 0 },
], ],
}, },
'5': { '5': {
studentIdentifier: 'L.C', studentIdentifier: 'L.C',
goals: [ goals: [
{ goalId: 'g15', goalParentId: null, title: 'Improve typing speed', description: 'Reach 40 WPM with 95% accuracy on typing assessments.', category: 'Career Readiness', progressEventCount: 3, benchmarkCount: 0 }, { goalId: 'g15', goalParentId: null, description: 'Reach 40 WPM with 95% accuracy on typing assessments.', category: 'Career Readiness', baseline: '', progressEventCount: 3, benchmarkCount: 0 },
], ],
}, },
}; };
@@ -116,9 +116,9 @@ export class DummyStudentService {
const newGoal: StudentGoalItem = { const newGoal: StudentGoalItem = {
goalId: `g${Date.now()}`, goalId: `g${Date.now()}`,
goalParentId: null, goalParentId: null,
title: data.title,
description: data.description, description: data.description,
category: data.category, category: data.category,
baseline: data.baseline,
progressEventCount: 0, progressEventCount: 0,
benchmarkCount: 0, benchmarkCount: 0,
}; };
@@ -174,9 +174,9 @@ export class DummyStudentService {
} }
const benchmarks: BenchmarkDto[] = [ const benchmarks: BenchmarkDto[] = [
{ benchmarkId: 'bm1', goalId: 'g1', goalTitle: 'Improve reading comprehension', benchmark: 'Student will identify the main idea of a grade-level nonfiction passage with 80% accuracy.', createdByName: 'Jane Smith', createdAt: new Date('2026-02-15'), updatedAt: null }, { benchmarkId: 'bm1', goalId: 'g1', goalCategory: 'Academics', benchmark: 'Student will identify the main idea of a grade-level nonfiction passage with 80% accuracy.', createdByName: 'Jane Smith', createdAt: new Date('2026-02-15'), updatedAt: null },
{ benchmarkId: 'bm2', goalId: 'g1', goalTitle: 'Improve reading comprehension', benchmark: 'Student will make at least two supported inferences per reading session.', createdByName: 'Jane Smith', createdAt: new Date('2026-02-16'), updatedAt: new Date('2026-02-20') }, { benchmarkId: 'bm2', goalId: 'g1', goalCategory: 'Academics', benchmark: 'Student will make at least two supported inferences per reading session.', createdByName: 'Jane Smith', createdAt: new Date('2026-02-16'), updatedAt: new Date('2026-02-20') },
{ benchmarkId: 'bm3', goalId: 'g3', goalTitle: 'Weekly journal entries', benchmark: 'Student will complete a minimum of one paragraph (5 sentences) per journal entry.', createdByName: 'John Doe', createdAt: new Date('2026-02-18'), updatedAt: null }, { benchmarkId: 'bm3', goalId: 'g3', goalCategory: 'Communication', benchmark: 'Student will complete a minimum of one paragraph (5 sentences) per journal entry.', createdByName: 'John Doe', createdAt: new Date('2026-02-18'), updatedAt: null },
]; ];
return ApiResult.ok({ return ApiResult.ok({
@@ -216,9 +216,9 @@ export class StudentService {
} }
// ***************************************************************** // *****************************************************************
// Updates a goal's title, description, and category. // Updates a goal's description, category, and baseline.
// ***************************************************************** // *****************************************************************
async updateGoal(studentId: string, goalId: string, data: { title?: string; description?: string; category?: string }): Promise<ApiResult<any>> { async updateGoal(studentId: string, goalId: string, data: { description?: string; category?: string; baseline?: string }): Promise<ApiResult<any>> {
try { try {
const result = await firstValueFrom( const result = await firstValueFrom(
this.http.put<ResponseResult<any>>(`${this.base}/api/Student/${studentId}/goals/${goalId}`, data) this.http.put<ResponseResult<any>>(`${this.base}/api/Student/${studentId}/goals/${goalId}`, data)