mirror of
https://github.com/opelly27/WinStudentGoalTracker.git
synced 2026-05-20 11:07:41 +00:00
Edit button locate, delete, group header
This commit is contained in:
@@ -324,6 +324,38 @@ public class StudentController : BaseController
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{idStudent:guid}/goals/{idGoal:guid}")]
|
||||||
|
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin}")]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<ResponseResult<object>>> 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<object>
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Student not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleted = await _studentRepository.DeleteGoalAsync(idGoal);
|
||||||
|
|
||||||
|
return Ok(new ResponseResult<object>
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = deleted ? "Goal deleted." : "Goal not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[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)]
|
||||||
@@ -412,6 +444,38 @@ public class StudentController : BaseController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[HttpDelete("{idStudent:guid}/progress-events/{idProgressEvent:guid}")]
|
||||||
|
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.ProgramAdmin}")]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<ResponseResult<object>>> 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<object>
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Student not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleted = await _studentRepository.DeleteProgressEventAsync(idProgressEvent);
|
||||||
|
|
||||||
|
return Ok(new ResponseResult<object>
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = deleted ? "Progress event deleted." : "Progress event not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("progress-events/{idProgressEvent:guid}/benchmarks")]
|
[HttpGet("progress-events/{idProgressEvent:guid}/benchmarks")]
|
||||||
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")]
|
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")]
|
||||||
[ProducesResponseType(typeof(ResponseResult<List<Guid>>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ResponseResult<List<Guid>>), 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<object>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ResponseResult<object>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ActionResult<ResponseResult<object>>> 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<object>
|
||||||
|
{
|
||||||
|
Success = false,
|
||||||
|
Message = "Student not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var deleted = await _studentRepository.DeleteBenchmarkAsync(idBenchmark);
|
||||||
|
|
||||||
|
return Ok(new ResponseResult<object>
|
||||||
|
{
|
||||||
|
Success = true,
|
||||||
|
Message = deleted ? "Benchmark deleted." : "Benchmark not found."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[HttpGet("{idStudent:guid}/progress-report")]
|
[HttpGet("{idStudent:guid}/progress-report")]
|
||||||
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")]
|
[Authorize(Roles = $"{UserRoles.Teacher},{UserRoles.Paraeducator},{UserRoles.ProgramAdmin}")]
|
||||||
[ProducesResponseType(typeof(ResponseResult<string>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ResponseResult<string>), StatusCodes.Status200OK)]
|
||||||
|
|||||||
@@ -145,6 +145,19 @@ public class StudentRepository
|
|||||||
return rows.Select(r => r.benchmarkId is Guid g ? g : Guid.Parse((string)r.benchmarkId)).ToList();
|
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<bool> DeleteProgressEventAsync(Guid progressEventId)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
var rowsAffected = await db.ExecuteScalarAsync<int>(
|
||||||
|
"sp_ProgressEvent_Delete",
|
||||||
|
new { p_id_progress_event = progressEventId.ToString() },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rowsAffected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<Guid?> GetStudentIdForGoalAsync(Guid idGoal)
|
public async Task<Guid?> GetStudentIdForGoalAsync(Guid idGoal)
|
||||||
{
|
{
|
||||||
using var db = Connection;
|
using var db = Connection;
|
||||||
@@ -276,6 +289,20 @@ public class StudentRepository
|
|||||||
return rowsAffected > 0;
|
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<bool> DeleteGoalAsync(Guid goalId)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
var rowsAffected = await db.ExecuteScalarAsync<int>(
|
||||||
|
"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
|
// Returns all benchmarks for a student, grouped under a summary
|
||||||
// with the student identifier. Returns null if student not found.
|
// with the student identifier. Returns null if student not found.
|
||||||
@@ -367,6 +394,19 @@ public class StudentRepository
|
|||||||
return rowsAffected > 0;
|
return rowsAffected > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// *****************************************************************
|
||||||
|
// Deletes a benchmark and its progress-event associations.
|
||||||
|
// *****************************************************************
|
||||||
|
public async Task<bool> DeleteBenchmarkAsync(Guid benchmarkId)
|
||||||
|
{
|
||||||
|
using var db = Connection;
|
||||||
|
var rowsAffected = await db.ExecuteScalarAsync<int>(
|
||||||
|
"sp_Benchmark_Delete",
|
||||||
|
new { p_id_benchmark = benchmarkId.ToString() },
|
||||||
|
commandType: CommandType.StoredProcedure);
|
||||||
|
return rowsAffected > 0;
|
||||||
|
}
|
||||||
|
|
||||||
// *****************************************************************
|
// *****************************************************************
|
||||||
// Returns a full student profile: student card, goals, benchmarks,
|
// Returns a full student profile: student card, goals, benchmarks,
|
||||||
// progress events, and benchmark/event associations in one call.
|
// progress events, and benchmark/event associations in one call.
|
||||||
|
|||||||
@@ -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 ;
|
||||||
@@ -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 ;
|
||||||
@@ -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 ;
|
||||||
@@ -4,7 +4,7 @@ CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_Delete`(
|
|||||||
)
|
)
|
||||||
BEGIN
|
BEGIN
|
||||||
DELETE FROM `ReportPrompt`
|
DELETE FROM `ReportPrompt`
|
||||||
WHERE `id_report_prompt` = p_id_report_prompt;
|
WHERE `id_ReportPrompt` = p_id_report_prompt;
|
||||||
SELECT ROW_COUNT() AS rowsAffected;
|
SELECT ROW_COUNT() AS rowsAffected;
|
||||||
END;;
|
END;;
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ DELIMITER ;;
|
|||||||
CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_GetAll`()
|
CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_GetAll`()
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT
|
SELECT
|
||||||
`id_report_prompt` AS `reportPromptId`,
|
`id_ReportPrompt` AS `reportPromptId`,
|
||||||
`id_program` AS `programId`,
|
`id_program` AS `programId`,
|
||||||
`prompt` AS `prompt`,
|
`prompt` AS `prompt`,
|
||||||
`reportname` AS `reportname`
|
`reportname` AS `reportname`
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_GetById`(
|
|||||||
)
|
)
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT
|
SELECT
|
||||||
`id_report_prompt` AS `reportPromptId`,
|
`id_ReportPrompt` AS `reportPromptId`,
|
||||||
`id_program` AS `programId`,
|
`id_program` AS `programId`,
|
||||||
`prompt` AS `prompt`,
|
`prompt` AS `prompt`,
|
||||||
`reportname` AS `reportname`
|
`reportname` AS `reportname`
|
||||||
FROM `ReportPrompt`
|
FROM `ReportPrompt`
|
||||||
WHERE `id_report_prompt` = p_id_report_prompt
|
WHERE `id_ReportPrompt` = p_id_report_prompt
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
END;;
|
END;;
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_GetByReportname`(
|
|||||||
)
|
)
|
||||||
BEGIN
|
BEGIN
|
||||||
SELECT
|
SELECT
|
||||||
`id_report_prompt` AS `reportPromptId`,
|
`id_ReportPrompt` AS `reportPromptId`,
|
||||||
`id_program` AS `programId`,
|
`id_program` AS `programId`,
|
||||||
`prompt` AS `prompt`,
|
`prompt` AS `prompt`,
|
||||||
`reportname` AS `reportname`
|
`reportname` AS `reportname`
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_Insert`(
|
|||||||
BEGIN
|
BEGIN
|
||||||
INSERT INTO `ReportPrompt`
|
INSERT INTO `ReportPrompt`
|
||||||
(
|
(
|
||||||
`id_report_prompt`,
|
`id_ReportPrompt`,
|
||||||
`id_program`,
|
`id_program`,
|
||||||
`prompt`,
|
`prompt`,
|
||||||
`reportname`
|
`reportname`
|
||||||
@@ -21,12 +21,12 @@ BEGIN
|
|||||||
p_reportname
|
p_reportname
|
||||||
);
|
);
|
||||||
SELECT
|
SELECT
|
||||||
`id_report_prompt` AS `reportPromptId`,
|
`id_ReportPrompt` AS `reportPromptId`,
|
||||||
`id_program` AS `programId`,
|
`id_program` AS `programId`,
|
||||||
`prompt` AS `prompt`,
|
`prompt` AS `prompt`,
|
||||||
`reportname` AS `reportname`
|
`reportname` AS `reportname`
|
||||||
FROM `ReportPrompt`
|
FROM `ReportPrompt`
|
||||||
WHERE `id_report_prompt` = p_id_report_prompt
|
WHERE `id_ReportPrompt` = p_id_report_prompt
|
||||||
LIMIT 1;
|
LIMIT 1;
|
||||||
END;;
|
END;;
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ CREATE DEFINER=`root`@`%` PROCEDURE `sp_ReportPrompt_Update`(
|
|||||||
BEGIN
|
BEGIN
|
||||||
UPDATE `ReportPrompt`
|
UPDATE `ReportPrompt`
|
||||||
SET
|
SET
|
||||||
`prompt` = p_prompt,
|
`prompt` = COALESCE(p_prompt, `prompt`),
|
||||||
`reportname` = p_reportname
|
`reportname` = COALESCE(p_reportname, `reportname`)
|
||||||
WHERE `id_report_prompt` = p_id_report_prompt;
|
WHERE `id_ReportPrompt` = p_id_report_prompt;
|
||||||
SELECT ROW_COUNT() AS rowsAffected;
|
SELECT ROW_COUNT() AS rowsAffected;
|
||||||
END;;
|
END;;
|
||||||
DELIMITER ;
|
DELIMITER ;
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
DELIMITER ;;
|
DELIMITER ;;
|
||||||
CREATE DEFINER=`root`@`%` PROCEDURE `sp_Student_Delete`(IN p_id_student CHAR(36))
|
CREATE DEFINER=`root`@`%` PROCEDURE `sp_Student_Delete`(IN p_id_student CHAR(36))
|
||||||
BEGIN
|
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
|
DELETE FROM user_student
|
||||||
WHERE id_student = p_id_student;
|
WHERE id_student = p_id_student;
|
||||||
|
-- Remove the student
|
||||||
DELETE FROM student
|
DELETE FROM student
|
||||||
WHERE id_student = p_id_student;
|
WHERE id_student = p_id_student;
|
||||||
SELECT ROW_COUNT() AS rows_affected;
|
SELECT ROW_COUNT() AS rows_affected;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
<app-modal-shell [title]="title()" (closed)="onCancel()">
|
||||||
|
@if (!awaitingSecondConfirm()) {
|
||||||
|
<p class="confirm-message">{{ message() }}</p>
|
||||||
|
} @else {
|
||||||
|
<p class="confirm-message second-confirm-message">Are you absolutely sure? This action is permanent and cannot be undone.</p>
|
||||||
|
}
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button class="btn-secondary" (click)="onCancel()">{{ cancelLabel() }}</button>
|
||||||
|
@if (!awaitingSecondConfirm()) {
|
||||||
|
<button [class]="destructive() ? 'btn-danger' : 'btn-primary'" (click)="onConfirm()">
|
||||||
|
{{ confirmLabel() }}
|
||||||
|
</button>
|
||||||
|
} @else {
|
||||||
|
<button class="btn-danger-confirm" (click)="onConfirm()">
|
||||||
|
Yes, delete permanently
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</app-modal-shell>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<string>();
|
||||||
|
readonly confirmLabel = input('Delete');
|
||||||
|
readonly cancelLabel = input('Cancel');
|
||||||
|
readonly destructive = input(false);
|
||||||
|
readonly doubleConfirm = input(false);
|
||||||
|
|
||||||
|
readonly confirmed = output<void>();
|
||||||
|
readonly closed = output<void>();
|
||||||
|
|
||||||
|
// ************************** 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,12 +23,58 @@
|
|||||||
[event]="showEditEventModal() === 'new' ? null : $any(showEditEventModal())" (saved)="onEventSaved()"
|
[event]="showEditEventModal() === 'new' ? null : $any(showEditEventModal())" (saved)="onEventSaved()"
|
||||||
(closed)="showEditEventModal.set(null)" />
|
(closed)="showEditEventModal.set(null)" />
|
||||||
}
|
}
|
||||||
|
@if (showDeleteConfirm()) {
|
||||||
|
<app-confirm-modal
|
||||||
|
title="Delete Goal"
|
||||||
|
[message]="'Delete \u0022' + selectedGoal()!.category + '\u0022 goal and all its benchmarks and progress events? This cannot be undone.'"
|
||||||
|
confirmLabel="Delete"
|
||||||
|
[destructive]="true"
|
||||||
|
[doubleConfirm]="true"
|
||||||
|
(confirmed)="onDeleteConfirmed()"
|
||||||
|
(closed)="showDeleteConfirm.set(false)" />
|
||||||
|
}
|
||||||
|
@if (showDeleteBenchmarkConfirm() && deletingBenchmark()) {
|
||||||
|
<app-confirm-modal
|
||||||
|
title="Delete Benchmark"
|
||||||
|
[message]="'Delete \u0022' + (deletingBenchmark()!.shortName || deletingBenchmark()!.benchmark) + '\u0022? This cannot be undone.'"
|
||||||
|
confirmLabel="Delete"
|
||||||
|
[destructive]="true"
|
||||||
|
(confirmed)="onDeleteBenchmarkConfirmed()"
|
||||||
|
(closed)="showDeleteBenchmarkConfirm.set(false)" />
|
||||||
|
}
|
||||||
|
@if (showDeleteEventConfirm() && deletingEvent()) {
|
||||||
|
<app-confirm-modal
|
||||||
|
title="Delete Progress Event"
|
||||||
|
message="Delete this progress event? This cannot be undone."
|
||||||
|
confirmLabel="Delete"
|
||||||
|
[destructive]="true"
|
||||||
|
(confirmed)="onDeleteEventConfirmed()"
|
||||||
|
(closed)="showDeleteEventConfirm.set(false)" />
|
||||||
|
}
|
||||||
|
@if (showDeleteStudentConfirm()) {
|
||||||
|
<app-confirm-modal
|
||||||
|
title="Delete Student"
|
||||||
|
[message]="'Delete \u0022' + student()!.identifier + '\u0022 and all their goals, benchmarks, and progress events? This cannot be undone.'"
|
||||||
|
confirmLabel="Delete"
|
||||||
|
[destructive]="true"
|
||||||
|
[doubleConfirm]="true"
|
||||||
|
(confirmed)="onDeleteStudentConfirmed()"
|
||||||
|
(closed)="showDeleteStudentConfirm.set(false)" />
|
||||||
|
}
|
||||||
|
|
||||||
<!-- Student Header -->
|
<!-- Student Header -->
|
||||||
<div class="student-header">
|
<div class="student-header">
|
||||||
<div class="student-info">
|
<div class="student-info">
|
||||||
<h1 class="student-name">{{ student()!.identifier }}</h1>
|
<h1 class="student-name">{{ student()!.identifier }}</h1>
|
||||||
<span class="student-iep">IEP {{ formatDate(student()!.nextIepDate) }}</span>
|
<span class="student-iep">IEP {{ formatDate(student()!.nextIepDate) }}</span>
|
||||||
|
<button class="delete-student-btn" (click)="onDeleteStudent()" aria-label="Delete student" title="Delete student">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||||
|
<line x1="14" y1="11" x2="14" y2="17"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="goal-tabs">
|
<div class="goal-tabs">
|
||||||
<button class="goal-tab add-goal" (click)="onAddGoal()">+ Goal</button>
|
<button class="goal-tab add-goal" (click)="onAddGoal()">+ Goal</button>
|
||||||
@@ -47,11 +93,19 @@
|
|||||||
<!-- Goal Card -->
|
<!-- Goal Card -->
|
||||||
<div class="goal-card">
|
<div class="goal-card">
|
||||||
<div class="goal-card-header">
|
<div class="goal-card-header">
|
||||||
<span class="goal-badge">{{ selectedGoal()!.category }} Goal</span>
|
|
||||||
<app-edit-icon (click)="onEditGoal()" ariaLabel="Edit goal" />
|
<app-edit-icon (click)="onEditGoal()" ariaLabel="Edit goal" />
|
||||||
|
<span class="goal-badge">{{ selectedGoal()!.category }} Goal</span>
|
||||||
@if (selectedGoal()!.targetCompletionDate) {
|
@if (selectedGoal()!.targetCompletionDate) {
|
||||||
<span class="goal-due">Due {{ formatDate(selectedGoal()!.targetCompletionDate) }}</span>
|
<span class="goal-due">Due {{ formatDate(selectedGoal()!.targetCompletionDate) }}</span>
|
||||||
}
|
}
|
||||||
|
<button class="delete-goal-btn" (click)="onDeleteGoal()" aria-label="Delete goal" title="Delete goal">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||||
|
<line x1="14" y1="11" x2="14" y2="17"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="goal-description">{{ selectedGoal()!.description }}</p>
|
<p class="goal-description">{{ selectedGoal()!.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,8 +129,16 @@
|
|||||||
@for (b of goalBenchmarks(); track b.benchmarkId) {
|
@for (b of goalBenchmarks(); track b.benchmarkId) {
|
||||||
<div class="benchmark-card">
|
<div class="benchmark-card">
|
||||||
<div class="benchmark-header">
|
<div class="benchmark-header">
|
||||||
<span class="benchmark-name">{{ b.shortName || b.benchmark }}</span>
|
|
||||||
<app-edit-icon size="13" (click)="onEditBenchmark(b)" ariaLabel="Edit benchmark" />
|
<app-edit-icon size="13" (click)="onEditBenchmark(b)" ariaLabel="Edit benchmark" />
|
||||||
|
<span class="benchmark-name">{{ b.shortName || b.benchmark }}</span>
|
||||||
|
<button class="delete-benchmark-btn" (click)="onDeleteBenchmark(b)" aria-label="Delete benchmark" title="Delete benchmark">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||||
|
<line x1="14" y1="11" x2="14" y2="17"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="benchmark-desc">{{ b.benchmark }}</p>
|
<p class="benchmark-desc">{{ b.benchmark }}</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -94,8 +156,16 @@
|
|||||||
<div class="timeline-dot"></div>
|
<div class="timeline-dot"></div>
|
||||||
<div class="event-card">
|
<div class="event-card">
|
||||||
<div class="event-header">
|
<div class="event-header">
|
||||||
<span class="event-date">{{ formatDate(ev.createdAt) }}</span>
|
|
||||||
<app-edit-icon size="13" (click)="onEditEvent(ev)" ariaLabel="Edit event" color="#bbb" />
|
<app-edit-icon size="13" (click)="onEditEvent(ev)" ariaLabel="Edit event" color="#bbb" />
|
||||||
|
<span class="event-date">{{ formatDate(ev.createdAt) }}</span>
|
||||||
|
<button class="delete-event-btn" (click)="onDeleteEvent(ev)" aria-label="Delete event" title="Delete event">
|
||||||
|
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="3 6 5 6 21 6"/>
|
||||||
|
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||||
|
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||||
|
<line x1="14" y1="11" x2="14" y2="17"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p class="event-content">{{ ev.content }}</p>
|
<p class="event-content">{{ ev.content }}</p>
|
||||||
@if (getBenchmarksForEvent(ev.progressEventId).length > 0) {
|
@if (getBenchmarksForEvent(ev.progressEventId).length > 0) {
|
||||||
|
|||||||
@@ -43,6 +43,26 @@
|
|||||||
color: var(--text-faint);
|
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 {
|
.goal-tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
@@ -117,6 +137,24 @@
|
|||||||
margin-left: auto;
|
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 {
|
.goal-description {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
line-height: 1.55;
|
line-height: 1.55;
|
||||||
@@ -193,6 +231,25 @@
|
|||||||
margin: 0;
|
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 ─── */
|
/* ─── Progress Timeline ─── */
|
||||||
.timeline {
|
.timeline {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -251,6 +308,25 @@
|
|||||||
color: #333;
|
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 {
|
.event-benchmarks {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|||||||
@@ -9,13 +9,14 @@ import { GoalModal } from '../goal-modal/goal-modal';
|
|||||||
import { EditBenchmarkModal } from '../edit-benchmark-modal/edit-benchmark-modal';
|
import { EditBenchmarkModal } from '../edit-benchmark-modal/edit-benchmark-modal';
|
||||||
import { EditEventModal } from '../edit-event-modal/edit-event-modal';
|
import { EditEventModal } from '../edit-event-modal/edit-event-modal';
|
||||||
import { EditIcon } from '../edit-icon/edit-icon';
|
import { EditIcon } from '../edit-icon/edit-icon';
|
||||||
|
import { ConfirmModal } from '../confirm-modal/confirm-modal';
|
||||||
import { formatDate } from '../../../shared/utils/format-date';
|
import { formatDate } from '../../../shared/utils/format-date';
|
||||||
|
|
||||||
type TabView = 'benchmarks' | 'progress';
|
type TabView = 'benchmarks' | 'progress';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-workspace',
|
selector: 'app-workspace',
|
||||||
imports: [GoalModal, EditBenchmarkModal, EditEventModal, EditIcon],
|
imports: [GoalModal, EditBenchmarkModal, EditEventModal, EditIcon, ConfirmModal],
|
||||||
templateUrl: './workspace.html',
|
templateUrl: './workspace.html',
|
||||||
styleUrl: './workspace.scss',
|
styleUrl: './workspace.scss',
|
||||||
})
|
})
|
||||||
@@ -79,6 +80,12 @@ export class Workspace {
|
|||||||
protected readonly showGoalModal = signal<StudentGoalItem | 'add' | null>(null);
|
protected readonly showGoalModal = signal<StudentGoalItem | 'add' | null>(null);
|
||||||
protected readonly showEditBenchmarkModal = signal<BenchmarkDto | 'new' | null>(null);
|
protected readonly showEditBenchmarkModal = signal<BenchmarkDto | 'new' | null>(null);
|
||||||
protected readonly showEditEventModal = signal<ProgressEventWithGoalDto | null | 'new'>(null);
|
protected readonly showEditEventModal = signal<ProgressEventWithGoalDto | null | 'new'>(null);
|
||||||
|
protected readonly showDeleteConfirm = signal(false);
|
||||||
|
protected readonly showDeleteBenchmarkConfirm = signal(false);
|
||||||
|
protected readonly deletingBenchmark = signal<BenchmarkDto | null>(null);
|
||||||
|
protected readonly showDeleteEventConfirm = signal(false);
|
||||||
|
protected readonly deletingEvent = signal<ProgressEventWithGoalDto | null>(null);
|
||||||
|
protected readonly showDeleteStudentConfirm = signal(false);
|
||||||
|
|
||||||
// ************************** Properties ***************************
|
// ************************** Properties ***************************
|
||||||
|
|
||||||
@@ -135,6 +142,28 @@ export class Workspace {
|
|||||||
this.showGoalModal.set('add');
|
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) {
|
onGoalCreated(goal: StudentGoalItem) {
|
||||||
this.showGoalModal.set(null);
|
this.showGoalModal.set(null);
|
||||||
this.studentService.notifyDataChanged();
|
this.studentService.notifyDataChanged();
|
||||||
@@ -156,6 +185,27 @@ export class Workspace {
|
|||||||
this.showEditBenchmarkModal.set('new');
|
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() {
|
onNewEvent() {
|
||||||
this.showEditEventModal.set('new');
|
this.showEditEventModal.set('new');
|
||||||
}
|
}
|
||||||
@@ -169,6 +219,27 @@ export class Workspace {
|
|||||||
this.refetchProfile();
|
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,
|
// Returns the benchmark IDs associated with a given progress event,
|
||||||
// read from the cached profile data.
|
// read from the cached profile data.
|
||||||
@@ -186,6 +257,26 @@ export class Workspace {
|
|||||||
|
|
||||||
formatDate = formatDate;
|
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 ***********************
|
// ********************** Support Procedures ***********************
|
||||||
|
|
||||||
private async loadStudentData(studentId: string) {
|
private async loadStudentData(studentId: string) {
|
||||||
|
|||||||
@@ -102,15 +102,11 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
color: var(--text-faint);
|
color: #4338CA;
|
||||||
padding: 12px 12px 4px;
|
background: #EEF2FF;
|
||||||
margin-top: 4px;
|
padding: 6px 12px;
|
||||||
border-top: 1px solid var(--border-color);
|
margin-top: 6px;
|
||||||
|
border-left: 3px solid #818CF8;
|
||||||
&:first-child {
|
|
||||||
border-top: none;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.student-item {
|
.student-item {
|
||||||
|
|||||||
@@ -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<ApiResult> {
|
||||||
|
try {
|
||||||
|
const result = await firstValueFrom(
|
||||||
|
this.http.delete<ResponseResult<void>>(`${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
|
// Creates a new progress event, optionally with benchmark
|
||||||
// associations. Returns the new progress event ID on success.
|
// 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<ApiResult> {
|
||||||
|
try {
|
||||||
|
const result = await firstValueFrom(
|
||||||
|
this.http.delete<ResponseResult<void>>(`${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
|
// Returns a full progress report for a student within a date
|
||||||
// range, including goals, events, and benchmark associations.
|
// 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<ApiResult> {
|
||||||
|
try {
|
||||||
|
const result = await firstValueFrom(
|
||||||
|
this.http.delete<ResponseResult<void>>(`${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.
|
// 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<ApiResult> {
|
||||||
|
try {
|
||||||
|
const result = await firstValueFrom(
|
||||||
|
this.http.delete<ResponseResult<void>>(`${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.
|
// Requests an AI-generated benchmark recommendation for a goal.
|
||||||
// *****************************************************************
|
// *****************************************************************
|
||||||
|
|||||||
Reference in New Issue
Block a user