From 0036e25b9c41ad822f0395c0551314f13e25caec Mon Sep 17 00:00:00 2001 From: ivan-pelly Date: Fri, 10 Apr 2026 17:52:52 -0700 Subject: [PATCH] Edit button locate, delete, group header --- api/src/Controllers/StudentController.cs | 96 +++++++++++++++++++ .../Repositories/StudentRepository.cs | 40 ++++++++ db/Objects/procedures/sp_Benchmark_Delete.sql | 12 +++ db/Objects/procedures/sp_Goal_Delete.sql | 22 +++++ .../procedures/sp_ProgressEvent_Delete.sql | 12 +++ .../procedures/sp_ReportPrompt_Delete.sql | 2 +- .../procedures/sp_ReportPrompt_GetAll.sql | 2 +- .../procedures/sp_ReportPrompt_GetById.sql | 4 +- .../sp_ReportPrompt_GetByReportname.sql | 2 +- .../procedures/sp_ReportPrompt_Insert.sql | 6 +- .../procedures/sp_ReportPrompt_Update.sql | 6 +- db/Objects/procedures/sp_Student_Delete.sql | 18 ++++ db/Objects/tables/school_district.sql | 7 -- db/Objects/tables/student.sql | 12 --- db/Objects/tables/user.sql | 12 --- db/Objects/tables/user_program.sql | 16 ---- db/Objects/tables/user_student.sql | 11 --- .../confirm-modal/confirm-modal.html | 19 ++++ .../confirm-modal/confirm-modal.scss | 42 ++++++++ .../components/confirm-modal/confirm-modal.ts | 46 +++++++++ .../components/workspace/workspace.html | 76 ++++++++++++++- .../components/workspace/workspace.scss | 76 +++++++++++++++ .../desktop/components/workspace/workspace.ts | 93 +++++++++++++++++- .../src/app/desktop/pages/home/home.scss | 14 +-- .../app/shared/services/student.service.ts | 65 +++++++++++++ 25 files changed, 629 insertions(+), 82 deletions(-) create mode 100644 db/Objects/procedures/sp_Benchmark_Delete.sql create mode 100644 db/Objects/procedures/sp_Goal_Delete.sql create mode 100644 db/Objects/procedures/sp_ProgressEvent_Delete.sql delete mode 100644 db/Objects/tables/school_district.sql delete mode 100644 db/Objects/tables/student.sql delete mode 100644 db/Objects/tables/user.sql delete mode 100644 db/Objects/tables/user_program.sql delete mode 100644 db/Objects/tables/user_student.sql create mode 100644 ui/winstudentgoaltracker/src/app/desktop/components/confirm-modal/confirm-modal.html create mode 100644 ui/winstudentgoaltracker/src/app/desktop/components/confirm-modal/confirm-modal.scss create mode 100644 ui/winstudentgoaltracker/src/app/desktop/components/confirm-modal/confirm-modal.ts diff --git a/api/src/Controllers/StudentController.cs b/api/src/Controllers/StudentController.cs index 56fe0a0..b2538b6 100644 --- a/api/src/Controllers/StudentController.cs +++ b/api/src/Controllers/StudentController.cs @@ -324,6 +324,38 @@ public class StudentController : BaseController }); } + [HttpDelete("{idStudent:guid}/goals/{idGoal:guid}")] + [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin}")] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status404NotFound)] + public async Task>> DeleteGoal(Guid idStudent, Guid idGoal) + { + var (userId, email, programId, role, error) = GetProgramUserFromClaims(); + if (error is not null) + { + return error; + } + + var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role, "all"); + + if (!students.Select(s => s.StudentId).Contains(idStudent)) + { + return NotFound(new ResponseResult + { + Success = false, + Message = "Student not found." + }); + } + + var deleted = await _studentRepository.DeleteGoalAsync(idGoal); + + return Ok(new ResponseResult + { + Success = true, + Message = deleted ? "Goal deleted." : "Goal not found." + }); + } + [HttpPost("{idStudent:guid}/progress-event")] [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")] [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status201Created)] @@ -412,6 +444,38 @@ public class StudentController : BaseController } } + [HttpDelete("{idStudent:guid}/progress-events/{idProgressEvent:guid}")] + [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin}")] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status404NotFound)] + public async Task>> DeleteProgressEvent(Guid idStudent, Guid idProgressEvent) + { + var (userId, email, programId, role, error) = GetProgramUserFromClaims(); + if (error is not null) + { + return error; + } + + var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role, "all"); + + if (!students.Select(s => s.StudentId).Contains(idStudent)) + { + return NotFound(new ResponseResult + { + Success = false, + Message = "Student not found." + }); + } + + var deleted = await _studentRepository.DeleteProgressEventAsync(idProgressEvent); + + return Ok(new ResponseResult + { + Success = true, + Message = deleted ? "Progress event deleted." : "Progress event not found." + }); + } + [HttpGet("progress-events/{idProgressEvent:guid}/benchmarks")] [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")] [ProducesResponseType(typeof(ResponseResult>), StatusCodes.Status200OK)] @@ -676,6 +740,38 @@ public class StudentController : BaseController }); } + [HttpDelete("{idStudent:guid}/benchmarks/{idBenchmark:guid}")] + [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin}")] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status404NotFound)] + public async Task>> DeleteBenchmark(Guid idStudent, Guid idBenchmark) + { + var (userId, email, programId, role, error) = GetProgramUserFromClaims(); + if (error is not null) + { + return error; + } + + var students = await _studentRepository.GetMyStudentsAsync(userId, programId, role, "all"); + + if (!students.Select(s => s.StudentId).Contains(idStudent)) + { + return NotFound(new ResponseResult + { + Success = false, + Message = "Student not found." + }); + } + + var deleted = await _studentRepository.DeleteBenchmarkAsync(idBenchmark); + + return Ok(new ResponseResult + { + Success = true, + Message = deleted ? "Benchmark deleted." : "Benchmark not found." + }); + } + [HttpGet("{idStudent:guid}/progress-report")] [Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")] [ProducesResponseType(typeof(ResponseResult), StatusCodes.Status200OK)] diff --git a/api/src/DataAccess/Repositories/StudentRepository.cs b/api/src/DataAccess/Repositories/StudentRepository.cs index 6666c8b..c0af377 100644 --- a/api/src/DataAccess/Repositories/StudentRepository.cs +++ b/api/src/DataAccess/Repositories/StudentRepository.cs @@ -145,6 +145,19 @@ public class StudentRepository return rows.Select(r => r.benchmarkId is Guid g ? g : Guid.Parse((string)r.benchmarkId)).ToList(); } + // ***************************************************************** + // Deletes a progress event and its benchmark associations. + // ***************************************************************** + public async Task DeleteProgressEventAsync(Guid progressEventId) + { + using var db = Connection; + var rowsAffected = await db.ExecuteScalarAsync( + "sp_ProgressEvent_Delete", + new { p_id_progress_event = progressEventId.ToString() }, + commandType: CommandType.StoredProcedure); + return rowsAffected > 0; + } + public async Task GetStudentIdForGoalAsync(Guid idGoal) { using var db = Connection; @@ -276,6 +289,20 @@ public class StudentRepository return rowsAffected > 0; } + // ***************************************************************** + // Deletes a goal and all its child entities (benchmarks, progress + // events, event-benchmark links, child goals) via cascade SP. + // ***************************************************************** + public async Task DeleteGoalAsync(Guid goalId) + { + using var db = Connection; + var rowsAffected = await db.ExecuteScalarAsync( + "sp_Goal_Delete", + new { p_id_goal = goalId.ToString() }, + commandType: CommandType.StoredProcedure); + return rowsAffected > 0; + } + // ***************************************************************** // Returns all benchmarks for a student, grouped under a summary // with the student identifier. Returns null if student not found. @@ -367,6 +394,19 @@ public class StudentRepository return rowsAffected > 0; } + // ***************************************************************** + // Deletes a benchmark and its progress-event associations. + // ***************************************************************** + public async Task DeleteBenchmarkAsync(Guid benchmarkId) + { + using var db = Connection; + var rowsAffected = await db.ExecuteScalarAsync( + "sp_Benchmark_Delete", + new { p_id_benchmark = benchmarkId.ToString() }, + commandType: CommandType.StoredProcedure); + return rowsAffected > 0; + } + // ***************************************************************** // Returns a full student profile: student card, goals, benchmarks, // progress events, and benchmark/event associations in one call. diff --git a/db/Objects/procedures/sp_Benchmark_Delete.sql b/db/Objects/procedures/sp_Benchmark_Delete.sql new file mode 100644 index 0000000..7c7250b --- /dev/null +++ b/db/Objects/procedures/sp_Benchmark_Delete.sql @@ -0,0 +1,12 @@ +DELIMITER ;; +CREATE DEFINER=`root`@`%` PROCEDURE `sp_Benchmark_Delete`(IN p_id_benchmark CHAR(36)) +BEGIN + -- Remove progress-event/benchmark associations + DELETE FROM progress_event_benchmark + WHERE id_benchmark = p_id_benchmark; + -- Remove the benchmark itself + DELETE FROM benchmark + WHERE id_benchmark = p_id_benchmark; + SELECT ROW_COUNT() AS rows_affected; +END;; +DELIMITER ; diff --git a/db/Objects/procedures/sp_Goal_Delete.sql b/db/Objects/procedures/sp_Goal_Delete.sql new file mode 100644 index 0000000..001b190 --- /dev/null +++ b/db/Objects/procedures/sp_Goal_Delete.sql @@ -0,0 +1,22 @@ +DELIMITER ;; +CREATE DEFINER=`root`@`%` PROCEDURE `sp_Goal_Delete`(IN p_id_goal CHAR(36)) +BEGIN + -- Remove benchmark/event associations for progress events under this goal + DELETE peb FROM progress_event_benchmark peb + INNER JOIN progress_event pe ON pe.id_progress_event = peb.id_progress_event + WHERE pe.id_goal = p_id_goal; + -- Remove progress events under this goal + DELETE FROM progress_event + WHERE id_goal = p_id_goal; + -- Remove benchmarks under this goal + DELETE FROM benchmark + WHERE id_goal = p_id_goal; + -- Remove child goals (one level) + DELETE FROM goal + WHERE id_goal_parent = p_id_goal; + -- Remove the goal itself + DELETE FROM goal + WHERE id_goal = p_id_goal; + SELECT ROW_COUNT() AS rows_affected; +END;; +DELIMITER ; diff --git a/db/Objects/procedures/sp_ProgressEvent_Delete.sql b/db/Objects/procedures/sp_ProgressEvent_Delete.sql new file mode 100644 index 0000000..2a901c5 --- /dev/null +++ b/db/Objects/procedures/sp_ProgressEvent_Delete.sql @@ -0,0 +1,12 @@ +DELIMITER ;; +CREATE DEFINER=`root`@`%` PROCEDURE `sp_ProgressEvent_Delete`(IN p_id_progress_event CHAR(36)) +BEGIN + -- Remove benchmark associations + DELETE FROM progress_event_benchmark + WHERE id_progress_event = p_id_progress_event; + -- Remove the progress event itself + DELETE FROM progress_event + WHERE id_progress_event = p_id_progress_event; + SELECT ROW_COUNT() AS rows_affected; +END;; +DELIMITER ; diff --git a/db/Objects/procedures/sp_ReportPrompt_Delete.sql b/db/Objects/procedures/sp_ReportPrompt_Delete.sql index 49ca422..9a5eca8 100644 --- a/db/Objects/procedures/sp_ReportPrompt_Delete.sql +++ b/db/Objects/procedures/sp_ReportPrompt_Delete.sql @@ -4,7 +4,7 @@ CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_Delete`( ) BEGIN DELETE FROM `ReportPrompt` - WHERE `id_report_prompt` = p_id_report_prompt; + WHERE `id_ReportPrompt` = p_id_report_prompt; SELECT ROW_COUNT() AS rowsAffected; END;; DELIMITER ; diff --git a/db/Objects/procedures/sp_ReportPrompt_GetAll.sql b/db/Objects/procedures/sp_ReportPrompt_GetAll.sql index b0bdb7a..bceb77f 100644 --- a/db/Objects/procedures/sp_ReportPrompt_GetAll.sql +++ b/db/Objects/procedures/sp_ReportPrompt_GetAll.sql @@ -2,7 +2,7 @@ DELIMITER ;; CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_GetAll`() BEGIN SELECT - `id_report_prompt` AS `reportPromptId`, + `id_ReportPrompt` AS `reportPromptId`, `id_program` AS `programId`, `prompt` AS `prompt`, `reportname` AS `reportname` diff --git a/db/Objects/procedures/sp_ReportPrompt_GetById.sql b/db/Objects/procedures/sp_ReportPrompt_GetById.sql index 41e0b4a..9e1f589 100644 --- a/db/Objects/procedures/sp_ReportPrompt_GetById.sql +++ b/db/Objects/procedures/sp_ReportPrompt_GetById.sql @@ -4,12 +4,12 @@ CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_GetById`( ) BEGIN SELECT - `id_report_prompt` AS `reportPromptId`, + `id_ReportPrompt` AS `reportPromptId`, `id_program` AS `programId`, `prompt` AS `prompt`, `reportname` AS `reportname` FROM `ReportPrompt` - WHERE `id_report_prompt` = p_id_report_prompt + WHERE `id_ReportPrompt` = p_id_report_prompt LIMIT 1; END;; DELIMITER ; diff --git a/db/Objects/procedures/sp_ReportPrompt_GetByReportname.sql b/db/Objects/procedures/sp_ReportPrompt_GetByReportname.sql index ad1e96e..5571363 100644 --- a/db/Objects/procedures/sp_ReportPrompt_GetByReportname.sql +++ b/db/Objects/procedures/sp_ReportPrompt_GetByReportname.sql @@ -5,7 +5,7 @@ CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_GetByReportname`( ) BEGIN SELECT - `id_report_prompt` AS `reportPromptId`, + `id_ReportPrompt` AS `reportPromptId`, `id_program` AS `programId`, `prompt` AS `prompt`, `reportname` AS `reportname` diff --git a/db/Objects/procedures/sp_ReportPrompt_Insert.sql b/db/Objects/procedures/sp_ReportPrompt_Insert.sql index 5b5bdcd..b5cfc20 100644 --- a/db/Objects/procedures/sp_ReportPrompt_Insert.sql +++ b/db/Objects/procedures/sp_ReportPrompt_Insert.sql @@ -8,7 +8,7 @@ CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_Insert`( BEGIN INSERT INTO `ReportPrompt` ( - `id_report_prompt`, + `id_ReportPrompt`, `id_program`, `prompt`, `reportname` @@ -21,12 +21,12 @@ BEGIN p_reportname ); SELECT - `id_report_prompt` AS `reportPromptId`, + `id_ReportPrompt` AS `reportPromptId`, `id_program` AS `programId`, `prompt` AS `prompt`, `reportname` AS `reportname` FROM `ReportPrompt` - WHERE `id_report_prompt` = p_id_report_prompt + WHERE `id_ReportPrompt` = p_id_report_prompt LIMIT 1; END;; DELIMITER ; diff --git a/db/Objects/procedures/sp_ReportPrompt_Update.sql b/db/Objects/procedures/sp_ReportPrompt_Update.sql index bedcbb5..d60d006 100644 --- a/db/Objects/procedures/sp_ReportPrompt_Update.sql +++ b/db/Objects/procedures/sp_ReportPrompt_Update.sql @@ -7,9 +7,9 @@ CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_Update`( BEGIN UPDATE `ReportPrompt` SET - `prompt` = p_prompt, - `reportname` = p_reportname - WHERE `id_report_prompt` = p_id_report_prompt; + `prompt` = COALESCE(p_prompt, `prompt`), + `reportname` = COALESCE(p_reportname, `reportname`) + WHERE `id_ReportPrompt` = p_id_report_prompt; SELECT ROW_COUNT() AS rowsAffected; END;; DELIMITER ; diff --git a/db/Objects/procedures/sp_Student_Delete.sql b/db/Objects/procedures/sp_Student_Delete.sql index 811a21d..486256a 100644 --- a/db/Objects/procedures/sp_Student_Delete.sql +++ b/db/Objects/procedures/sp_Student_Delete.sql @@ -1,8 +1,26 @@ DELIMITER ;; CREATE DEFINER=`root`@`%` PROCEDURE `sp_Student_Delete`(IN p_id_student CHAR(36)) BEGIN + -- Remove progress-event/benchmark associations + DELETE peb FROM progress_event_benchmark peb + INNER JOIN progress_event pe ON pe.id_progress_event = peb.id_progress_event + INNER JOIN goal g ON g.id_goal = pe.id_goal + WHERE g.id_student = p_id_student; + -- Remove progress events + DELETE pe FROM progress_event pe + INNER JOIN goal g ON g.id_goal = pe.id_goal + WHERE g.id_student = p_id_student; + -- Remove benchmarks + DELETE b FROM benchmark b + INNER JOIN goal g ON g.id_goal = b.id_goal + WHERE g.id_student = p_id_student; + -- Remove goals + DELETE FROM goal + WHERE id_student = p_id_student; + -- Remove user-student associations DELETE FROM user_student WHERE id_student = p_id_student; + -- Remove the student DELETE FROM student WHERE id_student = p_id_student; SELECT ROW_COUNT() AS rows_affected; diff --git a/db/Objects/tables/school_district.sql b/db/Objects/tables/school_district.sql deleted file mode 100644 index ae43873..0000000 --- a/db/Objects/tables/school_district.sql +++ /dev/null @@ -1,7 +0,0 @@ -CREATE TABLE `school_district` ( - `id_school_district` char(36) NOT NULL, - `name` varchar(255) DEFAULT NULL, - `contact_email` varchar(255) DEFAULT NULL, - `created_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id_school_district`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/db/Objects/tables/student.sql b/db/Objects/tables/student.sql deleted file mode 100644 index 44eebd8..0000000 --- a/db/Objects/tables/student.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE `student` ( - `id_student` char(36) NOT NULL, - `id_program` char(36) DEFAULT NULL, - `identifier` varchar(50) DEFAULT NULL, - `program_year` int DEFAULT NULL, - `enrollment_date` date DEFAULT NULL, - `next_iep_date` date DEFAULT NULL, - `created_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id_student`), - KEY `student_ibfk_1` (`id_program`), - CONSTRAINT `student_ibfk_1` FOREIGN KEY (`id_program`) REFERENCES `program` (`id_program`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/db/Objects/tables/user.sql b/db/Objects/tables/user.sql deleted file mode 100644 index 821ef59..0000000 --- a/db/Objects/tables/user.sql +++ /dev/null @@ -1,12 +0,0 @@ -CREATE TABLE `user` ( - `id_user` char(36) NOT NULL, - `email` varchar(255) DEFAULT NULL, - `name` varchar(255) DEFAULT NULL, - `password_hash` varchar(255) DEFAULT NULL, - `password_salt` varchar(255) DEFAULT NULL, - `password_updated_at` timestamp NULL DEFAULT NULL, - `failed_login_attempts` int DEFAULT '0', - `locked_until` timestamp NULL DEFAULT NULL, - `created_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id_user`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/db/Objects/tables/user_program.sql b/db/Objects/tables/user_program.sql deleted file mode 100644 index 4ab363e..0000000 --- a/db/Objects/tables/user_program.sql +++ /dev/null @@ -1,16 +0,0 @@ -CREATE TABLE `user_program` ( - `id_user_program` char(36) NOT NULL, - `id_user` char(36) DEFAULT NULL, - `id_program` char(36) DEFAULT NULL, - `id_role` char(36) DEFAULT NULL, - `is_primary` tinyint(1) DEFAULT '0', - `status` varchar(20) DEFAULT 'active', - `joined_at` timestamp NULL DEFAULT NULL, - PRIMARY KEY (`id_user_program`), - UNIQUE KEY `uq_user_program` (`id_user`,`id_program`), - KEY `idx_id_program` (`id_program`), - KEY `idx_user_program_role` (`id_role`), - CONSTRAINT `user_program_ibfk_1` FOREIGN KEY (`id_user`) REFERENCES `user` (`id_user`), - CONSTRAINT `user_program_ibfk_2` FOREIGN KEY (`id_program`) REFERENCES `program` (`id_program`), - CONSTRAINT `user_program_ibfk_3` FOREIGN KEY (`id_role`) REFERENCES `role` (`id_role`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/db/Objects/tables/user_student.sql b/db/Objects/tables/user_student.sql deleted file mode 100644 index ef9286b..0000000 --- a/db/Objects/tables/user_student.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE `user_student` ( - `id_user_student` char(36) NOT NULL, - `id_user` char(36) DEFAULT NULL, - `id_student` char(36) DEFAULT NULL, - `is_primary` tinyint(1) DEFAULT NULL, - PRIMARY KEY (`id_user_student`), - KEY `user_student_ibfk_1` (`id_user`), - KEY `user_student_ibfk_2` (`id_student`), - CONSTRAINT `user_student_ibfk_1` FOREIGN KEY (`id_user`) REFERENCES `user` (`id_user`), - CONSTRAINT `user_student_ibfk_2` FOREIGN KEY (`id_student`) REFERENCES `student` (`id_student`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/confirm-modal/confirm-modal.html b/ui/winstudentgoaltracker/src/app/desktop/components/confirm-modal/confirm-modal.html new file mode 100644 index 0000000..b677cb4 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/desktop/components/confirm-modal/confirm-modal.html @@ -0,0 +1,19 @@ + + @if (!awaitingSecondConfirm()) { +

{{ message() }}

+ } @else { +

Are you absolutely sure? This action is permanent and cannot be undone.

+ } + +
diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/confirm-modal/confirm-modal.scss b/ui/winstudentgoaltracker/src/app/desktop/components/confirm-modal/confirm-modal.scss new file mode 100644 index 0000000..a25ac09 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/desktop/components/confirm-modal/confirm-modal.scss @@ -0,0 +1,42 @@ +.confirm-message { + font-size: 14px; + line-height: 1.55; + color: var(--text-secondary); + margin: 0 0 16px; +} + +.second-confirm-message { + color: #DC2626; + font-weight: 600; +} + +:host ::ng-deep .btn-danger { + padding: 8px 20px; + border-radius: var(--radius-md); + border: none; + background: #DC2626; + color: #fff; + font-size: 13px; + font-weight: 600; + cursor: pointer; + + &:hover { + background: #B91C1C; + } +} + +:host ::ng-deep .btn-danger-confirm { + padding: 8px 20px; + border-radius: var(--radius-md); + border: 2px solid #DC2626; + background: #FEF2F2; + color: #DC2626; + font-size: 13px; + font-weight: 600; + cursor: pointer; + + &:hover { + background: #DC2626; + color: #fff; + } +} diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/confirm-modal/confirm-modal.ts b/ui/winstudentgoaltracker/src/app/desktop/components/confirm-modal/confirm-modal.ts new file mode 100644 index 0000000..395ab06 --- /dev/null +++ b/ui/winstudentgoaltracker/src/app/desktop/components/confirm-modal/confirm-modal.ts @@ -0,0 +1,46 @@ +import { Component, input, output, signal } from '@angular/core'; +import { ModalShell } from '../modal-shell/modal-shell'; + +@Component({ + selector: 'app-confirm-modal', + imports: [ModalShell], + templateUrl: './confirm-modal.html', + styleUrl: './confirm-modal.scss', +}) +export class ConfirmModal { + + // ************************** Declarations ************************* + + readonly title = input('Confirm'); + readonly message = input.required(); + readonly confirmLabel = input('Delete'); + readonly cancelLabel = input('Cancel'); + readonly destructive = input(false); + readonly doubleConfirm = input(false); + + readonly confirmed = output(); + readonly closed = output(); + + // ************************** Properties *************************** + + protected readonly awaitingSecondConfirm = signal(false); + + // ************************ Event Handlers ************************* + + // ***************************************************************** + // When doubleConfirm is enabled, the first click transitions to a + // second confirmation state. The second click emits confirmed. + // ***************************************************************** + onConfirm() { + if (this.doubleConfirm() && !this.awaitingSecondConfirm()) { + this.awaitingSecondConfirm.set(true); + return; + } + this.confirmed.emit(); + } + + onCancel() { + this.awaitingSecondConfirm.set(false); + this.closed.emit(); + } +} diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.html b/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.html index 19f307f..4e5f6b0 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.html +++ b/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.html @@ -23,12 +23,58 @@ [event]="showEditEventModal() === 'new' ? null : $any(showEditEventModal())" (saved)="onEventSaved()" (closed)="showEditEventModal.set(null)" /> } +@if (showDeleteConfirm()) { + +} +@if (showDeleteBenchmarkConfirm() && deletingBenchmark()) { + +} +@if (showDeleteEventConfirm() && deletingEvent()) { + +} +@if (showDeleteStudentConfirm()) { + +}

{{ student()!.identifier }}

IEP {{ formatDate(student()!.nextIepDate) }} +
@@ -47,11 +93,19 @@
- {{ selectedGoal()!.category }} Goal + {{ selectedGoal()!.category }} Goal @if (selectedGoal()!.targetCompletionDate) { Due {{ formatDate(selectedGoal()!.targetCompletionDate) }} } +

{{ selectedGoal()!.description }}

@@ -75,8 +129,16 @@ @for (b of goalBenchmarks(); track b.benchmarkId) {
- {{ b.shortName || b.benchmark }} + {{ b.shortName || b.benchmark }} +

{{ b.benchmark }}

@@ -94,8 +156,16 @@
- {{ formatDate(ev.createdAt) }} + {{ formatDate(ev.createdAt) }} +

{{ ev.content }}

@if (getBenchmarksForEvent(ev.progressEventId).length > 0) { diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.scss b/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.scss index c075c93..f34f998 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.scss +++ b/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.scss @@ -43,6 +43,26 @@ color: var(--text-faint); } +.delete-student-btn { + display: flex; + align-items: center; + justify-content: center; + margin-left: auto; + align-self: center; + padding: 4px; + border: none; + background: none; + color: var(--text-faint); + cursor: pointer; + border-radius: var(--radius-sm); + transition: color var(--transition-fast), background var(--transition-fast); + + &:hover { + color: #DC2626; + background: #FEE2E2; + } +} + .goal-tabs { display: flex; gap: 4px; @@ -117,6 +137,24 @@ margin-left: auto; } +.delete-goal-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + border: none; + background: none; + color: var(--text-faint); + cursor: pointer; + border-radius: var(--radius-sm); + transition: color var(--transition-fast), background var(--transition-fast); + + &:hover { + color: #DC2626; + background: #FEE2E2; + } +} + .goal-description { font-size: 15px; line-height: 1.55; @@ -193,6 +231,25 @@ margin: 0; } +.delete-benchmark-btn { + display: flex; + align-items: center; + justify-content: center; + margin-left: auto; + padding: 4px; + border: none; + background: none; + color: var(--text-faint); + cursor: pointer; + border-radius: var(--radius-sm); + transition: color var(--transition-fast), background var(--transition-fast); + + &:hover { + color: #DC2626; + background: #FEE2E2; + } +} + /* ─── Progress Timeline ─── */ .timeline { position: relative; @@ -251,6 +308,25 @@ color: #333; } +.delete-event-btn { + display: flex; + align-items: center; + justify-content: center; + margin-left: auto; + padding: 4px; + border: none; + background: none; + color: var(--text-faint); + cursor: pointer; + border-radius: var(--radius-sm); + transition: color var(--transition-fast), background var(--transition-fast); + + &:hover { + color: #DC2626; + background: #FEE2E2; + } +} + .event-benchmarks { display: flex; flex-wrap: wrap; diff --git a/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.ts b/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.ts index be6daad..ce28a65 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.ts +++ b/ui/winstudentgoaltracker/src/app/desktop/components/workspace/workspace.ts @@ -9,13 +9,14 @@ import { GoalModal } from '../goal-modal/goal-modal'; import { EditBenchmarkModal } from '../edit-benchmark-modal/edit-benchmark-modal'; import { EditEventModal } from '../edit-event-modal/edit-event-modal'; import { EditIcon } from '../edit-icon/edit-icon'; +import { ConfirmModal } from '../confirm-modal/confirm-modal'; import { formatDate } from '../../../shared/utils/format-date'; type TabView = 'benchmarks' | 'progress'; @Component({ selector: 'app-workspace', - imports: [GoalModal, EditBenchmarkModal, EditEventModal, EditIcon], + imports: [GoalModal, EditBenchmarkModal, EditEventModal, EditIcon, ConfirmModal], templateUrl: './workspace.html', styleUrl: './workspace.scss', }) @@ -79,6 +80,12 @@ export class Workspace { protected readonly showGoalModal = signal(null); protected readonly showEditBenchmarkModal = signal(null); protected readonly showEditEventModal = signal(null); + protected readonly showDeleteConfirm = signal(false); + protected readonly showDeleteBenchmarkConfirm = signal(false); + protected readonly deletingBenchmark = signal(null); + protected readonly showDeleteEventConfirm = signal(false); + protected readonly deletingEvent = signal(null); + protected readonly showDeleteStudentConfirm = signal(false); // ************************** Properties *************************** @@ -135,6 +142,28 @@ export class Workspace { this.showGoalModal.set('add'); } + onDeleteGoal() { + if (!this.selectedGoal()) return; + this.showDeleteConfirm.set(true); + } + + // ***************************************************************** + // Called when the user confirms deletion in the confirm modal. + // Deletes the selected goal and all its child entities. + // ***************************************************************** + async onDeleteConfirmed() { + this.showDeleteConfirm.set(false); + const goal = this.selectedGoal(); + if (!goal) return; + + const result = await this.studentService.deleteGoal(this.studentId()!, goal.goalId); + if (!result.success) return; + + this.selectedGoalId.set(null); + this.studentService.notifyDataChanged(); + await this.refetchProfile(); + } + onGoalCreated(goal: StudentGoalItem) { this.showGoalModal.set(null); this.studentService.notifyDataChanged(); @@ -156,6 +185,27 @@ export class Workspace { this.showEditBenchmarkModal.set('new'); } + onDeleteBenchmark(b: BenchmarkDto) { + this.deletingBenchmark.set(b); + this.showDeleteBenchmarkConfirm.set(true); + } + + // ***************************************************************** + // Called when the user confirms deletion in the confirm modal. + // Deletes the benchmark and its event associations. + // ***************************************************************** + async onDeleteBenchmarkConfirmed() { + this.showDeleteBenchmarkConfirm.set(false); + const b = this.deletingBenchmark(); + if (!b) return; + + this.deletingBenchmark.set(null); + const result = await this.studentService.deleteBenchmark(this.studentId()!, b.benchmarkId); + if (!result.success) return; + + await this.refetchProfile(); + } + onNewEvent() { this.showEditEventModal.set('new'); } @@ -169,6 +219,27 @@ export class Workspace { this.refetchProfile(); } + onDeleteEvent(ev: ProgressEventWithGoalDto) { + this.deletingEvent.set(ev); + this.showDeleteEventConfirm.set(true); + } + + // ***************************************************************** + // Called when the user confirms deletion in the confirm modal. + // Deletes the progress event and its benchmark associations. + // ***************************************************************** + async onDeleteEventConfirmed() { + this.showDeleteEventConfirm.set(false); + const ev = this.deletingEvent(); + if (!ev) return; + + this.deletingEvent.set(null); + const result = await this.studentService.deleteProgressEvent(this.studentId()!, ev.progressEventId); + if (!result.success) return; + + await this.refetchProfile(); + } + // ***************************************************************** // Returns the benchmark IDs associated with a given progress event, // read from the cached profile data. @@ -186,6 +257,26 @@ export class Workspace { formatDate = formatDate; + onDeleteStudent() { + this.showDeleteStudentConfirm.set(true); + } + + // ***************************************************************** + // Called when the user confirms student deletion. Deletes the + // student and navigates back to the home page. + // ***************************************************************** + async onDeleteStudentConfirmed() { + this.showDeleteStudentConfirm.set(false); + const id = this.studentId(); + if (!id) return; + + const result = await this.studentService.deleteStudent(id); + if (!result.success) return; + + this.studentService.notifyDataChanged(); + this.router.navigate(['/']); + } + // ********************** Support Procedures *********************** private async loadStudentData(studentId: string) { diff --git a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.scss b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.scss index eb666d7..efd0f84 100644 --- a/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.scss +++ b/ui/winstudentgoaltracker/src/app/desktop/pages/home/home.scss @@ -102,15 +102,11 @@ font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; - color: var(--text-faint); - padding: 12px 12px 4px; - margin-top: 4px; - border-top: 1px solid var(--border-color); - - &:first-child { - border-top: none; - margin-top: 0; - } + color: #4338CA; + background: #EEF2FF; + padding: 6px 12px; + margin-top: 6px; + border-left: 3px solid #818CF8; } .student-item { diff --git a/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts b/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts index 0d7ad1c..1ea961d 100644 --- a/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts +++ b/ui/winstudentgoaltracker/src/app/shared/services/student.service.ts @@ -145,6 +145,23 @@ export class StudentService { } } + // ***************************************************************** + // Deletes a goal and all its child entities (benchmarks, progress + // events, event-benchmark links). + // ***************************************************************** + async deleteGoal(studentId: string, goalId: string): Promise { + try { + const result = await firstValueFrom( + this.http.delete>(`${this.base}/api/Student/${studentId}/goals/${goalId}`) + ); + return result.success + ? ApiResult.empty() + : ApiResult.fail(result.message); + } catch (error) { + return ApiResult.fail(describeHttpError(error as HttpErrorResponse)); + } + } + // ***************************************************************** // Creates a new progress event, optionally with benchmark // associations. Returns the new progress event ID on success. @@ -178,6 +195,22 @@ export class StudentService { } } + // ***************************************************************** + // Deletes a progress event and its benchmark associations. + // ***************************************************************** + async deleteProgressEvent(studentId: string, progressEventId: string): Promise { + try { + const result = await firstValueFrom( + this.http.delete>(`${this.base}/api/Student/${studentId}/progress-events/${progressEventId}`) + ); + return result.success + ? ApiResult.empty() + : ApiResult.fail(result.message); + } catch (error) { + return ApiResult.fail(describeHttpError(error as HttpErrorResponse)); + } + } + // ***************************************************************** // Returns a full progress report for a student within a date // range, including goals, events, and benchmark associations. @@ -221,6 +254,22 @@ export class StudentService { } } + // ***************************************************************** + // Deletes a student and all associated data. + // ***************************************************************** + async deleteStudent(studentId: string): Promise { + try { + const result = await firstValueFrom( + this.http.delete>(`${this.base}/api/Student/${studentId}`) + ); + return result.success + ? ApiResult.empty() + : ApiResult.fail(result.message); + } catch (error) { + return ApiResult.fail(describeHttpError(error as HttpErrorResponse)); + } + } + // ***************************************************************** // Returns benchmarks for a given student. // ***************************************************************** @@ -269,6 +318,22 @@ export class StudentService { } } + // ***************************************************************** + // Deletes a benchmark and its progress-event associations. + // ***************************************************************** + async deleteBenchmark(studentId: string, benchmarkId: string): Promise { + try { + const result = await firstValueFrom( + this.http.delete>(`${this.base}/api/Student/${studentId}/benchmarks/${benchmarkId}`) + ); + return result.success + ? ApiResult.empty() + : ApiResult.fail(result.message); + } catch (error) { + return ApiResult.fail(describeHttpError(error as HttpErrorResponse)); + } + } + // ***************************************************************** // Requests an AI-generated benchmark recommendation for a goal. // *****************************************************************