From 79bf71ddd1a80603b86037df5650a0ff71fa544e Mon Sep 17 00:00:00 2001 From: Waylon Kenning Date: Sun, 5 Apr 2026 20:09:46 +1200 Subject: [PATCH 01/10] feat(training): Add edit functionality for training module Add ability for department admins to edit existing trainings: - Add Edit GET/POST actions to TrainingsController - Create EditTrainingModel view model - Create Edit.cshtml view with form for editing training details - Add Edit button to training index page (admin only) - Preserve existing attachments, questions, and user assignments - Support adding new attachments, users, groups, and roles - Add comprehensive unit tests for TrainingService Files added: - Tests/Resgrid.Tests/Mocks/MockTrainingRepository.cs - Tests/Resgrid.Tests/Mocks/MockTrainingAttachmentRepository.cs - Tests/Resgrid.Tests/Mocks/MockTrainingQuestionRepository.cs - Tests/Resgrid.Tests/Mocks/MockTrainingUserRepository.cs - Tests/Resgrid.Tests/Services/TrainingServiceTests.cs - Web/Resgrid.Web/Areas/User/Models/Training/EditTrainingModel.cs - Web/Resgrid.Web/Areas/User/Views/Trainings/Edit.cshtml - Web/Resgrid.Web/wwwroot/js/app/internal/training/resgrid.training.edittraining.js Files modified: - Web/Resgrid.Web/Areas/User/Controllers/TrainingsController.cs - Web/Resgrid.Web/Areas/User/Views/Trainings/Index.cshtml --- .../Mocks/MockTrainingAttachmentRepository.cs | 103 ++++ .../Mocks/MockTrainingQuestionRepository.cs | 103 ++++ .../Mocks/MockTrainingRepository.cs | 121 +++++ .../Mocks/MockTrainingUserRepository.cs | 106 ++++ .../Services/TrainingServiceTests.cs | 507 ++++++++++++++++++ .../User/Controllers/TrainingsController.cs | 233 ++++++++ .../User/Models/Training/EditTrainingModel.cs | 13 + .../Areas/User/Views/Trainings/Edit.cshtml | 272 ++++++++++ .../Areas/User/Views/Trainings/Index.cshtml | 3 + .../training/resgrid.training.edittraining.js | 105 ++++ 10 files changed, 1566 insertions(+) create mode 100644 Tests/Resgrid.Tests/Mocks/MockTrainingAttachmentRepository.cs create mode 100644 Tests/Resgrid.Tests/Mocks/MockTrainingQuestionRepository.cs create mode 100644 Tests/Resgrid.Tests/Mocks/MockTrainingRepository.cs create mode 100644 Tests/Resgrid.Tests/Mocks/MockTrainingUserRepository.cs create mode 100644 Tests/Resgrid.Tests/Services/TrainingServiceTests.cs create mode 100644 Web/Resgrid.Web/Areas/User/Models/Training/EditTrainingModel.cs create mode 100644 Web/Resgrid.Web/Areas/User/Views/Trainings/Edit.cshtml create mode 100644 Web/Resgrid.Web/wwwroot/js/app/internal/training/resgrid.training.edittraining.js diff --git a/Tests/Resgrid.Tests/Mocks/MockTrainingAttachmentRepository.cs b/Tests/Resgrid.Tests/Mocks/MockTrainingAttachmentRepository.cs new file mode 100644 index 00000000..db6263e8 --- /dev/null +++ b/Tests/Resgrid.Tests/Mocks/MockTrainingAttachmentRepository.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Resgrid.Model; +using Resgrid.Model.Repositories; + +namespace Resgrid.Tests.Mocks +{ + /// + /// In-memory mock for + /// + public sealed class MockTrainingAttachmentRepository : ITrainingAttachmentRepository + { + private readonly List _attachments = new List(); + private int _nextId = 1; + + public Task> GetAllAsync() + => Task.FromResult>(_attachments.ToList()); + + public Task GetByIdAsync(object id) + { + var intId = (int)id; + var attachment = _attachments.FirstOrDefault(a => a.TrainingAttachmentId == intId); + return Task.FromResult(attachment); + } + + public Task> GetAllByDepartmentIdAsync(int departmentId) + => Task.FromResult>(new List()); + + public Task> GetAllByUserIdAsync(string userId) + => Task.FromResult>(new List()); + + public Task InsertAsync(TrainingAttachment entity, CancellationToken cancellationToken, bool firstLevelOnly = false) + { + entity.TrainingAttachmentId = _nextId++; + _attachments.Add(entity); + return Task.FromResult(entity); + } + + public Task UpdateAsync(TrainingAttachment entity, CancellationToken cancellationToken, bool firstLevelOnly = false) + { + var existing = _attachments.FirstOrDefault(a => a.TrainingAttachmentId == entity.TrainingAttachmentId); + if (existing != null) + { + _attachments.Remove(existing); + } + _attachments.Add(entity); + return Task.FromResult(entity); + } + + public Task DeleteAsync(TrainingAttachment entity, CancellationToken cancellationToken) + { + var existing = _attachments.FirstOrDefault(a => a.TrainingAttachmentId == entity.TrainingAttachmentId); + if (existing != null) + { + _attachments.Remove(existing); + } + return Task.FromResult(true); + } + + public Task SaveOrUpdateAsync(TrainingAttachment entity, CancellationToken cancellationToken, bool firstLevelOnly = false) + { + if (entity.TrainingAttachmentId == 0) + { + entity.TrainingAttachmentId = _nextId++; + _attachments.Add(entity); + } + else + { + var existing = _attachments.FirstOrDefault(a => a.TrainingAttachmentId == entity.TrainingAttachmentId); + if (existing != null) + { + _attachments.Remove(existing); + } + _attachments.Add(entity); + } + return Task.FromResult(entity); + } + + public Task DeleteMultipleAsync(TrainingAttachment entity, string parentKeyName, object parentKeyId, List ids, CancellationToken cancellationToken) + => Task.FromResult(true); + + public Task> GetTrainingAttachmentsByTrainingIdAsync(int trainingId) + { + var result = _attachments.Where(a => a.TrainingId == trainingId).ToList(); + return Task.FromResult>(result); + } + + public void SeedAttachment(TrainingAttachment attachment) + { + if (attachment.TrainingAttachmentId == 0) + { + attachment.TrainingAttachmentId = _nextId++; + } + else + { + _nextId = System.Math.Max(_nextId, attachment.TrainingAttachmentId + 1); + } + _attachments.Add(attachment); + } + } +} \ No newline at end of file diff --git a/Tests/Resgrid.Tests/Mocks/MockTrainingQuestionRepository.cs b/Tests/Resgrid.Tests/Mocks/MockTrainingQuestionRepository.cs new file mode 100644 index 00000000..d47fe531 --- /dev/null +++ b/Tests/Resgrid.Tests/Mocks/MockTrainingQuestionRepository.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Resgrid.Model; +using Resgrid.Model.Repositories; + +namespace Resgrid.Tests.Mocks +{ + /// + /// In-memory mock for + /// + public sealed class MockTrainingQuestionRepository : ITrainingQuestionRepository + { + private readonly List _questions = new List(); + private int _nextId = 1; + + public Task> GetAllAsync() + => Task.FromResult>(_questions.ToList()); + + public Task GetByIdAsync(object id) + { + var intId = (int)id; + var question = _questions.FirstOrDefault(q => q.TrainingQuestionId == intId); + return Task.FromResult(question); + } + + public Task> GetAllByDepartmentIdAsync(int departmentId) + => Task.FromResult>(new List()); + + public Task> GetAllByUserIdAsync(string userId) + => Task.FromResult>(new List()); + + public Task InsertAsync(TrainingQuestion entity, CancellationToken cancellationToken, bool firstLevelOnly = false) + { + entity.TrainingQuestionId = _nextId++; + _questions.Add(entity); + return Task.FromResult(entity); + } + + public Task UpdateAsync(TrainingQuestion entity, CancellationToken cancellationToken, bool firstLevelOnly = false) + { + var existing = _questions.FirstOrDefault(q => q.TrainingQuestionId == entity.TrainingQuestionId); + if (existing != null) + { + _questions.Remove(existing); + } + _questions.Add(entity); + return Task.FromResult(entity); + } + + public Task DeleteAsync(TrainingQuestion entity, CancellationToken cancellationToken) + { + var existing = _questions.FirstOrDefault(q => q.TrainingQuestionId == entity.TrainingQuestionId); + if (existing != null) + { + _questions.Remove(existing); + } + return Task.FromResult(true); + } + + public Task SaveOrUpdateAsync(TrainingQuestion entity, CancellationToken cancellationToken, bool firstLevelOnly = false) + { + if (entity.TrainingQuestionId == 0) + { + entity.TrainingQuestionId = _nextId++; + _questions.Add(entity); + } + else + { + var existing = _questions.FirstOrDefault(q => q.TrainingQuestionId == entity.TrainingQuestionId); + if (existing != null) + { + _questions.Remove(existing); + } + _questions.Add(entity); + } + return Task.FromResult(entity); + } + + public Task DeleteMultipleAsync(TrainingQuestion entity, string parentKeyName, object parentKeyId, List ids, CancellationToken cancellationToken) + => Task.FromResult(true); + + public Task> GetTrainingQuestionsByTrainingIdAsync(int trainingId) + { + var result = _questions.Where(q => q.TrainingId == trainingId).ToList(); + return Task.FromResult>(result); + } + + public void SeedQuestion(TrainingQuestion question) + { + if (question.TrainingQuestionId == 0) + { + question.TrainingQuestionId = _nextId++; + } + else + { + _nextId = System.Math.Max(_nextId, question.TrainingQuestionId + 1); + } + _questions.Add(question); + } + } +} \ No newline at end of file diff --git a/Tests/Resgrid.Tests/Mocks/MockTrainingRepository.cs b/Tests/Resgrid.Tests/Mocks/MockTrainingRepository.cs new file mode 100644 index 00000000..f728b6e1 --- /dev/null +++ b/Tests/Resgrid.Tests/Mocks/MockTrainingRepository.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Resgrid.Model; +using Resgrid.Model.Repositories; + +namespace Resgrid.Tests.Mocks +{ + /// + /// In-memory mock for that stores trainings + /// without requiring a database connection. + /// + public sealed class MockTrainingRepository : ITrainingRepository + { + private readonly List _trainings = new List(); + private int _nextId = 1; + + public List Trainings => _trainings; + + public Task> GetAllAsync() + => Task.FromResult>(_trainings.ToList()); + + public Task GetByIdAsync(object id) + { + var intId = (int)id; + var training = _trainings.FirstOrDefault(t => t.TrainingId == intId); + return Task.FromResult(training); + } + + public Task> GetAllByDepartmentIdAsync(int departmentId) + { + var result = _trainings.Where(t => t.DepartmentId == departmentId).ToList(); + return Task.FromResult>(result); + } + + public Task> GetAllByUserIdAsync(string userId) + => Task.FromResult>(new List()); + + public Task InsertAsync(Training entity, CancellationToken cancellationToken, bool firstLevelOnly = false) + { + entity.TrainingId = _nextId++; + _trainings.Add(entity); + return Task.FromResult(entity); + } + + public Task UpdateAsync(Training entity, CancellationToken cancellationToken, bool firstLevelOnly = false) + { + var existing = _trainings.FirstOrDefault(t => t.TrainingId == entity.TrainingId); + if (existing != null) + { + _trainings.Remove(existing); + } + _trainings.Add(entity); + return Task.FromResult(entity); + } + + public Task DeleteAsync(Training entity, CancellationToken cancellationToken) + { + var existing = _trainings.FirstOrDefault(t => t.TrainingId == entity.TrainingId); + if (existing != null) + { + _trainings.Remove(existing); + } + return Task.FromResult(true); + } + + public Task SaveOrUpdateAsync(Training entity, CancellationToken cancellationToken, bool firstLevelOnly = false) + { + if (entity.TrainingId == 0) + { + entity.TrainingId = _nextId++; + _trainings.Add(entity); + } + else + { + var existing = _trainings.FirstOrDefault(t => t.TrainingId == entity.TrainingId); + if (existing != null) + { + _trainings.Remove(existing); + } + _trainings.Add(entity); + } + return Task.FromResult(entity); + } + + public Task DeleteMultipleAsync(Training entity, string parentKeyName, object parentKeyId, List ids, CancellationToken cancellationToken) + => Task.FromResult(true); + + public List GetAllTrainings() + => _trainings.ToList(); + + public Task> GetTrainingsByDepartmentIdAsync(int departmentId) + { + var result = _trainings.Where(t => t.DepartmentId == departmentId).ToList(); + return Task.FromResult>(result); + } + + public Task GetTrainingByTrainingIdAsync(int trainingId) + { + var training = _trainings.FirstOrDefault(t => t.TrainingId == trainingId); + return Task.FromResult(training); + } + + /// + /// Helper method to seed test data + /// + public void SeedTraining(Training training) + { + if (training.TrainingId == 0) + { + training.TrainingId = _nextId++; + } + else + { + _nextId = System.Math.Max(_nextId, training.TrainingId + 1); + } + _trainings.Add(training); + } + } +} \ No newline at end of file diff --git a/Tests/Resgrid.Tests/Mocks/MockTrainingUserRepository.cs b/Tests/Resgrid.Tests/Mocks/MockTrainingUserRepository.cs new file mode 100644 index 00000000..00c024dd --- /dev/null +++ b/Tests/Resgrid.Tests/Mocks/MockTrainingUserRepository.cs @@ -0,0 +1,106 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Resgrid.Model; +using Resgrid.Model.Repositories; + +namespace Resgrid.Tests.Mocks +{ + /// + /// In-memory mock for + /// + public sealed class MockTrainingUserRepository : ITrainingUserRepository + { + private readonly List _users = new List(); + private int _nextId = 1; + + public Task> GetAllAsync() + => Task.FromResult>(_users.ToList()); + + public Task GetByIdAsync(object id) + { + var intId = (int)id; + var user = _users.FirstOrDefault(u => u.TrainingUserId == intId); + return Task.FromResult(user); + } + + public Task> GetAllByDepartmentIdAsync(int departmentId) + => Task.FromResult>(new List()); + + public Task> GetAllByUserIdAsync(string userId) + { + var result = _users.Where(u => u.UserId == userId).ToList(); + return Task.FromResult>(result); + } + + public Task InsertAsync(TrainingUser entity, CancellationToken cancellationToken, bool firstLevelOnly = false) + { + entity.TrainingUserId = _nextId++; + _users.Add(entity); + return Task.FromResult(entity); + } + + public Task UpdateAsync(TrainingUser entity, CancellationToken cancellationToken, bool firstLevelOnly = false) + { + var existing = _users.FirstOrDefault(u => u.TrainingUserId == entity.TrainingUserId); + if (existing != null) + { + _users.Remove(existing); + } + _users.Add(entity); + return Task.FromResult(entity); + } + + public Task DeleteAsync(TrainingUser entity, CancellationToken cancellationToken) + { + var existing = _users.FirstOrDefault(u => u.TrainingUserId == entity.TrainingUserId); + if (existing != null) + { + _users.Remove(existing); + } + return Task.FromResult(true); + } + + public Task SaveOrUpdateAsync(TrainingUser entity, CancellationToken cancellationToken, bool firstLevelOnly = false) + { + if (entity.TrainingUserId == 0) + { + entity.TrainingUserId = _nextId++; + _users.Add(entity); + } + else + { + var existing = _users.FirstOrDefault(u => u.TrainingUserId == entity.TrainingUserId); + if (existing != null) + { + _users.Remove(existing); + } + _users.Add(entity); + } + return Task.FromResult(entity); + } + + public Task DeleteMultipleAsync(TrainingUser entity, string parentKeyName, object parentKeyId, List ids, CancellationToken cancellationToken) + => Task.FromResult(true); + + public Task GetTrainingUserByTrainingIdAndUserIdAsync(int trainingId, string userId) + { + var user = _users.FirstOrDefault(u => u.TrainingId == trainingId && u.UserId == userId); + return Task.FromResult(user); + } + + public void SeedUser(TrainingUser user) + { + if (user.TrainingUserId == 0) + { + user.TrainingUserId = _nextId++; + } + else + { + _nextId = System.Math.Max(_nextId, user.TrainingUserId + 1); + } + _users.Add(user); + } + } +} \ No newline at end of file diff --git a/Tests/Resgrid.Tests/Services/TrainingServiceTests.cs b/Tests/Resgrid.Tests/Services/TrainingServiceTests.cs new file mode 100644 index 00000000..1efd683d --- /dev/null +++ b/Tests/Resgrid.Tests/Services/TrainingServiceTests.cs @@ -0,0 +1,507 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using FluentAssertions; +using Moq; +using NUnit.Framework; +using Resgrid.Framework.Testing; +using Resgrid.Model; +using Resgrid.Model.Repositories; +using Resgrid.Model.Services; +using Resgrid.Services; +using Resgrid.Tests.Mocks; + +namespace Resgrid.Tests.Services +{ + [TestFixture] + public class TrainingServiceTests + { + private MockTrainingRepository _trainingRepository; + private MockTrainingAttachmentRepository _attachmentRepository; + private MockTrainingQuestionRepository _questionRepository; + private MockTrainingUserRepository _userRepository; + private Mock _communicationServiceMock; + private Mock _departmentServiceMock; + private TrainingService _trainingService; + + [SetUp] + public void SetUp() + { + _trainingRepository = new MockTrainingRepository(); + _attachmentRepository = new MockTrainingAttachmentRepository(); + _questionRepository = new MockTrainingQuestionRepository(); + _userRepository = new MockTrainingUserRepository(); + _communicationServiceMock = new Mock(); + _departmentServiceMock = new Mock(); + + _trainingService = new TrainingService( + _trainingRepository, + _attachmentRepository, + _userRepository, + _questionRepository, + _communicationServiceMock.Object, + _departmentServiceMock.Object + ); + } + + #region GetTrainingByIdAsync Tests + + [Test] + public async Task GetTrainingByIdAsync_Should_Return_Training_With_Questions_And_Attachments() + { + // Arrange + var training = new Training + { + TrainingId = 1, + DepartmentId = 1, + Name = "Fire Safety Training", + Description = "Basic fire safety", + TrainingText = "Learn fire safety basics", + CreatedByUserId = TestData.Users.TestUser1Id, + CreatedOn = DateTime.UtcNow + }; + _trainingRepository.SeedTraining(training); + + var question = new TrainingQuestion + { + TrainingQuestionId = 1, + TrainingId = 1, + Question = "What is the correct response to a fire?" + }; + _questionRepository.SeedQuestion(question); + + var attachment = new TrainingAttachment + { + TrainingAttachmentId = 1, + TrainingId = 1, + FileName = "fire_safety.pdf" + }; + _attachmentRepository.SeedAttachment(attachment); + + // Act + var result = await _trainingService.GetTrainingByIdAsync(1); + + // Assert + result.Should().NotBeNull(); + result.TrainingId.Should().Be(1); + result.Name.Should().Be("Fire Safety Training"); + result.Questions.Should().NotBeNull(); + result.Questions.Should().HaveCount(1); + result.Attachments.Should().NotBeNull(); + result.Attachments.Should().HaveCount(1); + } + + [Test] + public async Task GetTrainingByIdAsync_Should_Return_Null_For_NonExistent_Training() + { + // Act + var result = await _trainingService.GetTrainingByIdAsync(999); + + // Assert + result.Should().BeNull(); + } + + #endregion + + #region GetAllTrainingsForDepartmentAsync Tests + + [Test] + public async Task GetAllTrainingsForDepartmentAsync_Should_Return_Trainings_For_Department() + { + // Arrange + _trainingRepository.SeedTraining(new Training + { + TrainingId = 1, + DepartmentId = 1, + Name = "Training 1", + Description = "Description 1", + TrainingText = "Text 1", + CreatedByUserId = TestData.Users.TestUser1Id, + CreatedOn = DateTime.UtcNow + }); + _trainingRepository.SeedTraining(new Training + { + TrainingId = 2, + DepartmentId = 1, + Name = "Training 2", + Description = "Description 2", + TrainingText = "Text 2", + CreatedByUserId = TestData.Users.TestUser1Id, + CreatedOn = DateTime.UtcNow + }); + _trainingRepository.SeedTraining(new Training + { + TrainingId = 3, + DepartmentId = 2, + Name = "Training 3 (Other Dept)", + Description = "Description 3", + TrainingText = "Text 3", + CreatedByUserId = TestData.Users.TestUser5Id, + CreatedOn = DateTime.UtcNow + }); + + // Act + var result = await _trainingService.GetAllTrainingsForDepartmentAsync(1); + + // Assert + result.Should().NotBeNull(); + result.Should().HaveCount(2); + result.All(t => t.DepartmentId == 1).Should().BeTrue(); + } + + [Test] + public async Task GetAllTrainingsForDepartmentAsync_Should_Return_Empty_List_For_NonExistent_Department() + { + // Act + var result = await _trainingService.GetAllTrainingsForDepartmentAsync(999); + + // Assert + result.Should().NotBeNull(); + result.Should().BeEmpty(); + } + + #endregion + + #region SaveAsync Tests + + [Test] + public async Task SaveAsync_Should_Create_New_Training() + { + // Arrange + var training = new Training + { + Name = "New Training", + Description = "New Description", + TrainingText = "New Text", + DepartmentId = 1, + CreatedByUserId = TestData.Users.TestUser1Id, + CreatedOn = DateTime.UtcNow + }; + + // Act + var result = await _trainingService.SaveAsync(training); + + // Assert + result.Should().NotBeNull(); + result.TrainingId.Should().BeGreaterThan(0); + result.Name.Should().Be("New Training"); + } + + [Test] + public async Task SaveAsync_Should_Update_Existing_Training() + { + // Arrange + var existing = new Training + { + TrainingId = 1, + DepartmentId = 1, + Name = "Original Name", + Description = "Original Description", + TrainingText = "Original Text", + CreatedByUserId = TestData.Users.TestUser1Id, + CreatedOn = DateTime.UtcNow + }; + _trainingRepository.SeedTraining(existing); + + // Act + existing.Name = "Updated Name"; + existing.Description = "Updated Description"; + var result = await _trainingService.SaveAsync(existing); + + // Assert + result.Should().NotBeNull(); + result.TrainingId.Should().Be(1); + result.Name.Should().Be("Updated Name"); + result.Description.Should().Be("Updated Description"); + } + + [Test] + public async Task SaveAsync_Should_Save_Training_With_Questions() + { + // Arrange + var training = new Training + { + Name = "Quiz Training", + Description = "Training with questions", + TrainingText = "Text", + DepartmentId = 1, + CreatedByUserId = TestData.Users.TestUser1Id, + CreatedOn = DateTime.UtcNow, + Questions = new List + { + new TrainingQuestion + { + Question = "Question 1?", + Answers = new List + { + new TrainingQuestionAnswer { Answer = "Answer A", Correct = true }, + new TrainingQuestionAnswer { Answer = "Answer B", Correct = false } + } + } + } + }; + + // Act + var result = await _trainingService.SaveAsync(training); + + // Assert + result.Should().NotBeNull(); + result.Questions.Should().NotBeNull(); + result.Questions.Should().HaveCount(1); + } + + [Test] + public async Task SaveAsync_Should_Sanitize_Html_In_Description_And_Text() + { + // Arrange + var training = new Training + { + Name = "Training", + Description = "

Safe content

", + TrainingText = "

Safe training text

", + DepartmentId = 1, + CreatedByUserId = TestData.Users.TestUser1Id, + CreatedOn = DateTime.UtcNow + }; + + // Act + var result = await _trainingService.SaveAsync(training); + + // Assert + result.Description.Should().NotContain(" +} \ No newline at end of file diff --git a/Web/Resgrid.Web/Areas/User/Views/Trainings/Index.cshtml b/Web/Resgrid.Web/Areas/User/Views/Trainings/Index.cshtml index 4a800b19..7a168ef8 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Trainings/Index.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Trainings/Index.cshtml @@ -104,6 +104,9 @@ @if (ClaimsAuthorizationHelper.IsUserDepartmentAdmin()) { + + Edit + Report diff --git a/Web/Resgrid.Web/wwwroot/js/app/internal/training/resgrid.training.edittraining.js b/Web/Resgrid.Web/wwwroot/js/app/internal/training/resgrid.training.edittraining.js new file mode 100644 index 00000000..d58e7bd2 --- /dev/null +++ b/Web/Resgrid.Web/wwwroot/js/app/internal/training/resgrid.training.edittraining.js @@ -0,0 +1,105 @@ +var resgrid; +(function (resgrid) { + var training; + (function (training) { + var edittraining; + (function (edittraining) { + $(document).ready(function () { + resgrid.common.analytics.track('Training - Edit'); + + var quillDescription = new Quill('#editor-container', { + placeholder: '', + theme: 'snow' + }); + + var quillTraining = new Quill('#editor-container2', { + placeholder: '', + theme: 'snow' + }); + + $(document).on('submit', '#editTrainingForm', function () { + $('#Training_Description').val(quillDescription.root.innerHTML); + $('#Training_TrainingText').val(quillTraining.root.innerHTML); + + return true; + }); + + // Date picker - no time needed + $('#Training_ToBeCompletedBy').datetimepicker({ + timepicker: false, + format: 'm/d/Y', + scrollMonth: false, + scrollInput: false + }); + + // File upload: use native HTML file input (no Kendo Upload needed) + + $('#SendToAll').change(function () { + if (this.checked) { + $('#groupsToAdd').prop('disabled', true).trigger('change.select2'); + $('#rolesToAdd').prop('disabled', true).trigger('change.select2'); + $('#usersToAdd').prop('disabled', true).trigger('change.select2'); + } else { + $('#groupsToAdd').prop('disabled', false).trigger('change.select2'); + $('#rolesToAdd').prop('disabled', false).trigger('change.select2'); + $('#usersToAdd').prop('disabled', false).trigger('change.select2'); + } + }); + + function initSelect2(selector, placeholder, url) { + $(selector).select2({ + placeholder: placeholder, + allowClear: true, + ajax: { + url: url, + dataType: 'json', + processResults: function (data) { + return { + results: $.map(data, function (item) { + return { id: item.Id, text: item.Name }; + }) + }; + } + } + }); + } + + initSelect2('#groupsToAdd', 'Select groups...', resgrid.absoluteBaseUrl + '/User/Department/GetRecipientsForGrid?filter=1'); + initSelect2('#rolesToAdd', 'Select roles...', resgrid.absoluteBaseUrl + '/User/Department/GetRecipientsForGrid?filter=2'); + initSelect2('#usersToAdd', 'Select users...', resgrid.absoluteBaseUrl + '/User/Department/GetRecipientsForGrid?filter=3&filterSelf=true'); + resgrid.training.edittraining.questionsCount = 0; + }); + function addQuestion() { + resgrid.training.edittraining.questionsCount++; + $('#questions tbody').first().append("" + resgrid.training.edittraining.generateAnswersTable(edittraining.questionsCount) + ""); + } + edittraining.addQuestion = addQuestion; + function generateAnswersTable(count) { + var answersTable = '
AnsAnswer Text Add Answer
'; + return answersTable; + } + edittraining.generateAnswersTable = generateAnswersTable; + function addAnswer(count) { + var id = generate(4); + $('#answersTable_' + count + ' tbody').append(""); + } + edittraining.addAnswer = addAnswer; + function removeQuestion(index) { + $('#questionRow_' + index).remove(); + } + edittraining.removeQuestion = removeQuestion; + function generate(length) { + var arr = []; + var n; + for (var i = 0; i < length; i++) { + do + n = Math.floor(Math.random() * 20 + 1); + while (arr.indexOf(n) !== -1); + arr[i] = n; + } + return arr.join(''); + } + edittraining.generate = generate; + })(edittraining = training.edittraining || (training.edittraining = {})); + })(training = resgrid.training || (resgrid.training = {})); +})(resgrid || (resgrid = {})); \ No newline at end of file From 0f572084112276bd57cc5cc8e5548315c920a396 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 8 Apr 2026 19:23:12 -0700 Subject: [PATCH 02/10] Update Web/Resgrid.Web/Areas/User/Controllers/TrainingsController.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Web/Resgrid.Web/Areas/User/Controllers/TrainingsController.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Web/Resgrid.Web/Areas/User/Controllers/TrainingsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/TrainingsController.cs index 1c700297..0610e087 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/TrainingsController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/TrainingsController.cs @@ -280,8 +280,7 @@ public async Task Edit(int trainingId, EditTrainingModel model, I { if (file != null && file.Length > 0) { - var extension = file.FileName.Substring(file.FileName.IndexOf(char.Parse(".")) + 1, - file.FileName.Length - file.FileName.IndexOf(char.Parse(".")) - 1); +var extension = System.IO.Path.GetExtension(file.FileName)?.TrimStart('.') ?? string.Empty; if (!String.IsNullOrWhiteSpace(extension)) extension = extension.ToLower(); From 98166622f93d399ab42555ef64a33f360fce72c2 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 8 Apr 2026 19:23:41 -0700 Subject: [PATCH 03/10] Update Web/Resgrid.Web/Areas/User/Controllers/TrainingsController.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Areas/User/Controllers/TrainingsController.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Web/Resgrid.Web/Areas/User/Controllers/TrainingsController.cs b/Web/Resgrid.Web/Areas/User/Controllers/TrainingsController.cs index 0610e087..8081234b 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/TrainingsController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/TrainingsController.cs @@ -301,8 +301,9 @@ public async Task Edit(int trainingId, EditTrainingModel model, I attachment.FileName = file.FileName; attachment.TrainingId = trainingId; - var uploadedFile = new byte[file.OpenReadStream().Length]; - file.OpenReadStream().Read(uploadedFile, 0, uploadedFile.Length); +using var stream = file.OpenReadStream(); +var uploadedFile = new byte[stream.Length]; +stream.Read(uploadedFile, 0, uploadedFile.Length); attachment.Data = uploadedFile; existingTraining.Attachments.Add(attachment); From 8280959de1c116645927f42e45992357dc748d00 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 8 Apr 2026 19:24:28 -0700 Subject: [PATCH 04/10] Update Web/Resgrid.Web/Areas/User/Models/Training/EditTrainingModel.cs Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- .../Areas/User/Models/Training/EditTrainingModel.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Web/Resgrid.Web/Areas/User/Models/Training/EditTrainingModel.cs b/Web/Resgrid.Web/Areas/User/Models/Training/EditTrainingModel.cs index bcc3511f..9ac09c85 100644 --- a/Web/Resgrid.Web/Areas/User/Models/Training/EditTrainingModel.cs +++ b/Web/Resgrid.Web/Areas/User/Models/Training/EditTrainingModel.cs @@ -1,13 +1,16 @@ using System.Collections.Generic; using Resgrid.Model; +using System.Collections.Generic; + namespace Resgrid.Web.Areas.User.Models.Training { public class EditTrainingModel { - public Training Training { get; set; } + public Resgrid.Model.Training Training { get; set; } public string Message { get; set; } public bool SendToAll { get; set; } public List ExistingUserIds { get; set; } } +} } \ No newline at end of file From 7f5ab147b931db64eaf2f4f781702c94b0612104 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Wed, 8 Apr 2026 19:25:11 -0700 Subject: [PATCH 05/10] Update Web/Resgrid.Web/Areas/User/Views/Home/Dashboard.cshtml Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- Web/Resgrid.Web/Areas/User/Views/Home/Dashboard.cshtml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Web/Resgrid.Web/Areas/User/Views/Home/Dashboard.cshtml b/Web/Resgrid.Web/Areas/User/Views/Home/Dashboard.cshtml index b3056e7a..d88a1c73 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Home/Dashboard.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Home/Dashboard.cshtml @@ -222,7 +222,7 @@ -