lots of work done

This commit is contained in:
2026-03-02 16:23:29 -08:00
parent be4873283d
commit ef09a76bb4
25 changed files with 644 additions and 157 deletions
+75 -1
View File
@@ -97,11 +97,85 @@ public class StudentController : BaseController
}); });
} }
[HttpGet("{idStudent:guid}/goals")]
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")]
[ProducesResponseType(typeof(ResponseResult<StudentGoalSummary>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ResponseResult<StudentGoalSummary>), StatusCodes.Status404NotFound)]
public async Task<ActionResult<ResponseResult<StudentGoalSummary>>> GetGoals(Guid idStudent)
{
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
if (error is not null)
{
return error;
}
var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role);
if (!students.Select(s => s.StudentId).Contains(idStudent))
{
return NotFound(new ResponseResult<StudentGoalSummary>
{
Success = false,
Message = "Student not found."
});
}
var summary = await _studentRepository.GetGoalSummaryAsync(idStudent);
return Ok(new ResponseResult<StudentGoalSummary>
{
Success = true,
Message = "Goals retrieved successfully.",
Data = summary
});
}
[HttpPost("{idStudent:guid}/progress-event")]
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")]
[ProducesResponseType(typeof(ResponseResult), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ResponseResult), StatusCodes.Status404NotFound)]
[ProducesResponseType(typeof(ResponseResult), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<ResponseResult>> AddProgressEvent(Guid idStudent, [FromBody] AddProgressEventDto dto)
{
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
if (error is not null)
{
return error;
}
var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role);
if (!students.Select(s => s.StudentId).Contains(idStudent))
{
return NotFound(new ResponseResult
{
Success = false,
Message = "Student not found."
});
}
var created = await _studentRepository.AddProgressEventAsync(userId, dto);
if (!created)
{
return BadRequest(new ResponseResult
{
Success = false,
Message = "Unable to add progress event."
});
}
return StatusCode(StatusCodes.Status201Created, new ResponseResult
{
Success = true,
Message = "Progress event added successfully."
});
}
[HttpPost] [HttpPost]
[Authorize(Roles = $"{UserRoles.Teacher}")] [Authorize(Roles = $"{UserRoles.Teacher}")]
[ProducesResponseType(typeof(ResponseResult<StudentResponse>), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ResponseResult<StudentResponse>), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ResponseResult<StudentResponse>), StatusCodes.Status400BadRequest)] [ProducesResponseType(typeof(ResponseResult<StudentResponse>), StatusCodes.Status400BadRequest)]
public async Task<ActionResult<ResponseResult<StudentResponse>>> Create([FromBody] CreateStudentDto newStudentData) public async Task<ActionResult<ResponseResult<StudentResponse>>> CreateStudent([FromBody] CreateStudentDto newStudentData)
{ {
var (userId, email, programId, role, error) = GetProgramUserFromClaims(); var (userId, email, programId, role, error) = GetProgramUserFromClaims();
@@ -0,0 +1,8 @@
namespace WinStudentGoalTracker.DataAccess;
public class AddProgressEventDto
{
public Guid GoalId { get; set; }
public string? Content { get; set; }
public bool IsSensitive { get; set; }
}
@@ -0,0 +1,12 @@
namespace WinStudentGoalTracker.DataAccess;
public class dbStudentGoalRow
{
public string? StudentIdentifier { get; set; }
public required Guid GoalId { get; set; }
public Guid? GoalParentId { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public string? Category { get; set; }
public int ProgressEventCount { get; set; }
}
@@ -83,4 +83,48 @@ public class StudentRepository
commandType: CommandType.StoredProcedure); commandType: CommandType.StoredProcedure);
return rowsAffected > 0; return rowsAffected > 0;
} }
public async Task<bool> AddProgressEventAsync(Guid userId, AddProgressEventDto dto)
{
using var db = Connection;
var rowsAffected = await db.ExecuteAsync(
"sp_ProgressEvent_Insert",
new
{
p_id_progress_event = Guid.NewGuid().ToString(),
p_id_goal = dto.GoalId.ToString(),
p_id_user_created = userId.ToString(),
p_content = dto.Content,
p_is_sensitive = dto.IsSensitive ? 1 : 0
},
commandType: CommandType.StoredProcedure);
return rowsAffected > 0;
}
public async Task<StudentGoalSummary?> GetGoalSummaryAsync(Guid idStudent)
{
using var db = Connection;
var rows = await db.QueryAsync<dbStudentGoalRow>(
"sp_Goal_GetByStudentId",
new { p_id_student = idStudent.ToString() },
commandType: CommandType.StoredProcedure);
var list = rows.ToList();
if (list.Count == 0) return null;
return new StudentGoalSummary
{
StudentIdentifier = list[0].StudentIdentifier,
Goals = list.Select(r => new StudentGoalItem
{
GoalId = r.GoalId,
GoalParentId = r.GoalParentId,
Title = r.Title,
Description = r.Description,
Category = r.Category,
ProgressEventCount = r.ProgressEventCount
}).ToList()
};
}
} }
@@ -0,0 +1,11 @@
namespace WinStudentGoalTracker.Models;
public class StudentGoalItem
{
public Guid GoalId { get; set; }
public Guid? GoalParentId { get; set; }
public string? Title { get; set; }
public string? Description { get; set; }
public string? Category { get; set; }
public int ProgressEventCount { get; set; }
}
@@ -0,0 +1,7 @@
namespace WinStudentGoalTracker.Models;
public class StudentGoalSummary
{
public string? StudentIdentifier { get; set; }
public List<StudentGoalItem> Goals { get; set; } = [];
}
@@ -0,0 +1,17 @@
DELIMITER ;;
CREATE DEFINER=`root`@`%` PROCEDURE `sp_Goal_GetByStudentId`(IN p_id_student CHAR(36))
BEGIN
SELECT
s.`identifier` AS `studentIdentifier`,
vc.`goalId`,
vc.`goalParentId`,
vc.`title`,
vc.`description`,
vc.`category`,
vc.`progressEventCount`
FROM `v_goal_card` vc
INNER JOIN `student` s ON s.`id_student` = vc.`studentId`
WHERE vc.`studentId` = p_id_student
ORDER BY vc.`goalId`;
END;;
DELIMITER ;
+18
View File
@@ -0,0 +1,18 @@
CREATE OR REPLACE VIEW `v_goal_card` AS
SELECT
goal.`id_goal` AS `goalId`,
goal.`id_goal_parent` AS `goalParentId`,
goal.`id_student` AS `studentId`,
goal.`title` AS `title`,
goal.`description` AS `description`,
goal.`category` AS `category`,
COUNT(pe.`id_progress_event`) AS `progressEventCount`
FROM `goal`
LEFT JOIN `progress_event` pe ON pe.`id_goal` = goal.`id_goal`
GROUP BY
goal.`id_goal`,
goal.`id_goal_parent`,
goal.`id_student`,
goal.`title`,
goal.`description`,
goal.`category`;
+1 -1
View File
@@ -10,7 +10,7 @@ FROM `student` s
LEFT JOIN `goal` g LEFT JOIN `goal` g
ON g.`id_student` = s.`id_student` ON g.`id_student` = s.`id_student`
LEFT JOIN `progress_event` pe LEFT JOIN `progress_event` pe
ON pe.`id_student` = s.`id_student` ON pe.`id_goal` = g.`id_goal`
GROUP BY GROUP BY
s.`id_student`, s.`id_student`,
s.`identifier`, s.`identifier`,
@@ -0,0 +1,66 @@
<div class="overlay" (click)="onCancel()"></div>
<div class="modal">
<div class="modal-header">
<h2 class="modal-title">Add Student</h2>
<button class="close-btn" (click)="onCancel()" aria-label="Close">&times;</button>
</div>
<form class="modal-body" (ngSubmit)="onSubmit()" #studentForm="ngForm">
<div class="field">
<label for="identifier">Identifier</label>
<input
id="identifier"
type="text"
[(ngModel)]="form.identifier"
name="identifier"
required
placeholder="e.g. Student123"
/>
</div>
<div class="field">
<label for="programYear">Program Year</label>
<input
id="programYear"
type="number"
[(ngModel)]="form.programYear"
name="programYear"
placeholder="e.g. 2025"
/>
</div>
<div class="field">
<label for="enrollmentDate">Enrollment Date</label>
<input
id="enrollmentDate"
type="date"
[(ngModel)]="form.enrollmentDate"
name="enrollmentDate"
/>
</div>
<div class="field">
<label for="expectedGrad">Expected Graduation</label>
<input
id="expectedGrad"
type="date"
[(ngModel)]="form.expectedGrad"
name="expectedGrad"
/>
</div>
@if (errorMessage()) {
<p class="error">{{ errorMessage() }}</p>
}
<div class="modal-actions">
<button type="button" class="btn btn-secondary" (click)="onCancel()">Cancel</button>
<button type="submit" class="btn btn-primary" [disabled]="studentForm.invalid || isSubmitting()">
{{ isSubmitting() ? 'Saving...' : 'Add Student' }}
</button>
</div>
</form>
</div>
@@ -0,0 +1,130 @@
:host {
position: fixed;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.overlay {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.4);
}
.modal {
position: relative;
background: #fff;
border-radius: 10px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
width: 420px;
max-width: 95vw;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1.25rem 1.5rem 0;
}
.modal-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
font-size: 1.5rem;
line-height: 1;
cursor: pointer;
color: #666;
padding: 0;
}
.close-btn:hover {
color: #111;
}
.modal-body {
padding: 1.25rem 1.5rem 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.field {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
.field label {
font-size: 0.875rem;
font-weight: 500;
color: #333;
}
.field input {
padding: 0.5rem 0.75rem;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 0.9375rem;
outline: none;
}
.field input:focus {
border-color: #4f46e5;
box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.15);
}
.error {
font-size: 0.875rem;
color: #dc2626;
margin: 0;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 0.25rem;
}
.btn {
padding: 0.5rem 1.125rem;
border-radius: 6px;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
border: 1px solid transparent;
}
.btn-secondary {
background: transparent;
border-color: #ddd;
color: #555;
}
.btn-secondary:hover {
background: #f5f5f5;
}
.btn-primary {
background: #4f46e5;
color: #fff;
border-color: #4f46e5;
}
.btn-primary:hover:not(:disabled) {
background: #4338ca;
border-color: #4338ca;
}
.btn-primary:disabled {
opacity: 0.55;
cursor: not-allowed;
}
@@ -0,0 +1,61 @@
import { Component, inject, output, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CreateStudentDto } from '../../../shared/classes/create-student.dto';
import { StudentCardDto } from '../../../shared/classes/student-card.dto';
import { StudentService } from '../../../shared/services/student.service';
@Component({
selector: 'app-add-student-modal',
imports: [FormsModule],
templateUrl: './add-student-modal.html',
styleUrl: './add-student-modal.scss',
})
export class AddStudentModal {
// ************************** Constructor **************************
// ************************** Declarations *************************
private readonly studentService = inject(StudentService);
readonly studentCreated = output<StudentCardDto>();
readonly cancelled = output<void>();
protected readonly isSubmitting = signal(false);
protected readonly errorMessage = signal<string | null>(null);
protected form: CreateStudentDto = {
identifier: '',
programYear: null,
enrollmentDate: null,
expectedGrad: null,
};
// ************************** Properties ***************************
// ************************ Public Methods *************************
// ************************ Event Handlers *************************
async onSubmit() {
this.errorMessage.set(null);
this.isSubmitting.set(true);
const result = await this.studentService.createStudent(this.form);
this.isSubmitting.set(false);
if (!result.success) {
this.errorMessage.set(result.message);
return;
}
this.studentCreated.emit(result.payload!);
}
onCancel() {
this.cancelled.emit();
}
// ********************** Support Procedures ***********************
}
@@ -2,6 +2,13 @@
<button class="toolbar-btn" (click)="onAddStudent()">+ Add a Student</button> <button class="toolbar-btn" (click)="onAddStudent()">+ Add a Student</button>
</div> </div>
@if (showAddModal()) {
<app-add-student-modal
(studentCreated)="onStudentCreated($event)"
(cancelled)="onModalCancelled()"
/>
}
@if (displayMode() === 'card') { @if (displayMode() === 'card') {
<div class="card-grid"> <div class="card-grid">
@for (student of students(); track student.studentId) { @for (student of students(); track student.studentId) {
@@ -1,13 +1,15 @@
import { Component, inject, signal } from '@angular/core'; import { Component, inject, signal } from '@angular/core';
import { StudentCard } from '../student-card/student-card'; import { StudentCard } from '../student-card/student-card';
import { StudentService } from '../../../shared/services/dummy-student.service'; import { AddStudentModal } from '../add-student-modal/add-student-modal';
import { DummyStudentService } from '../../../shared/services/dummy-student.service';
import { StudentCardDto } from '../../../shared/classes/student-card.dto'; import { StudentCardDto } from '../../../shared/classes/student-card.dto';
import { StudentService } from '../../../shared/services/student.service';
export type DisplayMode = 'card' | 'list'; export type DisplayMode = 'card' | 'list';
@Component({ @Component({
selector: 'app-student-card-list', selector: 'app-student-card-list',
imports: [StudentCard], imports: [StudentCard, AddStudentModal],
templateUrl: './student-card-list.html', templateUrl: './student-card-list.html',
styleUrl: './student-card-list.scss', styleUrl: './student-card-list.scss',
}) })
@@ -24,6 +26,7 @@ export class StudentCardList {
private readonly studentService = inject(StudentService); private readonly studentService = inject(StudentService);
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);
public errorMessage = signal<String | null>(null); public errorMessage = signal<String | null>(null);
@@ -34,7 +37,16 @@ export class StudentCardList {
// ************************ Event Handlers ************************* // ************************ Event Handlers *************************
onAddStudent() { onAddStudent() {
// TODO: navigate to add-student form this.showAddModal.set(true);
}
onStudentCreated(student: StudentCardDto) {
this.students.update(list => [...list, student]);
this.showAddModal.set(false);
}
onModalCancelled() {
this.showAddModal.set(false);
} }
// ********************** Support Procedures *********************** // ********************** Support Procedures ***********************
@@ -43,7 +55,7 @@ export class StudentCardList {
// Loads students from the service and populates the students signal. // Loads students from the service and populates the students signal.
// ***************************************************************** // *****************************************************************
private loadStudents() { private loadStudents() {
this.studentService.getStudentCards().then(data => { this.studentService.getMyStudents().then(data => {
if(!data.success) if(!data.success)
{ {
@@ -2,7 +2,7 @@
<h2 class="identifier">🎓 {{ student().identifier }}</h2> <h2 class="identifier">🎓 {{ student().identifier }}</h2>
<div class="meta"> <div class="meta">
<span class="badge">Grad Date: {{ student().expectedGradDate }}</span> <span class="last-entry">Grad Date: {{ student().expectedGradDate | date:'M/d/yy'}}</span>
<span class="last-entry"> <span class="last-entry">
@if (student().lastEntryDate) { @if (student().lastEntryDate) {
Last entry: {{ student().lastEntryDate | date:'M/d/yy' }} Last entry: {{ student().lastEntryDate | date:'M/d/yy' }}
@@ -1,4 +1,4 @@
import { Component, input } from '@angular/core'; import { Component, computed, input } from '@angular/core';
import { DatePipe } from '@angular/common'; import { DatePipe } from '@angular/common';
import { StudentCardDto } from '../../../shared/classes/student-card.dto'; import { StudentCardDto } from '../../../shared/classes/student-card.dto';
@@ -2,8 +2,9 @@ import { Component, computed, inject, signal } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { HttpErrorResponse } from '@angular/common/http'; import { HttpErrorResponse } from '@angular/common/http';
import { DummySaveProgressEvent } from '../../../shared/services/dummy-save-progress-event.service';
import { describeHttpError } from '../../../shared/classes/http-errors'; import { describeHttpError } from '../../../shared/classes/http-errors';
import { DummyStudentService } from '../../../shared/services/dummy-student.service';
import { StudentService } from '../../../shared/services/student.service';
@Component({ @Component({
selector: 'app-add-progress-event', selector: 'app-add-progress-event',
@@ -26,7 +27,7 @@ export class AddProgressEvent {
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly saveService = inject(DummySaveProgressEvent); private readonly studentService = inject(StudentService);
private readonly studentId: string; private readonly studentId: string;
private readonly goalId: string; private readonly goalId: string;
@@ -66,7 +67,7 @@ export class AddProgressEvent {
this.saving.set(true); this.saving.set(true);
try { try {
const result = await this.saveService.save(this.studentId, this.goalId, this.notes().trim()); const result = await this.studentService.addProgressEvent(this.studentId, this.goalId, this.notes().trim());
this.saving.set(false); this.saving.set(false);
if (result.success) { if (result.success) {
this.router.navigate(['students', this.studentId, 'goals']); this.router.navigate(['students', this.studentId, 'goals']);
@@ -1,7 +1,8 @@
import { Component, inject, signal } from '@angular/core'; import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { DummyStudentGoalService } from '../../../shared/services/dummy-student-goal.service';
import { StudentGoalSummary } from '../../../shared/classes/student-goal'; import { StudentGoalSummary } from '../../../shared/classes/student-goal';
import { DummyStudentService } from '../../../shared/services/dummy-student.service';
import { StudentService } from '../../../shared/services/student.service';
@Component({ @Component({
selector: 'app-student-goals', selector: 'app-student-goals',
@@ -21,7 +22,7 @@ export class StudentGoals {
private readonly route = inject(ActivatedRoute); private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly goalService = inject(DummyStudentGoalService); private readonly studentService = inject(StudentService);
private readonly studentId = this.route.snapshot.paramMap.get('studentId') ?? ''; private readonly studentId = this.route.snapshot.paramMap.get('studentId') ?? '';
protected readonly data = signal<StudentGoalSummary | null>(null); protected readonly data = signal<StudentGoalSummary | null>(null);
@@ -60,7 +61,7 @@ export class StudentGoals {
private loadGoals() { private loadGoals() {
if (!this.studentId) return; if (!this.studentId) return;
this.goalService.getGoalsForStudent(this.studentId).then(result => { this.studentService.getGoalsForStudent(this.studentId).then(result => {
if (!result.success) if (!result.success)
{ {
@@ -1,7 +1,8 @@
import { Component, inject, signal } from '@angular/core'; import { Component, inject, signal } from '@angular/core';
import { StudentCard } from '../../components/student-card/student-card'; import { StudentCard } from '../../components/student-card/student-card';
import { StudentService } from '../../../shared/services/dummy-student.service'; import { DummyStudentService } from '../../../shared/services/dummy-student.service';
import { StudentCardDto } from '../../../shared/classes/student-card.dto'; import { StudentCardDto } from '../../../shared/classes/student-card.dto';
import { StudentService } from '../../../shared/services/student.service';
@Component({ @Component({
selector: 'app-students', selector: 'app-students',
@@ -36,7 +37,7 @@ export class Students {
// Loads the list of students assigned to the current user. // Loads the list of students assigned to the current user.
// ***************************************************************** // *****************************************************************
private loadStudents() { private loadStudents() {
this.studentService.getStudentsForUser().then(data => { this.studentService.getMyStudents().then(data => {
if (!data.success) if (!data.success)
{ {
@@ -0,0 +1,6 @@
export interface CreateStudentDto {
identifier: string;
programYear: number | null;
enrollmentDate: Date | null;
expectedGrad: Date | null;
}
@@ -5,6 +5,7 @@ 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;
title: string; // goal.title — varchar(255) 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)
@@ -1,34 +0,0 @@
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
// *****************************************************************
async save(studentId: string, goalId: string, content: string): Promise<ApiResult> {
return ApiResult.empty();
}
// ************************ Event Handlers *************************
// ********************** Support Procedures ***********************
}
@@ -1,89 +0,0 @@
import { Injectable } from '@angular/core';
import { ApiResult } from '../classes/api-result';
import { StudentGoalSummary } from '../classes/student-goal';
// *****************************************************************
// TODO: This dummy service should be replaced by StudentGoalService,
// which will fetch real data from the API.
// *****************************************************************
@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<string, StudentGoalSummary> = {
'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.
// *****************************************************************
async getGoalsForStudent(studentId: string): Promise<ApiResult<StudentGoalSummary | null>> {
var goals = this.data[studentId] ?? null;
if (goals === null)
{
return ApiResult.fail('Student not found');
}
return ApiResult.ok(goals);
}
// ************************ Event Handlers *************************
// ********************** Support Procedures ***********************
}
@@ -1,17 +1,60 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { StudentCardDto } from '../classes/student-card.dto'; import { StudentCardDto } from '../classes/student-card.dto';
import { ApiResult } from '../classes/api-result'; import { ApiResult } from '../classes/api-result';
import { StudentGoalSummary } from '../classes/student-goal';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class StudentService { export class DummyStudentService {
// ************************** Constructor ************************** // ************************** Constructor **************************
// ************************** Declarations ************************* // ************************** Declarations *************************
private readonly data: Record<string, StudentGoalSummary> = {
'1': {
studentIdentifier: 'J.B',
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 },
{ 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 },
{ goalId: 'g3', goalParentId: null, 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', goalParentId: null, title: 'Pass certification exam', description: 'Prepare for and pass the industry certification exam by end of quarter.', category: 'Career Readiness', progressEventCount: 3 },
{ goalId: 'g5', goalParentId: null, title: 'Attendance above 90%', description: 'Maintain consistent attendance throughout the term.', category: 'Behavior', progressEventCount: 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 },
{ goalId: 'g7', goalParentId: null, 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', goalParentId: null, title: 'GED preparation', description: 'Complete practice tests and study modules for GED math and reading sections.', category: 'Academics', progressEventCount: 6 },
{ goalId: 'g9', goalParentId: null, 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', goalParentId: null, title: 'Public speaking practice', description: 'Present in front of the class at least once per month.', category: 'Communication', progressEventCount: 4 },
{ 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 },
{ goalId: 'g12', goalParentId: null, title: 'Conflict resolution strategies', description: 'Learn and apply at least three de-escalation techniques.', category: 'Behavior', progressEventCount: 2 },
{ goalId: 'g13', goalParentId: null, title: 'Daily attendance streak', description: 'Achieve a 30-day unbroken attendance streak.', category: 'Behavior', progressEventCount: 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 },
],
},
'5': {
studentIdentifier: 'L.C',
goals: [
{ goalId: 'g15', goalParentId: null, title: 'Improve typing speed', description: 'Reach 40 WPM with 95% accuracy on typing assessments.', category: 'Career Readiness', progressEventCount: 3 },
],
},
};
// ************************** Properties *************************** // ************************** Properties ***************************
// ************************ Public Methods ************************* // ************************ Public Methods *************************
@@ -20,7 +63,7 @@ export class StudentService {
// Returns student card summaries. Currently returns dummy data // Returns student card summaries. Currently returns dummy data
// until the API endpoint is available. // until the API endpoint is available.
// ***************************************************************** // *****************************************************************
async getStudentCards(): Promise<ApiResult<StudentCardDto[]>> { async getMyStudents(): Promise<ApiResult<StudentCardDto[]>> {
var payload = [ var payload = [
{ {
studentId: '1', studentId: '1',
@@ -51,24 +94,22 @@ export class StudentService {
return ApiResult.ok(payload); return ApiResult.ok(payload);
} }
// ***************************************************************** async getGoalsForStudent(studentId: string): Promise<ApiResult<StudentGoalSummary | null>> {
// TODO: DUMMY DATA — Replace with getStudentsPerUser, which will var goals = this.data[studentId] ?? null;
// call GET /api/users/:id/students to return real data. if (goals === null)
// Returns students assigned to the current user with their {
// identifier, age, goal count, and progress event count. return ApiResult.fail('Student not found');
// *****************************************************************
async getStudentsForUser(): Promise<ApiResult<StudentCardDto[]>> {
var payload = [
{ studentId: '1', identifier: 'J.B', expectedGradDate: new Date('2027-02-27'), lastEntryDate: new Date('2026-02-21'), goalCount: 3, progressEventCount: 5 },
{ studentId: '2', identifier: 'M.K', expectedGradDate: new Date('2027-02-27'), lastEntryDate: new Date('2026-02-25'), goalCount: 4, progressEventCount: 8 },
{ studentId: '3', identifier: 'A.R', expectedGradDate: new Date('2027-02-27'), lastEntryDate: null, goalCount: 2, progressEventCount: 0 },
{ studentId: '4', identifier: 'T.W', expectedGradDate: new Date('2027-02-27'), lastEntryDate: new Date('2026-02-18'), goalCount: 5, progressEventCount: 12 },
{ studentId: '5', identifier: 'L.C', expectedGradDate: new Date('2027-02-27'), lastEntryDate: new Date('2026-02-27'), goalCount: 1, progressEventCount: 2 },
];
return ApiResult.ok(payload);
} }
return ApiResult.ok(goals);
}
async addProgressEvent(studentId: string, goalId: string, content: string): Promise<ApiResult> {
return ApiResult.empty();
}
// ************************ Event Handlers ************************* // ************************ Event Handlers *************************
// ********************** Support Procedures *********************** // ********************** Support Procedures ***********************
@@ -0,0 +1,92 @@
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { firstValueFrom } from 'rxjs';
import { environment } from '../../../environments/environment';
import { ApiResult } from '../classes/api-result';
import { ResponseResult } from '../classes/auth.models';
import { describeHttpError } from '../classes/http-errors';
import { CreateStudentDto } from '../classes/create-student.dto';
import { StudentCardDto } from '../classes/student-card.dto';
import { StudentGoalSummary } from '../classes/student-goal';
@Injectable({
providedIn: 'root',
})
export class StudentService {
// ************************** Constructor **************************
// ************************** Declarations *************************
private readonly http = inject(HttpClient);
private readonly base = environment.apiBaseUrl;
// ************************** Properties ***************************
// ************************ Public Methods *************************
// *****************************************************************
// Returns student card summaries for the authenticated user.
// *****************************************************************
async getMyStudents(): Promise<ApiResult<StudentCardDto[]>> {
try {
const result = await firstValueFrom(
this.http.get<ResponseResult<StudentCardDto[]>>(`${this.base}/api/Student/my`)
);
return result.success && result.data
? ApiResult.ok(result.data)
: ApiResult.fail(result.message);
} catch (error) {
return ApiResult.fail(describeHttpError(error as HttpErrorResponse));
}
}
// *****************************************************************
// Returns goal summary for a given student.
// *****************************************************************
async getGoalsForStudent(studentId: string): Promise<ApiResult<StudentGoalSummary | null>> {
try {
const result = await firstValueFrom(
this.http.get<ResponseResult<StudentGoalSummary>>(`${this.base}/api/Student/${studentId}/goals`)
);
return result.success && result.data
? ApiResult.ok(result.data)
: ApiResult.fail(result.message);
} catch (error) {
return ApiResult.fail(describeHttpError(error as HttpErrorResponse));
}
}
// *****************************************************************
// Creates a new student and returns the created student card.
// *****************************************************************
async createStudent(data: CreateStudentDto): Promise<ApiResult<StudentCardDto>> {
try {
const result = await firstValueFrom(
this.http.post<ResponseResult<StudentCardDto>>(`${this.base}/api/Student`, data)
);
return result.success && result.data
? ApiResult.ok(result.data)
: ApiResult.fail(result.message);
} catch (error) {
return ApiResult.fail(describeHttpError(error as HttpErrorResponse));
}
}
async addProgressEvent(studentId: string, goalId: string, content: string): Promise<ApiResult> {
try {
const result = await firstValueFrom(
this.http.post<ResponseResult<void>>(`${this.base}/api/Student/${studentId}/progress-event`, { goalId, content })
);
return result.success
? ApiResult.empty()
: ApiResult.fail(result.message);
} catch (error) {
return ApiResult.fail(describeHttpError(error as HttpErrorResponse));
}
}
// ************************ Event Handlers *************************
// ********************** Support Procedures ***********************
}