mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 08:47:42 +00:00
latest
This commit is contained in:
@@ -130,6 +130,57 @@ public class StudentController : BaseController
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpPost("{idStudent:guid}/goals")]
|
||||||
|
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin}")]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<StudentGoalItem>), StatusCodes.Status201Created)]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<StudentGoalItem>), StatusCodes.Status404NotFound)]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<StudentGoalItem>), StatusCodes.Status400BadRequest)]
|
||||||
|
public async Task<ActionResult<ResponseResult<StudentGoalItem>>> CreateGoal(Guid idStudent, [FromBody] CreateGoalDto 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<StudentGoalItem>
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Student not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PermissionService.IsAllowed(role, EntityType.Goal, PermissionAction.Create))
|
||||||
|
{
|
||||||
|
return BadRequest(new ResponseResult<StudentGoalItem>
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Unable to create goal."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var created = await _studentRepository.InsertGoalAsync(idStudent, userId, dto);
|
||||||
|
if (created is null)
|
||||||
|
{
|
||||||
|
return BadRequest(new ResponseResult<StudentGoalItem>
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Unable to create goal."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusCode(StatusCodes.Status201Created, new ResponseResult<StudentGoalItem>
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = "Goal created successfully.",
|
||||||
|
Data = created
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[HttpPost("{idStudent:guid}/progress-event")]
|
[HttpPost("{idStudent:guid}/progress-event")]
|
||||||
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")]
|
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")]
|
||||||
[ProducesResponseType(typeof(ResponseResult), StatusCodes.Status201Created)]
|
[ProducesResponseType(typeof(ResponseResult), StatusCodes.Status201Created)]
|
||||||
@@ -216,7 +267,7 @@ public class StudentController : BaseController
|
|||||||
[Authorize(Roles = $"{UserRoles.Teacher}")]
|
[Authorize(Roles = $"{UserRoles.Teacher}")]
|
||||||
[ProducesResponseType(typeof(ResponseResult<StudentResponse>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ResponseResult<StudentResponse>), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(ResponseResult<StudentResponse>), StatusCodes.Status404NotFound)]
|
[ProducesResponseType(typeof(ResponseResult<StudentResponse>), StatusCodes.Status404NotFound)]
|
||||||
public async Task<ActionResult<ResponseResult<StudentResponse>>> Update(Guid idStudent, [FromBody] UpdateStudentDto request)
|
public async Task<ActionResult<ResponseResult<StudentResponse>>> UpdateStudent(Guid idStudent, [FromBody] UpdateStudentDto request)
|
||||||
{
|
{
|
||||||
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
|
var (userId, email, programId, role, error) = GetProgramUserFromClaims();
|
||||||
if (error is not null)
|
if (error is not null)
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace WinStudentGoalTracker.DataAccess;
|
||||||
|
|
||||||
|
public class CreateGoalDto
|
||||||
|
{
|
||||||
|
public string? Title { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? Category { get; set; }
|
||||||
|
}
|
||||||
@@ -101,6 +101,36 @@ public class StudentRepository
|
|||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<StudentGoalItem?> InsertGoalAsync(Guid idStudent, Guid userId, CreateGoalDto dto)
|
||||||
|
{
|
||||||
|
var newGoalId = Guid.NewGuid();
|
||||||
|
using var db = Connection;
|
||||||
|
var rowsAffected = await db.ExecuteAsync(
|
||||||
|
"sp_Goal_Insert",
|
||||||
|
new
|
||||||
|
{
|
||||||
|
p_id_goal = newGoalId.ToString(),
|
||||||
|
p_id_student = idStudent.ToString(),
|
||||||
|
p_id_user = userId.ToString(),
|
||||||
|
p_title = dto.Title,
|
||||||
|
p_description = dto.Description,
|
||||||
|
p_category = dto.Category
|
||||||
|
},
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
|
if (rowsAffected == 0) return null;
|
||||||
|
|
||||||
|
return new StudentGoalItem
|
||||||
|
{
|
||||||
|
GoalId = newGoalId,
|
||||||
|
GoalParentId = null,
|
||||||
|
Title = dto.Title,
|
||||||
|
Description = dto.Description,
|
||||||
|
Category = dto.Category,
|
||||||
|
ProgressEventCount = 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<StudentGoalSummary?> GetGoalSummaryAsync(Guid idStudent)
|
public async Task<StudentGoalSummary?> GetGoalSummaryAsync(Guid idStudent)
|
||||||
{
|
{
|
||||||
using var db = Connection;
|
using var db = Connection;
|
||||||
@@ -110,7 +140,17 @@ public class StudentRepository
|
|||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
|
|
||||||
var list = rows.ToList();
|
var list = rows.ToList();
|
||||||
if (list.Count == 0) return null;
|
if (list.Count == 0)
|
||||||
|
{
|
||||||
|
var student = await GetByIdAsync(idStudent);
|
||||||
|
if (student is null) return null;
|
||||||
|
|
||||||
|
return new StudentGoalSummary
|
||||||
|
{
|
||||||
|
StudentIdentifier = student.Identifier,
|
||||||
|
Goals = []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return new StudentGoalSummary
|
return new StudentGoalSummary
|
||||||
{
|
{
|
||||||
|
|||||||
+58
@@ -0,0 +1,58 @@
|
|||||||
|
<div class="overlay" (click)="onCancel()"></div>
|
||||||
|
|
||||||
|
<div class="modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title">Add Goal</h2>
|
||||||
|
<button class="close-btn" (click)="onCancel()" aria-label="Close">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="modal-body" (ngSubmit)="onSubmit()" #goalForm="ngForm">
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="title">Title</label>
|
||||||
|
<input
|
||||||
|
id="title"
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="form.title"
|
||||||
|
name="title"
|
||||||
|
required
|
||||||
|
placeholder="e.g. Improve reading comprehension"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="category">Category</label>
|
||||||
|
<input
|
||||||
|
id="category"
|
||||||
|
type="text"
|
||||||
|
[(ngModel)]="form.category"
|
||||||
|
name="category"
|
||||||
|
required
|
||||||
|
placeholder="e.g. Academics"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field">
|
||||||
|
<label for="description">Description</label>
|
||||||
|
<textarea
|
||||||
|
id="description"
|
||||||
|
[(ngModel)]="form.description"
|
||||||
|
name="description"
|
||||||
|
rows="3"
|
||||||
|
placeholder="Describe the goal..."
|
||||||
|
></textarea>
|
||||||
|
</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]="goalForm.invalid || isSubmitting()">
|
||||||
|
{{ isSubmitting() ? 'Saving...' : 'Add Goal' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
+134
@@ -0,0 +1,134 @@
|
|||||||
|
: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,
|
||||||
|
.field textarea {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input:focus,
|
||||||
|
.field textarea: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, input, output, signal } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { CreateGoalDto } from '../../../shared/classes/create-goal.dto';
|
||||||
|
import { StudentGoalItem } from '../../../shared/classes/student-goal';
|
||||||
|
import { StudentService } from '../../../shared/services/student.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-add-goal-modal',
|
||||||
|
imports: [FormsModule],
|
||||||
|
templateUrl: './add-goal-modal.html',
|
||||||
|
styleUrl: './add-goal-modal.scss',
|
||||||
|
})
|
||||||
|
export class AddGoalModal {
|
||||||
|
|
||||||
|
// ************************** Constructor **************************
|
||||||
|
|
||||||
|
// ************************** Declarations *************************
|
||||||
|
|
||||||
|
private readonly studentService = inject(StudentService);
|
||||||
|
|
||||||
|
readonly studentId = input.required<string>();
|
||||||
|
readonly goalCreated = output<StudentGoalItem>();
|
||||||
|
readonly cancelled = output<void>();
|
||||||
|
|
||||||
|
protected readonly isSubmitting = signal(false);
|
||||||
|
protected readonly errorMessage = signal<string | null>(null);
|
||||||
|
|
||||||
|
protected form: CreateGoalDto = {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
category: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
// ************************** Properties ***************************
|
||||||
|
|
||||||
|
// ************************ Public Methods *************************
|
||||||
|
|
||||||
|
// ************************ Event Handlers *************************
|
||||||
|
|
||||||
|
async onSubmit() {
|
||||||
|
this.errorMessage.set(null);
|
||||||
|
this.isSubmitting.set(true);
|
||||||
|
|
||||||
|
const result = await this.studentService.createGoal(this.studentId(), this.form);
|
||||||
|
|
||||||
|
this.isSubmitting.set(false);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
this.errorMessage.set(result.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.goalCreated.emit(result.payload!);
|
||||||
|
}
|
||||||
|
|
||||||
|
onCancel() {
|
||||||
|
this.cancelled.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********************** Support Procedures ***********************
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<span class="category-badge">{{ goal().category }}</span>
|
||||||
|
<span class="event-count">{{ goal().progressEventCount }} events</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="title">{{ goal().title }}</h3>
|
||||||
|
<p class="description">{{ goal().description }}</p>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
padding: 1.25rem 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-badge {
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
background: #eef2ff;
|
||||||
|
color: #4f46e5;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-count {
|
||||||
|
font-size: 0.8125rem;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #111;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #555;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Component, input } from '@angular/core';
|
||||||
|
import { StudentGoalItem } from '../../../shared/classes/student-goal';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-goal-card',
|
||||||
|
imports: [],
|
||||||
|
templateUrl: './goal-card.html',
|
||||||
|
styleUrl: './goal-card.scss',
|
||||||
|
})
|
||||||
|
export class GoalCard {
|
||||||
|
|
||||||
|
// ************************** Constructor **************************
|
||||||
|
|
||||||
|
// ************************** Declarations *************************
|
||||||
|
|
||||||
|
readonly goal = input.required<StudentGoalItem>();
|
||||||
|
|
||||||
|
// ************************** Properties ***************************
|
||||||
|
|
||||||
|
// ************************ Public Methods *************************
|
||||||
|
|
||||||
|
// ************************ Event Handlers *************************
|
||||||
|
|
||||||
|
// ********************** Support Procedures ***********************
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
<div class="toolbar">
|
||||||
|
<button class="toolbar-btn back-btn" (click)="onBack()">← Students</button>
|
||||||
|
@if (studentIdentifier()) {
|
||||||
|
<span class="student-label">{{ studentIdentifier() }}</span>
|
||||||
|
}
|
||||||
|
<span class="spacer"></span>
|
||||||
|
<button class="toolbar-btn" (click)="onAddGoal()">+ Add a Goal</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (showAddModal()) {
|
||||||
|
<app-add-goal-modal
|
||||||
|
[studentId]="studentId"
|
||||||
|
(goalCreated)="onGoalCreated($event)"
|
||||||
|
(cancelled)="onModalCancelled()"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (errorMessage()) {
|
||||||
|
<p class="error">{{ errorMessage() }}</p>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="card-grid">
|
||||||
|
@for (goal of goals(); track goal.goalId) {
|
||||||
|
<app-goal-card [goal]="goal" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
height: 40px;
|
||||||
|
padding-right: 0.5rem;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #fff;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
background: transparent;
|
||||||
|
color: #4f46e5;
|
||||||
|
border: 1px solid #4f46e5;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar-btn:hover {
|
||||||
|
background: #eef2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-btn {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.student-label {
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: #dc2626;
|
||||||
|
margin: 0 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-grid {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 1rem;
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { Component, inject, signal } from '@angular/core';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { StudentGoalItem } from '../../../shared/classes/student-goal';
|
||||||
|
import { StudentService } from '../../../shared/services/student.service';
|
||||||
|
import { GoalCard } from '../goal-card/goal-card';
|
||||||
|
import { AddGoalModal } from '../add-goal-modal/add-goal-modal';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-goal-list',
|
||||||
|
imports: [GoalCard, AddGoalModal],
|
||||||
|
templateUrl: './goal-list.html',
|
||||||
|
styleUrl: './goal-list.scss',
|
||||||
|
})
|
||||||
|
export class GoalList {
|
||||||
|
|
||||||
|
// ************************** Constructor **************************
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.studentId = this.route.snapshot.paramMap.get('studentId')!;
|
||||||
|
this.loadGoals();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ************************** Declarations *************************
|
||||||
|
|
||||||
|
private readonly studentService = inject(StudentService);
|
||||||
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
|
protected readonly studentId: string;
|
||||||
|
protected readonly studentIdentifier = signal<string | null>(null);
|
||||||
|
protected readonly goals = signal<StudentGoalItem[]>([]);
|
||||||
|
protected readonly showAddModal = signal(false);
|
||||||
|
protected readonly errorMessage = signal<string | null>(null);
|
||||||
|
|
||||||
|
// ************************** Properties ***************************
|
||||||
|
|
||||||
|
// ************************ Public Methods *************************
|
||||||
|
|
||||||
|
// ************************ Event Handlers *************************
|
||||||
|
|
||||||
|
onAddGoal() {
|
||||||
|
this.showAddModal.set(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
onGoalCreated(goal: StudentGoalItem) {
|
||||||
|
this.goals.update(list => [...list, goal]);
|
||||||
|
this.showAddModal.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onModalCancelled() {
|
||||||
|
this.showAddModal.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
onBack() {
|
||||||
|
this.router.navigate(['/students']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ********************** Support Procedures ***********************
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Loads goals for the student from the service.
|
||||||
|
// *****************************************************************
|
||||||
|
private loadGoals() {
|
||||||
|
this.studentService.getGoalsForStudent(this.studentId).then(data => {
|
||||||
|
if (!data.success) {
|
||||||
|
this.errorMessage.set(data.message);
|
||||||
|
} else {
|
||||||
|
this.studentIdentifier.set(data.payload?.studentIdentifier ?? null);
|
||||||
|
this.goals.set(data.payload?.goals ?? []);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<div class="card">
|
<div class="card" [routerLink]="['/students', student().studentId, 'goals']">
|
||||||
<h2 class="identifier">🎓 {{ student().identifier }}</h2>
|
<h2 class="identifier">🎓 {{ student().identifier }}</h2>
|
||||||
|
|
||||||
<div class="meta">
|
<div class="meta">
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: box-shadow 0.15s ease, transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 4px 20px rgba(79, 70, 229, 0.15);
|
||||||
|
transform: translateY(-2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.identifier {
|
.identifier {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Component, computed, input } from '@angular/core';
|
import { Component, computed, input } from '@angular/core';
|
||||||
import { DatePipe } from '@angular/common';
|
import { DatePipe } from '@angular/common';
|
||||||
|
import { RouterLink } from '@angular/router';
|
||||||
import { StudentCardDto } from '../../../shared/classes/student-card.dto';
|
import { StudentCardDto } from '../../../shared/classes/student-card.dto';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-student-card',
|
selector: 'app-student-card',
|
||||||
imports: [DatePipe],
|
imports: [DatePipe, RouterLink],
|
||||||
templateUrl: './student-card.html',
|
templateUrl: './student-card.html',
|
||||||
styleUrl: './student-card.scss',
|
styleUrl: './student-card.scss',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { Home } from './pages/home/home';
|
import { Home } from './pages/home/home';
|
||||||
import { StudentCardList } from './components/student-card-list/student-card-list';
|
import { StudentCardList } from './components/student-card-list/student-card-list';
|
||||||
|
import { GoalList } from './components/goal-list/goal-list';
|
||||||
|
|
||||||
export default [
|
export default [
|
||||||
{
|
{
|
||||||
@@ -9,6 +10,7 @@ export default [
|
|||||||
children: [
|
children: [
|
||||||
{ path: '', redirectTo: 'students', pathMatch: 'full' },
|
{ path: '', redirectTo: 'students', pathMatch: 'full' },
|
||||||
{ path: 'students', component: StudentCardList },
|
{ path: 'students', component: StudentCardList },
|
||||||
|
{ path: 'students/:studentId/goals', component: GoalList },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] satisfies Routes;
|
] satisfies Routes;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface CreateGoalDto {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
category: string;
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
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';
|
import { StudentGoalSummary, StudentGoalItem } from '../classes/student-goal';
|
||||||
|
import { CreateGoalDto } from '../classes/create-goal.dto';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -105,6 +106,25 @@ export class DummyStudentService {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async createGoal(studentId: string, data: CreateGoalDto): Promise<ApiResult<StudentGoalItem>> {
|
||||||
|
const student = this.data[studentId];
|
||||||
|
if (!student) {
|
||||||
|
return ApiResult.fail('Student not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newGoal: StudentGoalItem = {
|
||||||
|
goalId: `g${Date.now()}`,
|
||||||
|
goalParentId: null,
|
||||||
|
title: data.title,
|
||||||
|
description: data.description,
|
||||||
|
category: data.category,
|
||||||
|
progressEventCount: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
student.goals.push(newGoal);
|
||||||
|
return ApiResult.ok(newGoal);
|
||||||
|
}
|
||||||
|
|
||||||
async addProgressEvent(studentId: string, goalId: string, content: string): Promise<ApiResult> {
|
async addProgressEvent(studentId: string, goalId: string, content: string): Promise<ApiResult> {
|
||||||
return ApiResult.empty();
|
return ApiResult.empty();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { ApiResult } from '../classes/api-result';
|
|||||||
import { ResponseResult } from '../classes/auth.models';
|
import { ResponseResult } from '../classes/auth.models';
|
||||||
import { describeHttpError } from '../classes/http-errors';
|
import { describeHttpError } from '../classes/http-errors';
|
||||||
import { CreateStudentDto } from '../classes/create-student.dto';
|
import { CreateStudentDto } from '../classes/create-student.dto';
|
||||||
|
import { CreateGoalDto } from '../classes/create-goal.dto';
|
||||||
import { StudentCardDto } from '../classes/student-card.dto';
|
import { StudentCardDto } from '../classes/student-card.dto';
|
||||||
import { StudentGoalSummary } from '../classes/student-goal';
|
import { StudentGoalSummary, StudentGoalItem } from '../classes/student-goal';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
@@ -73,6 +74,22 @@ export class StudentService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Creates a new goal for a student and returns the created goal.
|
||||||
|
// *****************************************************************
|
||||||
|
async createGoal(studentId: string, data: CreateGoalDto): Promise<ApiResult<StudentGoalItem>> {
|
||||||
|
try {
|
||||||
|
const result = await firstValueFrom(
|
||||||
|
this.http.post<ResponseResult<StudentGoalItem>>(`${this.base}/api/Student/${studentId}/goals`, 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> {
|
async addProgressEvent(studentId: string, goalId: string, content: string): Promise<ApiResult> {
|
||||||
try {
|
try {
|
||||||
const result = await firstValueFrom(
|
const result = await firstValueFrom(
|
||||||
|
|||||||
Reference in New Issue
Block a user