diff --git a/api/src/Controllers/StudentController.cs b/api/src/Controllers/StudentController.cs index 924233d..494db80 100644 --- a/api/src/Controllers/StudentController.cs +++ b/api/src/Controllers/StudentController.cs @@ -97,11 +97,85 @@ public class StudentController : BaseController }); } + [HttpGet("{idStudent:guid}/goals")] + [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status404NotFound)] + public async Task>> 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 + { + Success = false, + Message = "Student not found." + }); + } + + var summary = await _studentRepository.GetGoalSummaryAsync(idStudent); + + return Ok(new ResponseResult + { + 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> 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] [Authorize(Roles = $"{UserRoles.Teacher}")] [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status201Created)] [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status400BadRequest)] - public async Task>> Create([FromBody] CreateStudentDto newStudentData) + public async Task>> CreateStudent([FromBody] CreateStudentDto newStudentData) { var (userId, email, programId, role, error) = GetProgramUserFromClaims(); diff --git a/api/src/DataAccess/Models/DataTransferObjects/AddProgressEventDto.cs b/api/src/DataAccess/Models/DataTransferObjects/AddProgressEventDto.cs new file mode 100644 index 0000000..911e3cd --- /dev/null +++ b/api/src/DataAccess/Models/DataTransferObjects/AddProgressEventDto.cs @@ -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; } +} diff --git a/api/src/DataAccess/Models/DatabaseObjects/dbStudentGoalRow.cs b/api/src/DataAccess/Models/DatabaseObjects/dbStudentGoalRow.cs new file mode 100644 index 0000000..50e645f --- /dev/null +++ b/api/src/DataAccess/Models/DatabaseObjects/dbStudentGoalRow.cs @@ -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; } +} diff --git a/api/src/DataAccess/Repositories/StudentRepository.cs b/api/src/DataAccess/Repositories/StudentRepository.cs index b743fcb..c985e3f 100644 --- a/api/src/DataAccess/Repositories/StudentRepository.cs +++ b/api/src/DataAccess/Repositories/StudentRepository.cs @@ -83,4 +83,48 @@ public class StudentRepository commandType: CommandType.StoredProcedure); return rowsAffected > 0; } + + public async Task 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 GetGoalSummaryAsync(Guid idStudent) + { + using var db = Connection; + var rows = await db.QueryAsync( + "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() + }; + } + } diff --git a/api/src/Models/ResponseTypes/StudentGoalItem.cs b/api/src/Models/ResponseTypes/StudentGoalItem.cs new file mode 100644 index 0000000..5d6b7c1 --- /dev/null +++ b/api/src/Models/ResponseTypes/StudentGoalItem.cs @@ -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; } +} diff --git a/api/src/Models/ResponseTypes/StudentGoalSummary.cs b/api/src/Models/ResponseTypes/StudentGoalSummary.cs new file mode 100644 index 0000000..1d1e931 --- /dev/null +++ b/api/src/Models/ResponseTypes/StudentGoalSummary.cs @@ -0,0 +1,7 @@ +namespace WinStudentGoalTracker.Models; + +public class StudentGoalSummary +{ + public string? StudentIdentifier { get; set; } + public List Goals { get; set; } = []; +} diff --git a/db/Objects/procedures/sp_Goal_GetByStudentId.sql b/db/Objects/procedures/sp_Goal_GetByStudentId.sql new file mode 100644 index 0000000..9c968a7 --- /dev/null +++ b/db/Objects/procedures/sp_Goal_GetByStudentId.sql @@ -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 ; diff --git a/db/Objects/views/v_goal_card.sql b/db/Objects/views/v_goal_card.sql new file mode 100644 index 0000000..58fd4e4 --- /dev/null +++ b/db/Objects/views/v_goal_card.sql @@ -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`; diff --git a/db/Objects/views/v_student_card.sql b/db/Objects/views/v_student_card.sql index 5da95fa..3277fd7 100644 --- a/db/Objects/views/v_student_card.sql +++ b/db/Objects/views/v_student_card.sql @@ -10,7 +10,7 @@ FROM `student` s LEFT JOIN `goal` g ON g.`id_student` = s.`id_student` LEFT JOIN `progress_event` pe - ON pe.`id_student` = s.`id_student` + ON pe.`id_goal` = g.`id_goal` GROUP BY s.`id_student`, s.`identifier`, diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/add-student-modal/add-student-modal.html b/ui/winstudentgoaltracker/src/app/desktop/components/add-student-modal/add-student-modal.html new file mode 100644 index 0000000..ce8c23f --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/desktop/components/add-student-modal/add-student-modal.html @@ -0,0 +1,66 @@ +
+ + diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/add-student-modal/add-student-modal.scss b/ui/winstudentgoaltracker/src/app/desktop/components/add-student-modal/add-student-modal.scss new file mode 100644 index 0000000..b80ee9b --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/desktop/components/add-student-modal/add-student-modal.scss @@ -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; +} diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/add-student-modal/add-student-modal.ts b/ui/winstudentgoaltracker/src/app/desktop/components/add-student-modal/add-student-modal.ts new file mode 100644 index 0000000..5054058 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/desktop/components/add-student-modal/add-student-modal.ts @@ -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(); + readonly cancelled = output(); + + protected readonly isSubmitting = signal(false); + protected readonly errorMessage = signal(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 *********************** +} diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.html b/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.html index 79a45e3..767aa02 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.html +++ b/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.html @@ -2,6 +2,13 @@ +@if (showAddModal()) { + +} + @if (displayMode() === 'card') {
@for (student of students(); track student.studentId) { diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.ts b/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.ts index f28da62..7f79f1e 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.ts +++ b/ui/winstudentgoaltracker/src/app/desktop/components/student-card-list/student-card-list.ts @@ -1,13 +1,15 @@ import { Component, inject, signal } from '@angular/core'; 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 { StudentService } from '../../../shared/services/student.service'; export type DisplayMode = 'card' | 'list'; @Component({ selector: 'app-student-card-list', - imports: [StudentCard], + imports: [StudentCard, AddStudentModal], templateUrl: './student-card-list.html', styleUrl: './student-card-list.scss', }) @@ -24,6 +26,7 @@ export class StudentCardList { private readonly studentService = inject(StudentService); protected readonly students = signal([]); protected readonly displayMode = signal('card'); + protected readonly showAddModal = signal(false); public errorMessage = signal(null); @@ -34,7 +37,16 @@ export class StudentCardList { // ************************ Event Handlers ************************* 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 *********************** @@ -43,7 +55,7 @@ export class StudentCardList { // Loads students from the service and populates the students signal. // ***************************************************************** private loadStudents() { - this.studentService.getStudentCards().then(data => { + this.studentService.getMyStudents().then(data => { if(!data.success) { diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/student-card/student-card.html b/ui/winstudentgoaltracker/src/app/desktop/components/student-card/student-card.html index 54158b8..e8eef1a 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/student-card/student-card.html +++ b/ui/winstudentgoaltracker/src/app/desktop/components/student-card/student-card.html @@ -2,7 +2,7 @@

🎓 {{ student().identifier }}

- Grad Date: {{ student().expectedGradDate }} + Grad Date: {{ student().expectedGradDate | date:'M/d/yy'}} @if (student().lastEntryDate) { Last entry: {{ student().lastEntryDate | date:'M/d/yy' }} diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/student-card/student-card.ts b/ui/winstudentgoaltracker/src/app/desktop/components/student-card/student-card.ts index 42ba6b2..9e766f3 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/student-card/student-card.ts +++ b/ui/winstudentgoaltracker/src/app/desktop/components/student-card/student-card.ts @@ -1,4 +1,4 @@ -import { Component, input } from '@angular/core'; +import { Component, computed, input } from '@angular/core'; import { DatePipe } from '@angular/common'; import { StudentCardDto } from '../../../shared/classes/student-card.dto'; diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.ts b/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.ts index 57a7428..1e46745 100644 --- a/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.ts +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/add-progress-event/add-progress-event.ts @@ -2,8 +2,9 @@ import { Component, computed, inject, signal } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { FormsModule } from '@angular/forms'; import { HttpErrorResponse } from '@angular/common/http'; -import { DummySaveProgressEvent } from '../../../shared/services/dummy-save-progress-event.service'; import { describeHttpError } from '../../../shared/classes/http-errors'; +import { DummyStudentService } from '../../../shared/services/dummy-student.service'; +import { StudentService } from '../../../shared/services/student.service'; @Component({ selector: 'app-add-progress-event', @@ -26,7 +27,7 @@ export class AddProgressEvent { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); - private readonly saveService = inject(DummySaveProgressEvent); + private readonly studentService = inject(StudentService); private readonly studentId: string; private readonly goalId: string; @@ -66,7 +67,7 @@ export class AddProgressEvent { this.saving.set(true); 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); if (result.success) { this.router.navigate(['students', this.studentId, 'goals']); diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.ts b/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.ts index 80150b6..bbfa4ae 100644 --- a/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.ts +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/student-goals/student-goals.ts @@ -1,7 +1,8 @@ import { Component, inject, signal } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { DummyStudentGoalService } from '../../../shared/services/dummy-student-goal.service'; import { StudentGoalSummary } from '../../../shared/classes/student-goal'; +import { DummyStudentService } from '../../../shared/services/dummy-student.service'; +import { StudentService } from '../../../shared/services/student.service'; @Component({ selector: 'app-student-goals', @@ -21,7 +22,7 @@ export class StudentGoals { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); - private readonly goalService = inject(DummyStudentGoalService); + private readonly studentService = inject(StudentService); private readonly studentId = this.route.snapshot.paramMap.get('studentId') ?? ''; protected readonly data = signal(null); @@ -60,7 +61,7 @@ export class StudentGoals { private loadGoals() { if (!this.studentId) return; - this.goalService.getGoalsForStudent(this.studentId).then(result => { + this.studentService.getGoalsForStudent(this.studentId).then(result => { if (!result.success) { diff --git a/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.ts b/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.ts index 31331a3..f9bb9dd 100644 --- a/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.ts +++ b/ui/winstudentgoaltracker/src/app/mobile/pages/students/students.ts @@ -1,7 +1,8 @@ import { Component, inject, signal } from '@angular/core'; 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 { StudentService } from '../../../shared/services/student.service'; @Component({ selector: 'app-students', @@ -36,7 +37,7 @@ export class Students { // Loads the list of students assigned to the current user. // ***************************************************************** private loadStudents() { - this.studentService.getStudentsForUser().then(data => { + this.studentService.getMyStudents().then(data => { if (!data.success) { diff --git a/ui/winstudentgoaltracker/src/app/shared/classes/create-student.dto.ts b/ui/winstudentgoaltracker/src/app/shared/classes/create-student.dto.ts new file mode 100644 index 0000000..d1416ab --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/shared/classes/create-student.dto.ts @@ -0,0 +1,6 @@ +export interface CreateStudentDto { + identifier: string; + programYear: number | null; + enrollmentDate: Date | null; + expectedGrad: Date | null; +} diff --git a/ui/winstudentgoaltracker/src/app/shared/classes/student-goal.ts b/ui/winstudentgoaltracker/src/app/shared/classes/student-goal.ts index 6c982b3..306950b 100644 --- a/ui/winstudentgoaltracker/src/app/shared/classes/student-goal.ts +++ b/ui/winstudentgoaltracker/src/app/shared/classes/student-goal.ts @@ -5,6 +5,7 @@ export interface StudentGoalSummary { export interface StudentGoalItem { goalId: string; // goal.id_goal — char(36) + goalParentId: string | null; title: string; // goal.title — varchar(255) description: string; // goal.description — text category: string; // goal.category — varchar(100) diff --git a/ui/winstudentgoaltracker/src/app/shared/services/dummy-save-progress-event.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/dummy-save-progress-event.service.ts deleted file mode 100644 index a8d15e9..0000000 --- a/ui/winstudentgoaltracker/src/app/shared/services/dummy-save-progress-event.service.ts +++ /dev/null @@ -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 { - return ApiResult.empty(); - } - - // ************************ Event Handlers ************************* - - // ********************** Support Procedures *********************** -} diff --git a/ui/winstudentgoaltracker/src/app/shared/services/dummy-student-goal.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/dummy-student-goal.service.ts deleted file mode 100644 index f1db355..0000000 --- a/ui/winstudentgoaltracker/src/app/shared/services/dummy-student-goal.service.ts +++ /dev/null @@ -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 = { - '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> { - var goals = this.data[studentId] ?? null; - if (goals === null) - { - return ApiResult.fail('Student not found'); - } - - return ApiResult.ok(goals); - - } - - // ************************ Event Handlers ************************* - - // ********************** Support Procedures *********************** -} diff --git a/ui/winstudentgoaltracker/src/app/shared/services/dummy-student.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/dummy-student.service.ts index 2717d32..ef3b7a0 100644 --- a/ui/winstudentgoaltracker/src/app/shared/services/dummy-student.service.ts +++ b/ui/winstudentgoaltracker/src/app/shared/services/dummy-student.service.ts @@ -1,17 +1,60 @@ import { Injectable } from '@angular/core'; -import { Observable, of } from 'rxjs'; import { StudentCardDto } from '../classes/student-card.dto'; import { ApiResult } from '../classes/api-result'; +import { StudentGoalSummary } from '../classes/student-goal'; @Injectable({ providedIn: 'root', }) -export class StudentService { +export class DummyStudentService { // ************************** Constructor ************************** // ************************** Declarations ************************* + private readonly data: Record = { + '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 *************************** // ************************ Public Methods ************************* @@ -20,7 +63,7 @@ export class StudentService { // Returns student card summaries. Currently returns dummy data // until the API endpoint is available. // ***************************************************************** - async getStudentCards(): Promise> { + async getMyStudents(): Promise> { var payload = [ { studentId: '1', @@ -51,24 +94,22 @@ export class StudentService { return ApiResult.ok(payload); } - // ***************************************************************** - // TODO: DUMMY DATA — Replace with getStudentsPerUser, which will - // call GET /api/users/:id/students to return real data. - // Returns students assigned to the current user with their - // identifier, age, goal count, and progress event count. - // ***************************************************************** - async getStudentsForUser(): Promise> { - 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 }, - ]; + async getGoalsForStudent(studentId: string): Promise> { + var goals = this.data[studentId] ?? null; + if (goals === null) + { + return ApiResult.fail('Student not found'); + } + + return ApiResult.ok(goals); - return ApiResult.ok(payload); } + async addProgressEvent(studentId: string, goalId: string, content: string): Promise { + return ApiResult.empty(); + } + + // ************************ Event Handlers ************************* // ********************** Support Procedures *********************** diff --git a/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts new file mode 100644 index 0000000..f225aba --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts @@ -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> { + try { + const result = await firstValueFrom( + this.http.get>(`${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> { + try { + const result = await firstValueFrom( + this.http.get>(`${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> { + try { + const result = await firstValueFrom( + this.http.post>(`${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 { + try { + const result = await firstValueFrom( + this.http.post>(`${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 *********************** +}