From 20463aafdad3fce72ad79c8ade5eabf9521a2083 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 22 Aug 2024 16:14:01 -0700 Subject: [PATCH 01/20] Web service to retrieve evaluatees by instance and indicate whether they've had an EPA done during their rotation --- .../CTS/Controllers/AssessmentController.cs | 65 +++++++++++++++++-- .../CTS/Models/EvaluateeWithEpaCompletion.cs | 47 ++++++++++++++ web/Classes/SQLContext/CtsContext.cs | 21 ++++++ web/Models/CTS/EvaluateesByInstance.cs | 24 +++++++ web/Models/CTS/Instance.cs | 14 ++++ 5 files changed, 165 insertions(+), 6 deletions(-) create mode 100644 web/Areas/CTS/Models/EvaluateeWithEpaCompletion.cs create mode 100644 web/Models/CTS/EvaluateesByInstance.cs create mode 100644 web/Models/CTS/Instance.cs diff --git a/web/Areas/CTS/Controllers/AssessmentController.cs b/web/Areas/CTS/Controllers/AssessmentController.cs index 81020ed..1dca877 100644 --- a/web/Areas/CTS/Controllers/AssessmentController.cs +++ b/web/Areas/CTS/Controllers/AssessmentController.cs @@ -16,12 +16,14 @@ namespace Viper.Areas.CTS.Controllers public class AssessmentController : ApiController { private readonly VIPERContext context; - private AuditService auditService; - private CtsSecurityService ctsSecurityService; + private readonly RAPSContext rapsContext; + private readonly AuditService auditService; + private readonly CtsSecurityService ctsSecurityService; public AssessmentController(VIPERContext _context, RAPSContext rapsContext) { context = _context; + this.rapsContext = rapsContext; auditService = new AuditService(context); ctsSecurityService = new CtsSecurityService(rapsContext, _context); } @@ -91,15 +93,16 @@ public async Task>> GetAssessments(int? typ switch (sortBy.ToLower()) { case "enteredon": assessments = descending ? assessments.OrderByDescending(a => a.EnteredOn) : assessments.OrderBy(a => a.EnteredOn); break; - case "enteredbyname": + case "enteredbyname": assessments = descending ? assessments.OrderByDescending(a => a.EnteredByPerson.LastName) .ThenByDescending(a => a.EnteredByPerson.FirstName) : assessments.OrderBy(a => a.EnteredByPerson.LastName) .ThenBy(a => a.EnteredByPerson.FirstName); break; - case "levelname": assessments = descending - ? assessments.OrderByDescending(a => a.Level != null ? a.Level.LevelName : "") + case "levelname": + assessments = descending + ? assessments.OrderByDescending(a => a.Level != null ? a.Level.LevelName : "") : assessments.OrderBy(a => a.Level != null ? a.Level.LevelName : ""); break; case "servicename": assessments = descending @@ -115,7 +118,7 @@ public async Task>> GetAssessments(int? typ .ThenByDescending(a => a.Student.FirstName) : assessments.OrderBy(a => a.Student.LastName) .ThenBy(a => a.Student.FirstName); - break; + break; } } if (pagination != null) @@ -206,6 +209,56 @@ public async Task> GetStudentAssessment(int enco return sa; } + /// + /// Given an Eval360 instance id, get the list of student evalautees for this evaluator and whether or not they have an EPA during + /// this rotation. + /// + /// + /// + [HttpGet("epacompletion")] + [Permission(Allow = "SVMSecure")] + public async Task>> EvalauteeStudentsWithEpas(int instanceId) + { + List evaluateesWithCompletion = new(); + var userHelper = new UserHelper(); + var user = userHelper.GetCurrentUser(); + //get instance and check to make sure it belongs to the logged in user, or the logged in user has an admin permission + var instance = await context.Instances.FindAsync(instanceId); + if (instance == null) + { + return NotFound(); + } + + if (instance.InstanceMothraId != user?.MothraId + && !userHelper.HasPermission(rapsContext, user, "SVMSecure.Eval.ViewStudentClinResultsAll") + && !userHelper.HasPermission(rapsContext, user, "SVMSecure.CTS.Manage")) + { + return Forbid(); + } + + //get evaluatees for this eval that are on the rotation for at least one week the logged in user is on this rotation + var evaluatees = await context.EvaluateesByInstances + .Where(e => e.InstanceId == instanceId) + .OrderBy(e => e.LastName) + .ThenBy(e => e.FirstName) + .ThenBy(e => e.PersonId) + .ToListAsync(); + + //for each evaluatee, get the epas for this service that are dated within the block start/end, and mark that an EPA has been done or not done + foreach (var e in evaluatees) + { + var epaAssessments = await context.Encounters + .Where(enc => enc.EncounterType == (int)EncounterCreationService.EncounterType.Epa) + .Where(enc => enc.EncounterDate >= e.StartDate && enc.EncounterDate <= e.EndDate) + .Where(enc => enc.Student.PersonId == e.PersonId) + .Where(enc => enc.ServiceId == e.ServiceId) + .CountAsync(); + evaluateesWithCompletion.Add(new(e, epaAssessments > 0)); + } + + return evaluateesWithCompletion; + } + /// /// Create a new epa assessment /// diff --git a/web/Areas/CTS/Models/EvaluateeWithEpaCompletion.cs b/web/Areas/CTS/Models/EvaluateeWithEpaCompletion.cs new file mode 100644 index 0000000..3d8500f --- /dev/null +++ b/web/Areas/CTS/Models/EvaluateeWithEpaCompletion.cs @@ -0,0 +1,47 @@ +using Viper.Models.CTS; + +namespace Viper.Areas.CTS.Models +{ + public class EvaluateeWithEpaCompletion + { + public int EvaluateeId { get; set; } + public int EvalId { get; set; } + public int RotId { get; set; } + public string Rotation { get; set; } = null!; + public int ServiceId { get; set; } + public string Service { get; set; } = null!; + public int StartWeekId { get; set; } + public DateTime StartDate { get; set; } + public int EndWeekId { get; set; } + public DateTime EndDate { get; set; } + public string InstructorMothraId { get; set; } = null!; + public int InstanceId { get; set; } + public string FirstName { get; set; } = null!; + public string LastName { get; set; } = null!; + public string MothraId { get; set; } = null!; + public int PersonId { get; set; } + public bool EpaDone { get; set; } + + public EvaluateeWithEpaCompletion() { } + public EvaluateeWithEpaCompletion(EvaluateesByInstance e, bool epaDone = false) + { + EvaluateeId = e.EvaluateeId; + EvalId = e.EvalId; + RotId = e.RotId; + Rotation = e.Rotation; + ServiceId = e.ServiceId; + Service = e.Service; + StartWeekId = e.StartWeekId; + StartDate = e.StartDate; + EndWeekId = e.EndWeekId; + EndDate = e.EndDate; + InstructorMothraId = e.InstructorMothraId; + InstanceId = e.InstanceId; + FirstName = e.FirstName; + LastName = e.LastName; + MothraId = e.MothraId; + PersonId = e.PersonId; + EpaDone = epaDone; + } + } +} diff --git a/web/Classes/SQLContext/CtsContext.cs b/web/Classes/SQLContext/CtsContext.cs index 176a5b8..35ac14b 100644 --- a/web/Classes/SQLContext/CtsContext.cs +++ b/web/Classes/SQLContext/CtsContext.cs @@ -29,6 +29,10 @@ public partial class VIPERContext : DbContext /* CREST */ public virtual DbSet CourseSessionOffering { get; set; } + /* Eval */ + public virtual DbSet Instances { get; set; } + public virtual DbSet EvaluateesByInstances { get; set; } + partial void OnModelCreatingCTS(ModelBuilder modelBuilder) { modelBuilder.Entity(entity => @@ -235,5 +239,22 @@ partial void OnModelCreatingCTS(ModelBuilder modelBuilder) entity.Property(e => e.CanvasCourseId).IsRequired(false); entity.Property(e => e.CanvasEventId).IsRequired(false); }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.InstanceId); + entity.ToTable("vwInstances", schema: "eval"); + entity.Property(e => e.InstanceMode).IsRequired(false); + entity.Property(e => e.InstanceDueDate).IsRequired(false); + entity.Property(e => e.InstanceStartWeek).IsRequired(false); + entity.Property(e => e.InstanceStartWeekId).IsRequired(false); + }); + + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.EvaluateeId); + entity.ToTable("vwEvaluateesByInstances", schema: "eval"); + }); } } diff --git a/web/Models/CTS/EvaluateesByInstance.cs b/web/Models/CTS/EvaluateesByInstance.cs new file mode 100644 index 0000000..01ca719 --- /dev/null +++ b/web/Models/CTS/EvaluateesByInstance.cs @@ -0,0 +1,24 @@ +using Viper.Models.CTS; + +namespace Viper.Models.CTS +{ + public class EvaluateesByInstance + { + public int EvaluateeId { get; set; } + public int EvalId { get; set; } + public int RotId { get; set; } + public string Rotation { get; set; } = null!; + public int ServiceId { get; set; } + public string Service { get; set; } = null!; + public int StartWeekId { get; set; } + public DateTime StartDate { get; set; } + public int EndWeekId { get; set; } + public DateTime EndDate { get; set; } + public string InstructorMothraId { get; set; } = null!; + public int InstanceId { get; set; } + public string FirstName { get; set; } = null!; + public string LastName { get; set; } = null!; + public string MothraId { get; set; } = null!; + public int PersonId { get; set; } + } +} diff --git a/web/Models/CTS/Instance.cs b/web/Models/CTS/Instance.cs new file mode 100644 index 0000000..3b41f6d --- /dev/null +++ b/web/Models/CTS/Instance.cs @@ -0,0 +1,14 @@ +namespace Viper.Models.CTS +{ + public class Instance + { + public int InstanceId { get; set; } + public int InstanceEvalId { get; set; } + public string InstanceMothraId { get; set; } = null!; + public string InstanceStatus { get; set; } = null!; + public string? InstanceMode { get; set; } + public DateTime? InstanceDueDate { get; set; } + public DateTime? InstanceStartWeek { get; set; } + public int? InstanceStartWeekId { get; set; } + } +} From 15d060544b415fca0d36ae3b3da98ff1445f2b23 Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 4 Sep 2024 15:41:50 -0700 Subject: [PATCH 02/20] Creating student app in VueApp --- VueApp/.env | 5 +- VueApp/.env.production | 5 +- VueApp/.env.test | 5 +- VueApp/src/Students/App.vue | 20 ++ VueApp/src/Students/index.html | 12 + .../src/Students/pages/StudentClassYear.vue | 247 ++++++++++++++++++ .../Students/pages/StudentClassYearImport.vue | 1 + VueApp/src/Students/pages/StudentsHome.vue | 5 + VueApp/src/Students/router/index.ts | 17 ++ VueApp/src/Students/router/routes.ts | 30 +++ VueApp/src/Students/students.ts | 26 ++ VueApp/src/Students/types/index.ts | 63 +++++ VueApp/vite.config.ts | 1 + VueApp/vueapp.esproj | 2 + .../Curriculum/Controllers/TermsController.cs | 1 + web/Program.cs | 3 +- 16 files changed, 436 insertions(+), 7 deletions(-) create mode 100644 VueApp/src/Students/App.vue create mode 100644 VueApp/src/Students/index.html create mode 100644 VueApp/src/Students/pages/StudentClassYear.vue create mode 100644 VueApp/src/Students/pages/StudentClassYearImport.vue create mode 100644 VueApp/src/Students/pages/StudentsHome.vue create mode 100644 VueApp/src/Students/router/index.ts create mode 100644 VueApp/src/Students/router/routes.ts create mode 100644 VueApp/src/Students/students.ts create mode 100644 VueApp/src/Students/types/index.ts diff --git a/VueApp/.env b/VueApp/.env index aabdd4e..1eed2ce 100644 --- a/VueApp/.env +++ b/VueApp/.env @@ -1,3 +1,4 @@ -VITE_API_URL="/api/" +VITE_API_URL = "/api/" VITE_VIPER_HOME = "/" -VITE_ENVIRONMENT="DEVELOPMENT" \ No newline at end of file +VITE_ENVIRONMENT = "DEVELOPMENT" +VITE_VIPER_1_HOME = "http://localhost/" \ No newline at end of file diff --git a/VueApp/.env.production b/VueApp/.env.production index d973edf..552e5ed 100644 --- a/VueApp/.env.production +++ b/VueApp/.env.production @@ -1,3 +1,4 @@ -VITE_API_URL="/2/api/" +VITE_API_URL = "/2/api/" VITE_VIPER_HOME = "/2/" -VITE_ENVIRONMENT="PRODUCTION" \ No newline at end of file +VITE_ENVIRONMENT = "PRODUCTION" +VITE_VIPER_1_HOME = "/" \ No newline at end of file diff --git a/VueApp/.env.test b/VueApp/.env.test index 4c11ddc..40e2c62 100644 --- a/VueApp/.env.test +++ b/VueApp/.env.test @@ -1,3 +1,4 @@ -VITE_API_URL="/2/api/" +VITE_API_URL = "/2/api/" VITE_VIPER_HOME = "/2/" -VITE_ENVIRONMENT="TEST" \ No newline at end of file +VITE_ENVIRONMENT = "TEST" +VITE_VIPER_1_HOME = "/" \ No newline at end of file diff --git a/VueApp/src/Students/App.vue b/VueApp/src/Students/App.vue new file mode 100644 index 0000000..e276969 --- /dev/null +++ b/VueApp/src/Students/App.vue @@ -0,0 +1,20 @@ + + + diff --git a/VueApp/src/Students/index.html b/VueApp/src/Students/index.html new file mode 100644 index 0000000..ec6c1ad --- /dev/null +++ b/VueApp/src/Students/index.html @@ -0,0 +1,12 @@ + + + + + + VIPER - Students + + +
+ + + diff --git a/VueApp/src/Students/pages/StudentClassYear.vue b/VueApp/src/Students/pages/StudentClassYear.vue new file mode 100644 index 0000000..9e0b637 --- /dev/null +++ b/VueApp/src/Students/pages/StudentClassYear.vue @@ -0,0 +1,247 @@ + + \ No newline at end of file diff --git a/VueApp/src/Students/pages/StudentClassYearImport.vue b/VueApp/src/Students/pages/StudentClassYearImport.vue new file mode 100644 index 0000000..d99b5af --- /dev/null +++ b/VueApp/src/Students/pages/StudentClassYearImport.vue @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/VueApp/src/Students/pages/StudentsHome.vue b/VueApp/src/Students/pages/StudentsHome.vue new file mode 100644 index 0000000..ad429b2 --- /dev/null +++ b/VueApp/src/Students/pages/StudentsHome.vue @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/VueApp/src/Students/router/index.ts b/VueApp/src/Students/router/index.ts new file mode 100644 index 0000000..7f01e74 --- /dev/null +++ b/VueApp/src/Students/router/index.ts @@ -0,0 +1,17 @@ +import { createRouter, createWebHistory } from 'vue-router' +import routes from './routes' +import useRequireLogin from '@/composables/RequireLogin' + +const baseUrl = import.meta.env.VITE_VIPER_HOME +const router = createRouter({ + scrollBehavior: () => ({ left: 0, top: 0 }), + history: createWebHistory(baseUrl), + routes, +}) + +router.beforeEach(async (to) => { + const { requireLogin } = useRequireLogin(to) + return requireLogin(true, "SVMSecure.Students") +}) + +export default router \ No newline at end of file diff --git a/VueApp/src/Students/router/routes.ts b/VueApp/src/Students/router/routes.ts new file mode 100644 index 0000000..8eb1d36 --- /dev/null +++ b/VueApp/src/Students/router/routes.ts @@ -0,0 +1,30 @@ +import ViperLayout from '@/layouts/ViperLayout.vue' +import ViperLayoutSimple from '@/layouts/ViperLayoutSimple.vue' + +const viperURL = import.meta.env.VITE_VIPER_HOME + +const routes = [ + { + path: '/Students/', + meta: { layout: ViperLayout }, + component: () => import('@/Students/pages/StudentsHome.vue'), + name: "StudentsHome" + }, + { + path: '/Students/Home', + meta: { layout: ViperLayout }, + component: () => import('@/Students/pages/StudentsHome.vue'), + }, + { + path: '/Students/StudentClassYear', + meta: { layout: ViperLayout }, + component: () => import('@/Students/pages/StudentClassYear.vue'), + }, + { + path: '/Students/StudentClassYearImport', + meta: { layout: ViperLayout }, + component: () => import('@/Students/pages/StudentClassYearImport.vue'), + }, +] + +export default routes \ No newline at end of file diff --git a/VueApp/src/Students/students.ts b/VueApp/src/Students/students.ts new file mode 100644 index 0000000..500d3b3 --- /dev/null +++ b/VueApp/src/Students/students.ts @@ -0,0 +1,26 @@ +//import './assets/main.css' + +import { createApp } from 'vue' +import { createPinia } from 'pinia'; +import router from './router' +import App from './App.vue' +import { Quasar, Loading, QSpinnerOval } from 'quasar' +// Import icon libraries +import '@quasar/extras/material-icons/material-icons.css' + +// Import Quasar css +import 'quasar/dist/quasar.css' +import { useQuasarConfig } from '@/composables/QuasarConfig' + +//import our css +import '@/assets/site.css' + +const { quasarConfig } = useQuasarConfig() +const pinia = createPinia() +const app = createApp(App) +app.provide("apiURL", import.meta.env.VITE_API_URL) +app.provide("viperOneUrl", import.meta.env.VITE_VIPER_1_HOME) +app.use(pinia) +app.use(router) +app.use(Quasar, quasarConfig) +app.mount('#myApp') \ No newline at end of file diff --git a/VueApp/src/Students/types/index.ts b/VueApp/src/Students/types/index.ts new file mode 100644 index 0000000..4b1ea9e --- /dev/null +++ b/VueApp/src/Students/types/index.ts @@ -0,0 +1,63 @@ + +export type Student = { + personId: number, + mailId: string, + lastName: string, + firstName: string, + middleName: string | null, + fullName: string, + classLevel: string | null, + termCode: number | null, + classYear: number | null, + email: string, + currentClassYear: boolean, + active: boolean, + classYears: StudentClassYear[], + +} + +export type StudentClassYear = { + studentClassYearId: number, + personId: number, + classYear: number, + active: boolean, + graduated: boolean, + ross: boolean, + leftTerm: number | null, + leftReason: number | null, + added: Date, + addedBy: number | null, + updated: Date | null, + updatedBy: number | null, + comment: string | null, + leftReasonText: string | null, +} + +export type StudentClassYearProblem = { + personId: number, + mailId: string, + lastName: string, + firstName: string, + middleName: string | null, + fullName: string, + classLevel: string | null, + termCode: number | null, + classYear: number | null, + email: string, + currentClassYear: boolean, + active: boolean, + classYears: StudentClassYear[], + expectedClassYear: number | null, + problems: string, +} + +export type StudentClassYearUpdate = { + studentClassYearId: number, + classYear: number | null, + personId: number | null, + ross: boolean | null, + leftReason: number | null, + leftTerm: number | null, + comment: string | null, + active: boolean, +} \ No newline at end of file diff --git a/VueApp/vite.config.ts b/VueApp/vite.config.ts index 6546da8..1cc4c8a 100644 --- a/VueApp/vite.config.ts +++ b/VueApp/vite.config.ts @@ -74,6 +74,7 @@ export default defineConfig(({ mode }) => ({ main: resolve(__dirname, 'index.html'), cts: resolve(__dirname, 'src/cts/index.html'), computing: resolve(__dirname, 'src/computing/index.html'), + students: resolve(__dirname, 'src/students/index.html'), } } }, diff --git a/VueApp/vueapp.esproj b/VueApp/vueapp.esproj index dda6a1e..d6487a6 100644 --- a/VueApp/vueapp.esproj +++ b/VueApp/vueapp.esproj @@ -14,5 +14,7 @@ + + \ No newline at end of file diff --git a/web/Areas/Curriculum/Controllers/TermsController.cs b/web/Areas/Curriculum/Controllers/TermsController.cs index ad02e08..a366746 100644 --- a/web/Areas/Curriculum/Controllers/TermsController.cs +++ b/web/Areas/Curriculum/Controllers/TermsController.cs @@ -9,6 +9,7 @@ namespace Viper.Areas.Curriculum.Controllers { [Route("/curriculum/terms")] + [Route("/api/curriculum/terms")] [Permission(Allow = "SVMSecure.Curriculum")] public class TermsController : ApiController { diff --git a/web/Program.cs b/web/Program.cs index d10f862..6f75abc 100644 --- a/web/Program.cs +++ b/web/Program.cs @@ -254,7 +254,8 @@ var baseUrl = app.Environment.IsDevelopment() ? "" : "2/"; RewriteOptions rewriteOptions = new RewriteOptions() .AddRewrite(@"(?i)^CTS", "/vue/src/cts/index.html", true) - .AddRewrite(@"(?i)^Computing", "/vue/src/computing/index.html", true); + .AddRewrite(@"(?i)^Computing", "/vue/src/computing/index.html", true) + .AddRewrite(@"(?i)^Students", "/vue/src/students/index.html", true); app.UseRewriter(rewriteOptions); //for the vue src files, use directories in the url but serve index.html From d8855ee69d09d4f9d3fcc5d484119fdd28edb230 Mon Sep 17 00:00:00 2001 From: Brandon Date: Wed, 4 Sep 2024 17:06:36 -0700 Subject: [PATCH 03/20] Class Year import --- .../src/Students/pages/StudentClassYear.vue | 14 +- .../Students/pages/StudentClassYearImport.vue | 126 +++++++++++++++++- .../Students/Controller/DvmController.cs | 2 +- web/Areas/Students/Services/StudentList.cs | 4 +- web/Classes/SQLContext/VIPERContext.cs | 4 + web/Models/VIPER/Person.cs | 1 + 6 files changed, 136 insertions(+), 15 deletions(-) diff --git a/VueApp/src/Students/pages/StudentClassYear.vue b/VueApp/src/Students/pages/StudentClassYear.vue index 9e0b637..17a7cbb 100644 --- a/VueApp/src/Students/pages/StudentClassYear.vue +++ b/VueApp/src/Students/pages/StudentClassYear.vue @@ -1,6 +1,6 @@ + + diff --git a/web/Areas/Students/Controller/DvmController.cs b/web/Areas/Students/Controller/DvmController.cs index 04c42f0..19334a5 100644 --- a/web/Areas/Students/Controller/DvmController.cs +++ b/web/Areas/Students/Controller/DvmController.cs @@ -47,7 +47,7 @@ public DvmStudentsController(VIPERContext context, RAPSContext rapsContext) { includeAllClassYears = false; } - var students = await studentList.GetStudents(classLevel: classLevel, classYear: classYear, activeYearOnly: !includeAllClassYears); + var students = await studentList.GetStudents(classLevel: classLevel, classYear: classYear, activeYearOnly: !includeAllClassYears, currentYearsOnly: false); return students; } diff --git a/web/Areas/Students/Services/StudentList.cs b/web/Areas/Students/Services/StudentList.cs index 29af66c..ee1121f 100644 --- a/web/Areas/Students/Services/StudentList.cs +++ b/web/Areas/Students/Services/StudentList.cs @@ -111,8 +111,8 @@ public async Task> GetStudentsByTermCodeAndClassLevel(int termCode { //Get students based on AAUD Student info for the given term var students = _context.People - .Include(p => p.StudentInfo) - .Where(p => p.StudentInfo != null && p.StudentInfo.TermCode == termCode && p.StudentInfo.ClassLevel == classLevel) + .Include(p => p.StudentHistory) + .Where(p => p.StudentHistory != null && p.StudentHistory.Any(h => h.TermCode == termCode && h.ClassLevel == classLevel)) .OrderBy(p => p.LastName) .ThenBy(p => p.FirstName); var studentList = await students diff --git a/web/Classes/SQLContext/VIPERContext.cs b/web/Classes/SQLContext/VIPERContext.cs index 982097b..f8d65c5 100644 --- a/web/Classes/SQLContext/VIPERContext.cs +++ b/web/Classes/SQLContext/VIPERContext.cs @@ -1322,6 +1322,10 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.HasKey("PersonId"); entity.HasOne(e => e.StudentInfo).WithMany() .HasForeignKey(e => new { e.StudentTerm, e.SpridenId }); + entity.HasMany(e => e.StudentHistory) + .WithOne() + .HasForeignKey(e => e.SpridenId) + .HasPrincipalKey(e => e.SpridenId); }); OnModelCreatingCTS(modelBuilder); OnModelCreatingStudents(modelBuilder); diff --git a/web/Models/VIPER/Person.cs b/web/Models/VIPER/Person.cs index a1d75e7..2e8a0f9 100644 --- a/web/Models/VIPER/Person.cs +++ b/web/Models/VIPER/Person.cs @@ -34,5 +34,6 @@ public partial class Person public DateTime? Inactivated { get; set; } public AaudStudent? StudentInfo { get; set; } + public IEnumerable? StudentHistory { get; set; } } } From aa8c72ebf9e428136930ffe47d6871c2c51161a3 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 12 Sep 2024 12:59:15 -0700 Subject: [PATCH 04/20] Adding remaining CTS db classes --- web/Areas/CTS/Services/CrestCourseService.cs | 5 +- web/Classes/SQLContext/CtsContext.cs | 259 +++++++++++++++++++ web/Controllers/HomeController.cs | 2 +- web/Models/CTS/Bundle.cs | 31 +++ web/Models/CTS/BundleCompetency.cs | 23 ++ web/Models/CTS/BundleCompetencyGroup.cs | 21 ++ web/Models/CTS/BundleLevel.cs | 16 ++ web/Models/CTS/BundleRole.cs | 16 ++ web/Models/CTS/BundleService.cs | 16 ++ web/Models/CTS/CompetencyOutcome.cs | 13 + web/Models/CTS/Course.cs | 15 ++ web/Models/CTS/CourseCompetency.cs | 19 ++ web/Models/CTS/CourseRole.cs | 16 ++ web/Models/CTS/Encounter.cs | 1 + web/Models/CTS/MilestoneLevel.cs | 18 ++ web/Models/CTS/Patient.cs | 18 ++ web/Models/CTS/Role.cs | 11 + web/Models/CTS/Session.cs | 17 ++ web/Models/CTS/SessionCompetency.cs | 24 ++ web/Models/CTS/StudentCompetency.cs | 46 ++++ 20 files changed, 584 insertions(+), 3 deletions(-) create mode 100644 web/Models/CTS/Bundle.cs create mode 100644 web/Models/CTS/BundleCompetency.cs create mode 100644 web/Models/CTS/BundleCompetencyGroup.cs create mode 100644 web/Models/CTS/BundleLevel.cs create mode 100644 web/Models/CTS/BundleRole.cs create mode 100644 web/Models/CTS/BundleService.cs create mode 100644 web/Models/CTS/CompetencyOutcome.cs create mode 100644 web/Models/CTS/Course.cs create mode 100644 web/Models/CTS/CourseCompetency.cs create mode 100644 web/Models/CTS/CourseRole.cs create mode 100644 web/Models/CTS/MilestoneLevel.cs create mode 100644 web/Models/CTS/Patient.cs create mode 100644 web/Models/CTS/Role.cs create mode 100644 web/Models/CTS/Session.cs create mode 100644 web/Models/CTS/SessionCompetency.cs create mode 100644 web/Models/CTS/StudentCompetency.cs diff --git a/web/Areas/CTS/Services/CrestCourseService.cs b/web/Areas/CTS/Services/CrestCourseService.cs index c00d5b0..e6abdcf 100644 --- a/web/Areas/CTS/Services/CrestCourseService.cs +++ b/web/Areas/CTS/Services/CrestCourseService.cs @@ -1,12 +1,13 @@ using Microsoft.EntityFrameworkCore; -using NuGet.Protocol; using Viper.Areas.CTS.Models; using Viper.Classes.SQLContext; using Viper.Models.CTS; +using Course = Viper.Areas.CTS.Models.Course; +using Session = Viper.Areas.CTS.Models.Session; namespace Viper.Areas.CTS.Services { - public class CrestCourseService + public class CrestCourseService { private readonly VIPERContext _context; public CrestCourseService(VIPERContext context) diff --git a/web/Classes/SQLContext/CtsContext.cs b/web/Classes/SQLContext/CtsContext.cs index 35ac14b..f8afd14 100644 --- a/web/Classes/SQLContext/CtsContext.cs +++ b/web/Classes/SQLContext/CtsContext.cs @@ -14,6 +14,20 @@ public partial class VIPERContext : DbContext public virtual DbSet Encounters { get; set; } public virtual DbSet EncounterInstructors { get; set; } public virtual DbSet CtsAudits { get; set; } + public virtual DbSet Roles { get; set; } + public virtual DbSet Bundles { get; set; } + public virtual DbSet BundleCompetencies { get; set; } + public virtual DbSet BundleCompetencyGroups { get; set; } + public virtual DbSet BundleLevels { get; set; } + public virtual DbSet BundleRoles { get; set; } + public virtual DbSet BundleServices { get; set; } + public virtual DbSet CompetencyOutcomes { get; set; } + public virtual DbSet CourseCompetencies { get; set; } + public virtual DbSet CourseRoles { get; set; } + public virtual DbSet MilestoneLevels { get; set; } + public virtual DbSet Patients { get; set; } + public virtual DbSet SessionCompetencies { get; set; } + public virtual DbSet StudentCompetencies { get; set; } /* Students */ public virtual DbSet DvmStudent { get; set; } @@ -119,6 +133,227 @@ partial void OnModelCreatingCTS(ModelBuilder modelBuilder) .HasForeignKey(e => e.EncounterId); }); + modelBuilder.Entity(entity => + { + entity.ToTable("Role", "cts"); + + entity.Property(e => e.Name) + .HasMaxLength(250) + .IsUnicode(false); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Bundle", "cts"); + + entity.Property(e => e.Name) + .HasMaxLength(500) + .IsUnicode(false); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("BundleCompetency", "cts"); + + entity.HasOne(d => d.BundleCompetencyGroup).WithMany(p => p.BundleCompetencies) + .HasForeignKey(d => d.BundleCompetencyGroupId) + .HasConstraintName("FK_BundleCompetency_BundleCompetencyGroupId"); + + entity.HasOne(d => d.Bundle).WithMany(p => p.BundleCompetencies) + .HasForeignKey(d => d.BundleId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_BundleCompetency_Bundle"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.BundleCompetencyGroupId).HasName("PK_BundleCompetencyGroupId"); + + entity.ToTable("BundleCompetencyGroup", "cts"); + + entity.Property(e => e.Name) + .HasMaxLength(500) + .IsUnicode(false); + + entity.HasOne(d => d.Bundle).WithMany(p => p.BundleCompetencyGroups) + .HasForeignKey(d => d.BundleId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_BundleCompetencyGroupId_Bundle"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("BundleLevel", "cts"); + + entity.HasOne(d => d.Bundle).WithMany(p => p.BundleLevels) + .HasForeignKey(d => d.BundleId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_BundleLevel_Bundle"); + + entity.HasOne(d => d.Level).WithMany() + .HasForeignKey(d => d.LevelId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_BundleLevel_Level"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("BundleRole", "cts"); + + entity.HasOne(d => d.Bundle).WithMany(p => p.BundleRoles) + .HasForeignKey(d => d.BundleId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_BundleRole_Bundle"); + entity.HasOne(d => d.Role).WithMany() + .HasForeignKey(d => d.RoleId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_BundleRole_Role"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("BundleService", "cts"); + + entity.HasOne(d => d.Bundle).WithMany(p => p.BundleServices) + .HasForeignKey(d => d.BundleId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_BundleService_Bundle"); + entity.HasOne(d => d.Service).WithMany() + .HasForeignKey(d => d.ServiceId) + .OnDelete(DeleteBehavior.ClientSetNull); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("CompetencyOutcome", "cts"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("CourseCompetency", "cts"); + entity.HasOne(d => d.Course).WithMany() + .HasForeignKey(d => d.CourseId) + .OnDelete(DeleteBehavior.ClientSetNull); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("CourseRole", "cts"); + entity.HasOne(d => d.Course).WithMany() + .HasForeignKey(d => d.CourseId) + .OnDelete(DeleteBehavior.ClientSetNull); + entity.HasOne(d => d.Role).WithMany() + .HasForeignKey(d => d.RoleId) + .OnDelete(DeleteBehavior.ClientSetNull); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("MilestoneLevel", "cts"); + + entity.Property(e => e.Description).IsUnicode(false); + + entity.HasOne(d => d.Bundle).WithMany(p => p.MilestoneLevels) + .HasForeignKey(d => d.BundleId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_MilestoneLevel_Bundle"); + entity.HasOne(d => d.Level).WithMany() + .HasForeignKey(d => d.LevelId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_MilestoneLevel_MilestoneLevel"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("Patient", "cts"); + + entity.Property(e => e.PatientId).ValueGeneratedNever(); + entity.Property(e => e.Gender) + .HasMaxLength(100) + .IsUnicode(false); + entity.Property(e => e.PatientName) + .HasMaxLength(500) + .IsUnicode(false); + entity.Property(e => e.Species) + .HasMaxLength(100) + .IsUnicode(false); + entity.HasOne(d => d.Encounter).WithOne(d => d.Patient) + .HasForeignKey(d => d.PatientId) + .IsRequired(false); + + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("SessionCompetency", "cts"); + entity.HasKey(e => e.SessionCompetencyId); + entity.HasOne(d => d.Competency).WithMany() + .HasForeignKey(d => d.CompetencyId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_SessionCompetency_Competency"); + entity.HasOne(d => d.Session).WithMany() + .HasForeignKey(d => d.SessionId) + .OnDelete(DeleteBehavior.ClientSetNull); + entity.HasOne(d => d.Level).WithMany() + .HasForeignKey(d => d.LevelId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_SessionCompetency_Level"); + entity.HasOne(d => d.Role).WithMany() + .HasForeignKey(d => d.RoleId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_SessionCompetency_Role") + .IsRequired(false); + + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("StudentCompetency", "cts"); + + entity.Property(e => e.Added).HasColumnType("datetime"); + entity.Property(e => e.Updated).HasColumnType("datetime"); + entity.Property(e => e.VerifiedTimestamp).HasColumnType("datetime"); + + entity.HasOne(d => d.BundleGroup).WithMany() + .HasForeignKey(d => d.BundleGroupId) + .HasConstraintName("FK_StudentCompetency_BundleCompetencyGroupId"); + + entity.HasOne(d => d.Bundle).WithMany() + .HasForeignKey(d => d.BundleId) + .HasConstraintName("FK_StudentCompetency_Bundle"); + + entity.HasOne(d => d.Person).WithMany() + .HasForeignKey(d => d.StudentUserId) + .HasConstraintName("FK_StudentCompetency_Student") + .OnDelete(DeleteBehavior.ClientSetNull); + entity.HasOne(d => d.Competency).WithMany() + .HasForeignKey(d => d.CompetencyId) + .HasConstraintName("FK_StudentCompetency_Competency") + .OnDelete(DeleteBehavior.ClientSetNull); + entity.HasOne(d => d.Level).WithMany() + .HasForeignKey(d => d.LevelId) + .HasConstraintName("FK_StudentCompetency_Level") + .OnDelete(DeleteBehavior.ClientSetNull); + entity.HasOne(d => d.Encounter).WithMany() + .HasForeignKey(d => d.EncounterId) + .HasConstraintName("FK_StudentCompetency_Encounter") + .OnDelete(DeleteBehavior.ClientSetNull) + .IsRequired(false); + entity.HasOne(d => d.Course).WithMany() + .HasForeignKey(d => d.CourseId) + .OnDelete(DeleteBehavior.ClientSetNull) + .IsRequired(false); + entity.HasOne(d => d.Session).WithMany() + .HasForeignKey(d => d.SessionId) + .OnDelete(DeleteBehavior.ClientSetNull) + .IsRequired(false); + entity.HasOne(d => d.Verifier).WithMany() + .HasForeignKey(d => d.VerifiedBy) + .HasConstraintName("FK_StudentCompetency_VerifiedPerson") + .OnDelete(DeleteBehavior.ClientSetNull) + .IsRequired(false); + }); + /* "Exteral" entities */ modelBuilder.Entity(entity => { @@ -240,6 +475,30 @@ partial void OnModelCreatingCTS(ModelBuilder modelBuilder) entity.Property(e => e.CanvasEventId).IsRequired(false); }); + modelBuilder.Entity(entity => + { + entity.ToTable("vwCourse", schema: "cts"); + entity.HasKey(e => e.CourseId); + entity.Property(e => e.Description).IsRequired(false); + entity.Property(e => e.Crn).IsRequired(false); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("vwSession", schema: "cts"); + entity.HasKey(e => e.SessionId); + entity.Property(e => e.Type).IsRequired(false); + entity.Property(e => e.TypeDescription).IsRequired(false); + entity.Property(e => e.Description).IsRequired(false); + }); + + /* + modelBuilder.Entity(entity => + { + + }); + */ + modelBuilder.Entity(entity => { entity.HasKey(e => e.InstanceId); diff --git a/web/Controllers/HomeController.cs b/web/Controllers/HomeController.cs index f74a817..a5ad3b9 100644 --- a/web/Controllers/HomeController.cs +++ b/web/Controllers/HomeController.cs @@ -56,7 +56,7 @@ public IActionResult Index() [Authorize(Policy = "2faAuthentication")] [Permission(Allow = "SVMSecure")] public IActionResult Policy() - { + { return View(); } diff --git a/web/Models/CTS/Bundle.cs b/web/Models/CTS/Bundle.cs new file mode 100644 index 0000000..5f1599a --- /dev/null +++ b/web/Models/CTS/Bundle.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class Bundle +{ + public int BundleId { get; set; } + + public string Name { get; set; } = null!; + + public bool Clinical { get; set; } + + public bool Assessment { get; set; } + + public bool Milestone { get; set; } + + public virtual ICollection BundleCompetencies { get; set; } = new List(); + + public virtual ICollection BundleCompetencyGroups { get; set; } = new List(); + + public virtual ICollection BundleLevels { get; set; } = new List(); + + public virtual ICollection BundleRoles { get; set; } = new List(); + + public virtual ICollection BundleServices { get; set; } = new List(); + + public virtual ICollection MilestoneLevels { get; set; } = new List(); + + //public virtual ICollection StudentCompetencies { get; set; } = new List(); +} diff --git a/web/Models/CTS/BundleCompetency.cs b/web/Models/CTS/BundleCompetency.cs new file mode 100644 index 0000000..13ca2d6 --- /dev/null +++ b/web/Models/CTS/BundleCompetency.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class BundleCompetency +{ + public int BundleCompetencyId { get; set; } + + public int BundleId { get; set; } + + public int? BundleCompetencyGroupId { get; set; } + + public int? RoleId { get; set; } + + public int CompetencyId { get; set; } + + public int Order { get; set; } + + public virtual Bundle Bundle { get; set; } = null!; + + public virtual BundleCompetencyGroup? BundleCompetencyGroup { get; set; } +} diff --git a/web/Models/CTS/BundleCompetencyGroup.cs b/web/Models/CTS/BundleCompetencyGroup.cs new file mode 100644 index 0000000..6c4b2b1 --- /dev/null +++ b/web/Models/CTS/BundleCompetencyGroup.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class BundleCompetencyGroup +{ + public int BundleCompetencyGroupId { get; set; } + + public int BundleId { get; set; } + + public string Name { get; set; } = null!; + + public int Order { get; set; } + + public virtual Bundle Bundle { get; set; } = null!; + + public virtual ICollection BundleCompetencies { get; set; } = new List(); + + //public virtual ICollection StudentCompetencies { get; set; } = new List(); +} diff --git a/web/Models/CTS/BundleLevel.cs b/web/Models/CTS/BundleLevel.cs new file mode 100644 index 0000000..81c5966 --- /dev/null +++ b/web/Models/CTS/BundleLevel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class BundleLevel +{ + public int BundleLevelId { get; set; } + + public int BundleId { get; set; } + + public int LevelId { get; set; } + + public virtual Bundle Bundle { get; set; } = null!; + public virtual Level Level { get; set; } = null!; +} diff --git a/web/Models/CTS/BundleRole.cs b/web/Models/CTS/BundleRole.cs new file mode 100644 index 0000000..e63d8b4 --- /dev/null +++ b/web/Models/CTS/BundleRole.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class BundleRole +{ + public int BundleRoleId { get; set; } + + public int BundleId { get; set; } + + public int RoleId { get; set; } + + public virtual Bundle Bundle { get; set; } = null!; + public virtual Role Role { get; set; } = null!; +} diff --git a/web/Models/CTS/BundleService.cs b/web/Models/CTS/BundleService.cs new file mode 100644 index 0000000..61e8b6c --- /dev/null +++ b/web/Models/CTS/BundleService.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class BundleService +{ + public int BundleServiceId { get; set; } + + public int BundleId { get; set; } + + public int ServiceId { get; set; } + + public virtual Bundle Bundle { get; set; } = null!; + public virtual Service Service { get; set; } = null!; +} diff --git a/web/Models/CTS/CompetencyOutcome.cs b/web/Models/CTS/CompetencyOutcome.cs new file mode 100644 index 0000000..747d162 --- /dev/null +++ b/web/Models/CTS/CompetencyOutcome.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class CompetencyOutcome +{ + public int CompetencyOutcomeId { get; set; } + + public int CompetencyId { get; set; } + + public int OutcomeId { get; set; } +} diff --git a/web/Models/CTS/Course.cs b/web/Models/CTS/Course.cs new file mode 100644 index 0000000..c5b729d --- /dev/null +++ b/web/Models/CTS/Course.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace Viper.Models.CTS; + +public class Course +{ + [Key] + public int CourseId { get; set; } + public string Status { get; set; } = null!; + public string Title { get; set; } = null!; + public string? Description { get; set; } + public string AcademicYear { get; set; } = null!; + public string? Crn { get; set; } + public string CourseNum { get; set; } = null!; +} diff --git a/web/Models/CTS/CourseCompetency.cs b/web/Models/CTS/CourseCompetency.cs new file mode 100644 index 0000000..27c21bf --- /dev/null +++ b/web/Models/CTS/CourseCompetency.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class CourseCompetency +{ + public int CourseCompetencyId { get; set; } + + public int CourseId { get; set; } + + public int CompetencyId { get; set; } + + public int LevelId { get; set; } + + public int Order { get; set; } + + public virtual Course Course { get; set; } = null!; +} diff --git a/web/Models/CTS/CourseRole.cs b/web/Models/CTS/CourseRole.cs new file mode 100644 index 0000000..b90b855 --- /dev/null +++ b/web/Models/CTS/CourseRole.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class CourseRole +{ + public int CourseRoleId { get; set; } + + public int CourseId { get; set; } + + public int RoleId { get; set; } + + public virtual Role Role { get; set; } = null!; + public virtual Course Course { get; set; } = null!; +} diff --git a/web/Models/CTS/Encounter.cs b/web/Models/CTS/Encounter.cs index 6c211f0..c04da78 100644 --- a/web/Models/CTS/Encounter.cs +++ b/web/Models/CTS/Encounter.cs @@ -35,6 +35,7 @@ public class Encounter public virtual CourseSessionOffering? Offering { get; set; } public virtual Service? Service { get; set; } public virtual ICollection EncounterInstructors { get; set; } = new List(); + public virtual Patient? Patient { get; set; } //For assessments public virtual Epa? Epa { get; set; } = null!; diff --git a/web/Models/CTS/MilestoneLevel.cs b/web/Models/CTS/MilestoneLevel.cs new file mode 100644 index 0000000..ea2138d --- /dev/null +++ b/web/Models/CTS/MilestoneLevel.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class MilestoneLevel +{ + public int MilestoneLevelId { get; set; } + + public int LevelId { get; set; } + + public int BundleId { get; set; } + + public string Description { get; set; } = null!; + + public virtual Bundle Bundle { get; set; } = null!; + public virtual Level Level { get; set; } = null!; +} diff --git a/web/Models/CTS/Patient.cs b/web/Models/CTS/Patient.cs new file mode 100644 index 0000000..45f23fa --- /dev/null +++ b/web/Models/CTS/Patient.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class Patient +{ + public int PatientId { get; set; } + + public int? PatientNumber { get; set; } + + public string PatientName { get; set; } = null!; + + public string Gender { get; set; } = null!; + + public string Species { get; set; } = null!; + public virtual Encounter Encounter { get; set; } = null!; +} diff --git a/web/Models/CTS/Role.cs b/web/Models/CTS/Role.cs new file mode 100644 index 0000000..7528e4e --- /dev/null +++ b/web/Models/CTS/Role.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class Role +{ + public int RoleId { get; set; } + + public string Name { get; set; } = null!; +} diff --git a/web/Models/CTS/Session.cs b/web/Models/CTS/Session.cs new file mode 100644 index 0000000..88ecd47 --- /dev/null +++ b/web/Models/CTS/Session.cs @@ -0,0 +1,17 @@ +namespace Viper.Models.CTS; + +public class Session +{ + public int SessionId { get; set; } + public string Status { get; set; } = null!; + public string? Type { get; set; } + public string? TypeDescription { get; set; } + public string Title { get; set; } = null!; + public string CourseTitle { get; set; } = null!; + public string? Description { get; set; } + public int CourseId { get; set; } + public string AcademicYear { get; set; } = null!; + public int TypeOrder { get; set; } + public int PaceOrder { get; set; } + +} diff --git a/web/Models/CTS/SessionCompetency.cs b/web/Models/CTS/SessionCompetency.cs new file mode 100644 index 0000000..75340eb --- /dev/null +++ b/web/Models/CTS/SessionCompetency.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class SessionCompetency +{ + public int SessionCompetencyId { get; set; } + + public int SessionId { get; set; } + + public int CompetencyId { get; set; } + + public int LevelId { get; set; } + + public int? RoleId { get; set; } + + public int Order { get; set; } + + public virtual Session Session { get; set; } = null!; + public virtual Competency Competency { get; set; } = null!; + public virtual Level Level { get; set; } = null!; + public virtual Role? Role { get; set; } +} diff --git a/web/Models/CTS/StudentCompetency.cs b/web/Models/CTS/StudentCompetency.cs new file mode 100644 index 0000000..9735daa --- /dev/null +++ b/web/Models/CTS/StudentCompetency.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using Viper.Models.VIPER; + +namespace Viper.Models.CTS; + +public partial class StudentCompetency +{ + public int StudentCompetencyId { get; set; } + + public int StudentUserId { get; set; } + + public int CompetencyId { get; set; } + + public int LevelId { get; set; } + + public int? EncounterId { get; set; } + + public int? CourseId { get; set; } + + public int? SessionId { get; set; } + + public int? VerifiedBy { get; set; } + + public DateTime? VerifiedTimestamp { get; set; } + + public bool? Deleted { get; set; } + + public int? BundleGroupId { get; set; } + + public int? BundleId { get; set; } + + public DateTime Added { get; set; } + + public DateTime? Updated { get; set; } + + public virtual Bundle? Bundle { get; set; } + public virtual BundleCompetencyGroup? BundleGroup { get; set; } + public virtual Person Person { get; set; } = null!; + public virtual Competency Competency { get; set; } = null!; + public virtual Level Level { get; set; } = null!; + public virtual Encounter? Encounter { get; set; } + public virtual Course? Course { get; set; } + public virtual Session? Session { get; set; } + public virtual Person? Verifier { get; set; } +} From ff57e3be9fdbba02724a13e8403b24120cd7bc3d Mon Sep 17 00:00:00 2001 From: Brandon Date: Mon, 16 Sep 2024 14:11:39 -0700 Subject: [PATCH 05/20] Student results view --- VueApp/package-lock.json | 103 +++++----- VueApp/package.json | 4 +- VueApp/src/CTS/assets/cts.css | 110 ++++++++++ .../src/CTS/components/AssessmentBubble.vue | 58 ++++++ VueApp/src/CTS/cts.ts | 4 + VueApp/src/CTS/pages/MyAssessmentCharts.vue | 149 ++++++++++++++ VueApp/src/CTS/pages/MyAssessments.vue | 189 +++++++++--------- VueApp/src/composables/QuasarConfig.ts | 9 +- VueApp/vueapp.esproj | 1 + .../CTS/Controllers/PermissionsController.cs | 36 ++++ .../Computing/Controllers/PersonController.cs | 11 + 11 files changed, 523 insertions(+), 151 deletions(-) create mode 100644 VueApp/src/CTS/assets/cts.css create mode 100644 VueApp/src/CTS/components/AssessmentBubble.vue create mode 100644 VueApp/src/CTS/pages/MyAssessmentCharts.vue create mode 100644 web/Areas/CTS/Controllers/PermissionsController.cs diff --git a/VueApp/package-lock.json b/VueApp/package-lock.json index f03c6d5..8e7f6a3 100644 --- a/VueApp/package-lock.json +++ b/VueApp/package-lock.json @@ -8,10 +8,10 @@ "name": "vueapp", "version": "0.0.0", "dependencies": { - "@quasar/extras": "^1.16.11", + "@quasar/extras": "^1.16.12", "chart.js": "^4.4.3", "pinia": "^2.1.7", - "quasar": "^2.16.4", + "quasar": "^2.16.11", "vue": "^3.4.21", "vue-chartjs": "^5.3.1", "vue-router": "^4.3.3", @@ -592,9 +592,9 @@ } }, "node_modules/@quasar/extras": { - "version": "1.16.11", - "resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.16.11.tgz", - "integrity": "sha512-sbTBHOA+Hi7ah0P6qSm+xfRXqwJ94ct3NKA3Lkq3iNPYuHD7VXbSWtP2eA7Cu9Fd0WjVoPbngf6yFGg46U3IfQ==", + "version": "1.16.12", + "resolved": "https://registry.npmjs.org/@quasar/extras/-/extras-1.16.12.tgz", + "integrity": "sha512-hLlb3Buxo38Xg/2w0BTkz98RBh/VH8apZ2r6Fl8YpPgrVQ0diHyN/BVTvIOk5Kch2y38L2kvwOIddsB2UcCuIg==", "funding": { "type": "github", "url": "https://donate.quasar.dev" @@ -1059,31 +1059,29 @@ } }, "node_modules/@volar/language-core": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.2.5.tgz", - "integrity": "sha512-2htyAuxRrAgETmFeUhT4XLELk3LiEcqoW/B8YUXMF6BrGWLMwIR09MFaZYvrA2UhbdAeSyeQ726HaWSWkexUcQ==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.5.tgz", + "integrity": "sha512-F4tA0DCO5Q1F5mScHmca0umsi2ufKULAnMOVBfMsZdT4myhVl4WdKRwCaKcfOkIEuyrAVvtq1ESBdZ+rSyLVww==", "dev": true, "dependencies": { - "@volar/source-map": "2.2.5" + "@volar/source-map": "2.4.5" } }, "node_modules/@volar/source-map": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.2.5.tgz", - "integrity": "sha512-wrOEIiZNf4E+PWB0AxyM4tfhkfldPsb3bxg8N6FHrxJH2ohar7aGu48e98bp3pR9HUA7P/pR9VrLmkTrgCCnWQ==", - "dev": true, - "dependencies": { - "muggle-string": "^0.4.0" - } + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.5.tgz", + "integrity": "sha512-varwD7RaKE2J/Z+Zu6j3mNNJbNT394qIxXwdvz/4ao/vxOfyClZpSDtLKkwWmecinkOVos5+PWkWraelfMLfpw==", + "dev": true }, "node_modules/@volar/typescript": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.2.5.tgz", - "integrity": "sha512-eSV/n75+ppfEVugMC/salZsI44nXDPAyL6+iTYCNLtiLHGJsnMv9GwiDMujrvAUj/aLQyqRJgYtXRoxop2clCw==", + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.5.tgz", + "integrity": "sha512-mcT1mHvLljAEtHviVcBuOyAwwMKz1ibXTi5uYtP/pf4XxoAzpdkQ+Br2IC0NPCvLCbjPZmbf3I0udndkfB1CDg==", "dev": true, "dependencies": { - "@volar/language-core": "2.2.5", - "path-browserify": "^1.0.1" + "@volar/language-core": "2.4.5", + "path-browserify": "^1.0.1", + "vscode-uri": "^3.0.8" } }, "node_modules/@vue/compiler-core": { @@ -1132,6 +1130,16 @@ "@vue/shared": "3.4.27" } }, + "node_modules/@vue/compiler-vue2": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/@vue/compiler-vue2/-/compiler-vue2-2.7.16.tgz", + "integrity": "sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==", + "dev": true, + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, "node_modules/@vue/devtools-api": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", @@ -1162,18 +1170,19 @@ } }, "node_modules/@vue/language-core": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.19.tgz", - "integrity": "sha512-A9EGOnvb51jOvnCYoRLnMP+CcoPlbZVxI9gZXE/y2GksRWM6j/PrLEIC++pnosWTN08tFpJgxhSS//E9v/Sg+Q==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.1.6.tgz", + "integrity": "sha512-MW569cSky9R/ooKMh6xa2g1D0AtRKbL56k83dzus/bx//RDJk24RHWkMzbAlXjMdDNyxAaagKPRquBIxkxlCkg==", "dev": true, "dependencies": { - "@volar/language-core": "~2.2.4", + "@volar/language-core": "~2.4.1", "@vue/compiler-dom": "^3.4.0", + "@vue/compiler-vue2": "^2.7.16", "@vue/shared": "^3.4.0", "computeds": "^0.0.1", "minimatch": "^9.0.3", - "path-browserify": "^1.0.1", - "vue-template-compiler": "^2.7.14" + "muggle-string": "^0.4.1", + "path-browserify": "^1.0.1" }, "peerDependencies": { "typescript": "*" @@ -2278,9 +2287,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "dependencies": { "braces": "^3.0.3", @@ -2663,9 +2672,9 @@ } }, "node_modules/quasar": { - "version": "2.16.4", - "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.16.4.tgz", - "integrity": "sha512-ICntco9uZ4PeyLgzVckjK3fsS+LG7+rOUmRyR7Gq3XpfeCADs1edIRjlxsPpWBBJvK/9AHLGPO6XNmnJmdJm0A==", + "version": "2.16.11", + "resolved": "https://registry.npmjs.org/quasar/-/quasar-2.16.11.tgz", + "integrity": "sha512-64C03WzZTRlJFvn0xNG3xmq/4R97lqbZAbzNE19ObNeYSUbJzQ8xi4ft5YQDRfTTIJ7RxB3pNye/dOSb11qoxQ==", "engines": { "node": ">= 10.18.1", "npm": ">= 6.13.4", @@ -3067,6 +3076,12 @@ } } }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true + }, "node_modules/vue": { "version": "3.4.27", "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.27.tgz", @@ -3134,31 +3149,21 @@ "vue": "^3.2.0" } }, - "node_modules/vue-template-compiler": { - "version": "2.7.16", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", - "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", - "dev": true, - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, "node_modules/vue-tsc": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.19.tgz", - "integrity": "sha512-JWay5Zt2/871iodGF72cELIbcAoPyhJxq56mPPh+M2K7IwI688FMrFKc/+DvB05wDWEuCPexQJ6L10zSwzzapg==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.6.tgz", + "integrity": "sha512-f98dyZp5FOukcYmbFpuSCJ4Z0vHSOSmxGttZJCsFeX0M4w/Rsq0s4uKXjcSRsZqsRgQa6z7SfuO+y0HVICE57Q==", "dev": true, "dependencies": { - "@volar/typescript": "~2.2.4", - "@vue/language-core": "2.0.19", + "@volar/typescript": "~2.4.1", + "@vue/language-core": "2.1.6", "semver": "^7.5.4" }, "bin": { "vue-tsc": "bin/vue-tsc.js" }, "peerDependencies": { - "typescript": "*" + "typescript": ">=5.0.0" } }, "node_modules/watch": { diff --git a/VueApp/package.json b/VueApp/package.json index 6be166f..9ced96b 100644 --- a/VueApp/package.json +++ b/VueApp/package.json @@ -17,10 +17,10 @@ "build-only-test": "vite build --mode test" }, "dependencies": { - "@quasar/extras": "^1.16.11", + "@quasar/extras": "^1.16.12", "chart.js": "^4.4.3", "pinia": "^2.1.7", - "quasar": "^2.16.4", + "quasar": "^2.16.11", "vue": "^3.4.21", "vue-chartjs": "^5.3.1", "vue-router": "^4.3.3", diff --git a/VueApp/src/CTS/assets/cts.css b/VueApp/src/CTS/assets/cts.css new file mode 100644 index 0000000..954a2f7 --- /dev/null +++ b/VueApp/src/CTS/assets/cts.css @@ -0,0 +1,110 @@ +.assessmentGroup { + border-top: 1px solid silver; +} +/* +.assessmentbubble { + width: 15px; + height: 15px; + margin-left: 5px; + margin-right: 5px; + border-radius: 50%; + display: inline-block; +} + +.assessmentbubble.ab5_1 { + background-color: rgba(174,235,255,1); +} + +.assessmentbubble.ab5_2 { + background-color: rgba(134,198,255,1); +} + +.assessmentbubble.ab5_3 { + background-color: rgba(62,127,238,1); +} + +.assessmentbubble.ab5_4 { + background-color: rgba(0,44,219,1); +} + +.assessmentbubble.ab5_5 { + background-color: rgba(11,3,139,1); +} + +.assessmentIcon.ab5_1 { + color: rgba(174,235,255,1); +} + +.assessmentIcon.ab5_2 { + color: rgba(134,198,255,1); +} + +.assessmentIcon.ab5_3 { + color: rgba(62,127,238,1); +} + +.assessmentIcon.ab5_4 { + color: rgba(0,44,219,1); +} + +.assessmentIcon.ab5_5 { + color: rgba(11,3,139,1); +} +*/ +.assessmentBubble5_1 { + color: rgba(62,127,238,.3); +} + +.assessmentBubble5_2 { + color: rgba(62,127,238,.7); +} + +.assessmentBubble5_3 { + color: rgba(62,127,238,1); +} + +.assessmentBubble5_4 { + color: rgba(0,44,175,.8); +} + +.assessmentBubble5_5 { + color: rgba(11,3,139,1); +} + +.assessmentBubbleCloser5_1 { + color: rgba(2,40,150,.7); +} + +.assessmentBubbleCloser5_2 { + color: rgba(2,40,150,.8); +} + +.assessmentBubbleCloser5_3 { + color: rgba(2,40,150,.9); +} + +.assessmentBubbleCloser5_4 { + color: rgba(2,40,150,.95); +} + +.assessmentBubbleCloser5_5 { + color: rgba(2,40,150,1); +} + +/* +.assessmentbubble.ab5_1 { + background-color: rgb(169, 208, 255) +} +.assessmentbubble.ab5_2 { + background-color: rgb(121, 158, 223) +} +.assessmentbubble.ab5_3 { + background-color: rgb(78, 108, 189) +} +.assessmentbubble.ab5_4 { + background-color: rgb(42, 60, 152) +} +.assessmentbubble.ab5_5 { + background-color: rgb(0, 11, 113) +} +*/ diff --git a/VueApp/src/CTS/components/AssessmentBubble.vue b/VueApp/src/CTS/components/AssessmentBubble.vue new file mode 100644 index 0000000..5363f29 --- /dev/null +++ b/VueApp/src/CTS/components/AssessmentBubble.vue @@ -0,0 +1,58 @@ + + \ No newline at end of file diff --git a/VueApp/src/CTS/cts.ts b/VueApp/src/CTS/cts.ts index c3d496a..ccfa3a1 100644 --- a/VueApp/src/CTS/cts.ts +++ b/VueApp/src/CTS/cts.ts @@ -7,6 +7,8 @@ import App from './App.vue' import { Quasar, Loading, QSpinnerOval } from 'quasar' // Import icon libraries import '@quasar/extras/material-icons/material-icons.css' +import '@quasar/extras/material-symbols-outlined/material-symbols-outlined.css' +import IconSet from 'quasar/icon-set/material-symbols-outlined.js' // Import Quasar css import 'quasar/dist/quasar.css' @@ -14,10 +16,12 @@ import { useQuasarConfig } from '@/composables/QuasarConfig' //import our css import '@/assets/site.css' +import '@/cts/assets/cts.css' const { quasarConfig } = useQuasarConfig() const pinia = createPinia() const app = createApp(App) +Quasar.iconSet.set(IconSet) app.provide("apiURL", import.meta.env.VITE_API_URL) app.use(pinia) diff --git a/VueApp/src/CTS/pages/MyAssessmentCharts.vue b/VueApp/src/CTS/pages/MyAssessmentCharts.vue new file mode 100644 index 0000000..3f38593 --- /dev/null +++ b/VueApp/src/CTS/pages/MyAssessmentCharts.vue @@ -0,0 +1,149 @@ + + \ No newline at end of file diff --git a/VueApp/src/CTS/pages/MyAssessments.vue b/VueApp/src/CTS/pages/MyAssessments.vue index 3f38593..517bb6a 100644 --- a/VueApp/src/CTS/pages/MyAssessments.vue +++ b/VueApp/src/CTS/pages/MyAssessments.vue @@ -1,58 +1,62 @@ \ No newline at end of file diff --git a/VueApp/src/composables/QuasarConfig.ts b/VueApp/src/composables/QuasarConfig.ts index 92a42cc..e42ab77 100644 --- a/VueApp/src/composables/QuasarConfig.ts +++ b/VueApp/src/composables/QuasarConfig.ts @@ -27,8 +27,13 @@ export function useQuasarConfig() { backgroundColor: "dark", messageColor: "light", boxClass: "bg-grey-2 text-grey-9" - } - } + }, + animations: 'all', + extras: ['material-icons', 'material-symbols-outlined'], + framework: { + iconSet: 'material-symbols-outlined' + }, + }, } return { quasarConfig } diff --git a/VueApp/vueapp.esproj b/VueApp/vueapp.esproj index d6487a6..b683789 100644 --- a/VueApp/vueapp.esproj +++ b/VueApp/vueapp.esproj @@ -11,6 +11,7 @@ + diff --git a/web/Areas/CTS/Controllers/PermissionsController.cs b/web/Areas/CTS/Controllers/PermissionsController.cs new file mode 100644 index 0000000..7f132bc --- /dev/null +++ b/web/Areas/CTS/Controllers/PermissionsController.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Viper.Areas.CTS.Services; +using Viper.Classes; +using Viper.Classes.SQLContext; +using Web.Authorization; + +namespace Viper.Areas.CTS.Controllers +{ + [Route("/api/cts/permissions/")] + [Permission(Allow = "SVMSecure")] + public class PermissionsController : ApiController + { + private readonly CtsSecurityService ctsSecurityService; + private readonly RAPSContext _rapsContext; + private readonly VIPERContext _viperContext; + + public PermissionsController(VIPERContext context, RAPSContext rapsContext) + { + _viperContext = context; + _rapsContext = rapsContext; + ctsSecurityService = new CtsSecurityService(rapsContext, _viperContext); + } + + [HttpGet] + public ActionResult HasAccess(string access, int studentId) + { + switch(access) + { + case "ViewStudentAssessments": + return ctsSecurityService.CheckStudentAssessmentViewAccess(studentId); + } + return false; + } + } +} diff --git a/web/Areas/Computing/Controllers/PersonController.cs b/web/Areas/Computing/Controllers/PersonController.cs index c5ed57b..575e2a5 100644 --- a/web/Areas/Computing/Controllers/PersonController.cs +++ b/web/Areas/Computing/Controllers/PersonController.cs @@ -43,6 +43,17 @@ public async Task>> GetPeople(bool? active = nul .ToListAsync(); } + [HttpGet("{personId}")] + public async Task> GetPerson(int personId) + { + var p = await context.People.FindAsync(personId); + if(p == null) + { + return NotFound(); + } + return new PersonSimple(p); + } + [HttpPost("biorenderStudents")] [Permission(Allow = "SVMSecure.CATS.BiorenderStudentLookup")] public async Task>> GetBiorenderStudentList(List emails) From 918016eee3bff0037ccc8decfb4a421b1104f0ae Mon Sep 17 00:00:00 2001 From: Brandon Date: Tue, 17 Sep 2024 15:19:08 -0700 Subject: [PATCH 06/20] Competencies/Roles/Bundles work in progress --- VueApp/src/CTS/pages/ManageBundles.vue | 120 +++++++++++++ VueApp/src/CTS/pages/ManageCompetencies.vue | 127 ++++++++++++++ VueApp/src/CTS/pages/ManageLevels.vue | 21 ++- VueApp/src/CTS/pages/ManageRoles.vue | 55 ++++++ VueApp/src/CTS/router/routes.ts | 15 ++ VueApp/src/CTS/types/index.ts | 39 +++++ VueApp/src/components/GenericError.vue | 2 +- VueApp/src/composables/ViperFetch.ts | 21 ++- web/Areas/CTS/Controllers/BundleController.cs | 75 +++++++++ .../CTS/Controllers/CompetencyController.cs | 159 ++++++++++++++++++ web/Areas/CTS/Controllers/DomainController.cs | 25 +-- web/Areas/CTS/Controllers/LevelsController.cs | 39 ++++- web/Areas/CTS/Controllers/RoleController.cs | 99 +++++++++++ web/Areas/CTS/Models/AutoMapperProfileCts.cs | 19 +++ web/Areas/CTS/Models/BundleCompetencyDto.cs | 18 ++ .../CTS/Models/BundleCompetencyGroupDto.cs | 10 ++ web/Areas/CTS/Models/BundleDto.cs | 17 ++ web/Areas/CTS/Models/BundleRoleDto.cs | 9 + web/Areas/CTS/Models/CompetencyAddUpdate.cs | 13 ++ web/Areas/CTS/Models/CompetencyDto.cs | 37 ++++ web/Areas/CTS/Models/DomainDto.cs | 21 +++ web/Areas/CTS/Models/LevelDto.cs | 16 ++ web/Areas/CTS/Models/MilestoneLevelDto.cs | 10 ++ web/Areas/CTS/Models/RoleDto.cs | 8 + web/Areas/CTS/Models/ServiceDto.cs | 8 + web/Areas/CTS/Services/CtsNavMenu.cs | 2 + web/Classes/SQLContext/CtsContext.cs | 5 +- web/Models/CTS/Competency.cs | 2 + web/Models/CTS/Role.cs | 2 + web/Program.cs | 3 + web/Viper.csproj | 1 + 31 files changed, 975 insertions(+), 23 deletions(-) create mode 100644 VueApp/src/CTS/pages/ManageBundles.vue create mode 100644 VueApp/src/CTS/pages/ManageCompetencies.vue create mode 100644 VueApp/src/CTS/pages/ManageRoles.vue create mode 100644 web/Areas/CTS/Controllers/BundleController.cs create mode 100644 web/Areas/CTS/Controllers/CompetencyController.cs create mode 100644 web/Areas/CTS/Controllers/RoleController.cs create mode 100644 web/Areas/CTS/Models/AutoMapperProfileCts.cs create mode 100644 web/Areas/CTS/Models/BundleCompetencyDto.cs create mode 100644 web/Areas/CTS/Models/BundleCompetencyGroupDto.cs create mode 100644 web/Areas/CTS/Models/BundleDto.cs create mode 100644 web/Areas/CTS/Models/BundleRoleDto.cs create mode 100644 web/Areas/CTS/Models/CompetencyAddUpdate.cs create mode 100644 web/Areas/CTS/Models/CompetencyDto.cs create mode 100644 web/Areas/CTS/Models/DomainDto.cs create mode 100644 web/Areas/CTS/Models/LevelDto.cs create mode 100644 web/Areas/CTS/Models/MilestoneLevelDto.cs create mode 100644 web/Areas/CTS/Models/RoleDto.cs create mode 100644 web/Areas/CTS/Models/ServiceDto.cs diff --git a/VueApp/src/CTS/pages/ManageBundles.vue b/VueApp/src/CTS/pages/ManageBundles.vue new file mode 100644 index 0000000..e6d41c8 --- /dev/null +++ b/VueApp/src/CTS/pages/ManageBundles.vue @@ -0,0 +1,120 @@ + + \ No newline at end of file diff --git a/VueApp/src/CTS/pages/ManageCompetencies.vue b/VueApp/src/CTS/pages/ManageCompetencies.vue new file mode 100644 index 0000000..ef1dbf0 --- /dev/null +++ b/VueApp/src/CTS/pages/ManageCompetencies.vue @@ -0,0 +1,127 @@ + + \ No newline at end of file diff --git a/VueApp/src/CTS/pages/ManageLevels.vue b/VueApp/src/CTS/pages/ManageLevels.vue index fcbf76c..3ed9246 100644 --- a/VueApp/src/CTS/pages/ManageLevels.vue +++ b/VueApp/src/CTS/pages/ManageLevels.vue @@ -47,6 +47,10 @@ } else { level.value.epa = type.value == "EPA" + level.value.clinical = type.value == "Clinical" + level.value.course = type.value == "Course" + level.value.dops = type.value == "DOPS" + level.value.milestone = type.value == "Milestone" const r = await post(levelUrl, level.value) success = r.success } @@ -64,6 +68,17 @@ } } + function filterLevels() { + switch (type.value) { + case "EPA": return levels.value.filter(l => l.epa) + case "Milestone": return levels.value.filter(l => l.milestone) + case "DOPS": return levels.value.filter(l => l.dops) + case "Clinical": return levels.value.filter(l => l.clinical) + case "Course": return levels.value.filter(l => l.course) + default: return [] + } + } + loadLevels() @@ -98,11 +113,11 @@
- + - EPA Levels + {{type}} Levels - + diff --git a/VueApp/src/CTS/pages/ManageRoles.vue b/VueApp/src/CTS/pages/ManageRoles.vue new file mode 100644 index 0000000..325924f --- /dev/null +++ b/VueApp/src/CTS/pages/ManageRoles.vue @@ -0,0 +1,55 @@ + + \ No newline at end of file diff --git a/VueApp/src/CTS/router/routes.ts b/VueApp/src/CTS/router/routes.ts index 929f987..4adbccc 100644 --- a/VueApp/src/CTS/router/routes.ts +++ b/VueApp/src/CTS/router/routes.ts @@ -42,6 +42,11 @@ const routes = [ component: () => import('@/CTS/pages/AssessmentEpaEdit.vue'), }, /* Application Management */ + { + path: '/CTS/ManageCompetencies', + meta: { layout: ViperLayout }, + component: () => import('@/CTS/pages/ManageCompetencies.vue'), + }, { path: '/CTS/ManageDomains', meta: { layout: ViperLayout }, @@ -57,6 +62,16 @@ const routes = [ meta: { layout: ViperLayout }, component: () => import('@/CTS/pages/ManageLevels.vue'), }, + { + path: '/CTS/ManageBundles', + meta: { layout: ViperLayout }, + component: () => import('@/CTS/pages/ManageBundles.vue'), + }, + { + path: '/CTS/ManageRoles', + meta: { layout: ViperLayout }, + component: () => import('@/CTS/pages/ManageRoles.vue'), + }, { path: '/CTS/Audit', name: 'Audit Log', diff --git a/VueApp/src/CTS/types/index.ts b/VueApp/src/CTS/types/index.ts index 3fcf7c4..10b83ec 100644 --- a/VueApp/src/CTS/types/index.ts +++ b/VueApp/src/CTS/types/index.ts @@ -1,4 +1,24 @@ import { computed } from 'vue' + +export type Domain = { + domainId: number, + name: string, + order: number, + description: string | null +} + +export type Competency = { + competencyId: number | null, + domainId: number | null, + parentId: number | null, + name: string, + number: string, + description: string | null, + canLinkToStudent: boolean, + domain: Domain | null, + children: Competency[] | null, +} + export type Epa = { epaId: number | null order: number | null @@ -86,4 +106,23 @@ export type Person = { lastName: string, fullName: string, fullNameLastFirst: string, +} + +export type Role = { + roleId: number, + name: string, +} + +export type Bundle = { + bundleId: number | null, + name: string, + clinical: boolean, + assessment: boolean, + milestone: boolean, +} + +export type BundleRole = { + bundleRoleId: number, + bundleId: number, + roleId: number, } \ No newline at end of file diff --git a/VueApp/src/components/GenericError.vue b/VueApp/src/components/GenericError.vue index f482efb..58722a5 100644 --- a/VueApp/src/components/GenericError.vue +++ b/VueApp/src/components/GenericError.vue @@ -2,7 +2,7 @@ - + diff --git a/VueApp/src/composables/ViperFetch.ts b/VueApp/src/composables/ViperFetch.ts index b528aae..6704adf 100644 --- a/VueApp/src/composables/ViperFetch.ts +++ b/VueApp/src/composables/ViperFetch.ts @@ -142,12 +142,21 @@ export function useFetch() { isAuthError = true } else { - result = await response.json() - message = result.errorMessage != null - ? result.errorMessage - : result.detail != null - ? result.detail - : result.statusText + try { + result = await response.json() + } + catch (e) { + result = response + } + if (result.errorMessage != null) { + message = result.errorMessage + } + else if (result.detail != null) { + message = result.detail + } + else if (result.statusText != null) { + message = result.statusText + } } } catch (e) { diff --git a/web/Areas/CTS/Controllers/BundleController.cs b/web/Areas/CTS/Controllers/BundleController.cs new file mode 100644 index 0000000..421c7e6 --- /dev/null +++ b/web/Areas/CTS/Controllers/BundleController.cs @@ -0,0 +1,75 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Viper.Areas.CTS.Models; +using Viper.Classes; +using Viper.Classes.SQLContext; +using Viper.Models.CTS; +using Web.Authorization; + +namespace Viper.Areas.CTS.Controllers +{ + [Route("/api/cts/bundles/")] + [Permission(Allow = "SVMSecure")] + public class BundleController : ApiController + { + private readonly IMapper mapper; + private readonly VIPERContext context; + + public BundleController(IMapper mapper, VIPERContext context) + { + this.mapper = mapper; + this.context = context; + } + + [HttpGet] + public async Task>> GetBundles(bool? clinical = null, bool? assessment = null, bool? milestone = null, + int? serviceId = null, int? roleId = null) + { + var bundleQuery = context.Bundles + .AsQueryable(); + if (clinical != null) + { + bundleQuery = bundleQuery.Where(b => b.Clinical == clinical); + } + if (assessment != null) + { + bundleQuery = bundleQuery.Where(b => b.Assessment == assessment); + } + if (milestone != null) + { + bundleQuery = bundleQuery.Where(b => b.Milestone == milestone); + } + if (serviceId != null) + { + bundleQuery = bundleQuery.Where(b => b.BundleServices.Any(s => s.ServiceId == serviceId)); + } + if (roleId != null) + { + bundleQuery = bundleQuery.Where(b => b.BundleRoles.Any(br => br.RoleId == roleId)); + } + + var bundles = await bundleQuery + .OrderBy(b => b.Name) + .ToListAsync(); + + return mapper.Map>(bundles); + } + + [HttpPost] + public async Task> AddBundle(BundleDto bundleDto) + { + var nameCheck = await context.Bundles.Where(b => b.Name == bundleDto.Name).AnyAsync(); + if (nameCheck) + { + return BadRequest("Bundle Name must be unique."); + } + + Bundle b = mapper.Map(bundleDto); + context.Add(b); + await context.SaveChangesAsync(); + + return mapper.Map(b); + } + } +} diff --git a/web/Areas/CTS/Controllers/CompetencyController.cs b/web/Areas/CTS/Controllers/CompetencyController.cs new file mode 100644 index 0000000..d0d1f60 --- /dev/null +++ b/web/Areas/CTS/Controllers/CompetencyController.cs @@ -0,0 +1,159 @@ +using Viper.Classes; +using Web.Authorization; +using Microsoft.AspNetCore.Mvc; +using Viper.Classes.SQLContext; +using Microsoft.EntityFrameworkCore; +using Viper.Models.CTS; +using Viper.Areas.CTS.Models; +using System.DirectoryServices.ActiveDirectory; + +namespace Viper.Areas.CTS.Controllers +{ + [Route("/api/cts/competencies/")] + [Permission(Allow = "SVMSecure")] + public class CompetencyController : ApiController + { + private readonly VIPERContext context; + + public CompetencyController(VIPERContext context) + { + this.context = context; + } + + [HttpGet] + public async Task>> Index() + { + return await context.Competencies + .Include(c => c.Domain) + .OrderBy(c => c.Number) + .Select(c => new CompetencyDto(c)) + .ToListAsync(); + } + + [HttpGet("{competencyId}")] + public async Task> GetComp(int competencyId) + { + var comp = await context.Competencies.Include(c => c.Domain).Where(c => c.CompetencyId == competencyId).FirstOrDefaultAsync(); + if (comp == null) + { + return NotFound(); + } + return new CompetencyDto(comp); + } + + [HttpGet("{competencyId}/children")] + public async Task>> GetChildren(int competencyId) + { + var comps = await context.Competencies + .Include(c => c.Domain) + .Where(c => c.ParentId == competencyId) + .Select(c => new CompetencyDto(c)) + .ToListAsync(); + if (comps.Count == 0) + { + return NotFound(); + } + return comps; + } + + [HttpPost] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> AddCompetency(CompetencyAddUpdate competency) + { + if (competency.CompetencyId != null) + { + return BadRequest("CompetencyId cannot be set for a new competency."); + } + + if (competency.Name.Length < 3 || competency.Number.Length < 1) + { + return BadRequest("Name and Number are required."); + } + + var duplicates = await context.Competencies.Where(c => c.Name == competency.Name || c.Number == competency.Number).ToListAsync(); + if (duplicates.Count > 0) + { + return BadRequest("A competency with this name or number exists already."); + } + + var comp = new Competency() + { + Name = competency.Name, + Number = competency.Number, + CanLinkToStudent = competency.CanLinkToStudent, + Description = competency.Description, + DomainId = competency.DomainId, + ParentId = competency.ParentId + }; + context.Competencies.Add(comp); + await context.SaveChangesAsync(); + + return await GetComp(comp.CompetencyId); + } + + [HttpPut("{competencyId}")] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> UpdateComptency(int competencyId, CompetencyAddUpdate competency) + { + if(competencyId != competency.CompetencyId) + { + return BadRequest(); + } + + var c = await context.Competencies.FindAsync(competency.CompetencyId); + if (c == null) + { + return NotFound(); + } + + if (competency.CompetencyId == null || competency.CompetencyId <= 0) + { + return BadRequest("CompetencyId is required."); + } + if (competency.Name.Length < 3 || competency.Number.Length < 1) + { + return BadRequest("Name and Number are required."); + } + + var duplicates = await context.Competencies + .Where(c => c.CompetencyId != competency.CompetencyId) + .Where(c => c.Name == competency.Name || c.Number == competency.Number) + .ToListAsync(); + if (duplicates.Count > 0) + { + return BadRequest("A competency with this name or number exists already."); + } + + c.Name = competency.Name; + c.Number = competency.Number; + c.Description = competency.Description; + c.CanLinkToStudent = competency.CanLinkToStudent; + c.ParentId = competency.ParentId; + + context.Competencies.Update(c); + await context.SaveChangesAsync(); + return await GetComp(c.CompetencyId); + } + + [HttpDelete("{competencyId}")] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> DeleteCompetency(int competencyId) + { + var c = await context.Competencies.FindAsync(competencyId); + if (c == null) + { + return NotFound(); + } + context.Competencies.Remove(c); + try + { + await context.SaveChangesAsync(); + } + catch (Exception) + { + return BadRequest("Could not remove domain. It may be linked to other objects."); + } + return new CompetencyDto(c); + } + } +} diff --git a/web/Areas/CTS/Controllers/DomainController.cs b/web/Areas/CTS/Controllers/DomainController.cs index 3a73493..290fe39 100644 --- a/web/Areas/CTS/Controllers/DomainController.cs +++ b/web/Areas/CTS/Controllers/DomainController.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Linq.Dynamic.Core; +using Viper.Areas.CTS.Models; using Viper.Classes; using Viper.Classes.SQLContext; using Viper.Models.CTS; @@ -21,20 +22,22 @@ public DomainController(VIPERContext context) } [HttpGet] - public async Task>> Index() + public async Task>> Index() { - return await context.Domains.OrderBy(d => d.Order).ToListAsync(); + return await context.Domains.OrderBy(d => d.Order).Select(d => new DomainDto(d)).ToListAsync(); } [HttpGet("{domainId}")] - public async Task> GetDomain(int domainId) + public async Task> GetDomain(int domainId) { - return await context.Domains.FindAsync(domainId); + var d = await context.Domains.FindAsync(domainId); + + return d == null ? NotFound() : new DomainDto(d); } [HttpPost] [Permission(Allow = "SVMSecure.CTS.Manage")] - public async Task> CreateDomain(Domain domain) + public async Task> CreateDomain(Domain domain) { if(domain.DomainId != 0) { @@ -51,12 +54,12 @@ public async Task> CreateDomain(Domain domain) context.Add(domain); await context.SaveChangesAsync(); - return domain; + return new DomainDto(domain); } [HttpPut("{domainId}")] [Permission(Allow = "SVMSecure.CTS.Manage")] - public async Task> UpdateDomain(int domainId, Domain domain) + public async Task> UpdateDomain(int domainId, Domain domain) { if(domain.DomainId != domainId) { @@ -72,12 +75,12 @@ public async Task> UpdateDomain(int domainId, Domain domain context.Domains.Update(domain); await context.SaveChangesAsync(); - return domain; - } + return new DomainDto(domain); + } [HttpDelete("{domainId}")] [Permission(Allow = "SVMSecure.CTS.Manage")] - public async Task> DeleteDomain(int domainId) + public async Task> DeleteDomain(int domainId) { var domain = await context.Domains.FindAsync(domainId); if(domain == null) @@ -94,7 +97,7 @@ public async Task> DeleteDomain(int domainId) { return BadRequest("Could not remove domain. It may be linked to other objects."); } - return domain; + return new DomainDto(domain); } } } diff --git a/web/Areas/CTS/Controllers/LevelsController.cs b/web/Areas/CTS/Controllers/LevelsController.cs index c73aec1..2e605aa 100644 --- a/web/Areas/CTS/Controllers/LevelsController.cs +++ b/web/Areas/CTS/Controllers/LevelsController.cs @@ -22,13 +22,30 @@ public LevelsController(VIPERContext context) } [HttpGet] - public async Task>> GetLevels(bool? epa = null, bool active = true) + public async Task>> GetLevels(bool? epa = null, bool? dops = null, bool? milestone = null, + bool? course = null, bool? clinical = null, bool active = true) { var q = context.Levels.AsQueryable(); if (epa != null) { q = q.Where(l => l.Epa == epa); } + if (dops != null) + { + q = q.Where(l => l.Dops == dops); + } + if (milestone != null) + { + q = q.Where(l => l.Milestone == milestone); + } + if (course != null) + { + q = q.Where(l => l.Course == course); + } + if (clinical != null) + { + q = q.Where(l => l.Clinical == clinical); + } q = q.Where(l => (active && l.Active) || (!active && !l.Active)); return await q.OrderBy(l => l.Epa ? 1 : 0) .ThenBy(l => l.Dops ? 1 : 0) @@ -61,6 +78,10 @@ public async Task> Createlevel(LevelCreateUpdate level) { return BadRequest("Another level for the same type of assessment has the same text."); } + if (!level.Dops && !level.Clinical && !level.Course && !level.Milestone && !level.Epa) + { + return BadRequest("Level must belong to a type."); + } var l = new Level() { @@ -153,6 +174,22 @@ private void AdjustLevelOrders(Level level) { levels = levels.Where(l => l.Epa); } + if (level.Milestone) + { + levels = levels.Where(l => !l.Milestone); + } + if (level.Course) + { + levels = levels.Where(l => !l.Milestone); + } + if (level.Clinical) + { + levels = levels.Where(l => !l.Milestone); + } + if (level.Dops) + { + levels = levels.Where(l => !l.Milestone); + } levels = levels.OrderBy(l => l.Order); //check orders are correct diff --git a/web/Areas/CTS/Controllers/RoleController.cs b/web/Areas/CTS/Controllers/RoleController.cs new file mode 100644 index 0000000..f258da7 --- /dev/null +++ b/web/Areas/CTS/Controllers/RoleController.cs @@ -0,0 +1,99 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Diagnostics; +using Viper.Areas.CTS.Models; +using Viper.Classes; +using Viper.Classes.SQLContext; +using Viper.Models.CTS; +using Web.Authorization; + +namespace Viper.Areas.CTS.Controllers +{ + [Route("/api/cts/roles")] + [Permission(Allow = "SVMSecure.CTS")] + public class RoleController : ApiController + { + private readonly VIPERContext context; + private readonly IMapper mapper; + + public RoleController(VIPERContext context, IMapper mapper) + { + this.context = context; + this.mapper = mapper; + } + + public async Task>> GetRoles(int? bundleId = null) + { + var rolesQuery = context.Roles.AsQueryable(); + if (bundleId != null) + { + rolesQuery = rolesQuery.Where(r => r.BundleRoles.Any(br => br.BundleId == bundleId)); + } + var roles = await rolesQuery.OrderBy(r => r.RoleId).ToListAsync(); + return mapper.Map>(roles); + } + + [HttpPost] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> AddRole(RoleDto roleDto) + { + var checkName = await context.Roles.AnyAsync(r => r.Name == roleDto.Name); + if (checkName) + { + return BadRequest("Role name must be unique."); + } + + var role = mapper.Map(roleDto); + context.Add(role); + await context.SaveChangesAsync(); + + return mapper.Map(role); + } + + [HttpPut("{roleId}")] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> UpdateRole(int roleId, RoleDto roleDto) + { + var role = await context.Roles.FindAsync(roleId); + if(role == null) + { + return NotFound(); + } + + var checkName = await context.Roles.AnyAsync(r => r.Name == roleDto.Name && r.RoleId != roleId); + if (checkName) + { + return BadRequest("Role name must be unique."); + } + + role.Name = roleDto.Name; + context.Update(role); + await context.SaveChangesAsync(); + return mapper.Map(role); + } + + [HttpDelete("{roleId}")] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> DeleteRole(int roleId) + { + var role = await context.Roles.FindAsync(roleId); + if (role == null) + { + return NotFound(); + } + + try + { + context.Entry(role).State = EntityState.Deleted; + await context.SaveChangesAsync(); + } + catch (Exception) + { + return BadRequest("Could not delete role. If this role has been added to a bundle, it cannot be deleted."); + } + + return mapper.Map(role); + } + } +} diff --git a/web/Areas/CTS/Models/AutoMapperProfileCts.cs b/web/Areas/CTS/Models/AutoMapperProfileCts.cs new file mode 100644 index 0000000..6f250d9 --- /dev/null +++ b/web/Areas/CTS/Models/AutoMapperProfileCts.cs @@ -0,0 +1,19 @@ +using AutoMapper; +using Viper.Models.CTS; + +namespace Viper.Areas.CTS.Models +{ + public class AutoMapperProfileCts : Profile + { + public AutoMapperProfileCts() { + CreateMap(); + CreateMap(); + CreateMap() + .ForMember(dest => dest.CompetencyCount, opt => opt.MapFrom(src => src.BundleCompetencies.Count)); + CreateMap(); + CreateMap().ReverseMap(); + CreateMap(); + CreateMap(); + } + } +} diff --git a/web/Areas/CTS/Models/BundleCompetencyDto.cs b/web/Areas/CTS/Models/BundleCompetencyDto.cs new file mode 100644 index 0000000..4d5f7d8 --- /dev/null +++ b/web/Areas/CTS/Models/BundleCompetencyDto.cs @@ -0,0 +1,18 @@ +namespace Viper.Areas.CTS.Models +{ + public class BundleCompetencyDto + { + public int BundleCompetencyId { get; set; } + public int BundleId { get; set; } + + public int RoleId { get; set; } + public string RoleName { get; set; } = null!; + + public int CompetencyId { get; set; } + public string CompetencyName { get; set; } = null!; + public string? Description { get; set; } + public bool CanLinkToStudent { get; set; } + + public int Order { get; set; } + } +} diff --git a/web/Areas/CTS/Models/BundleCompetencyGroupDto.cs b/web/Areas/CTS/Models/BundleCompetencyGroupDto.cs new file mode 100644 index 0000000..139d831 --- /dev/null +++ b/web/Areas/CTS/Models/BundleCompetencyGroupDto.cs @@ -0,0 +1,10 @@ +namespace Viper.Areas.CTS.Models +{ + public class BundleCompetencyGroupDto + { + public int BundleCompetencyGroupId { get; set; } + public string Name { get; set; } = null!; + public int Order { get; set; } + public IEnumerable Competencies { get; set; } = new List(); + } +} diff --git a/web/Areas/CTS/Models/BundleDto.cs b/web/Areas/CTS/Models/BundleDto.cs new file mode 100644 index 0000000..9ccd1c1 --- /dev/null +++ b/web/Areas/CTS/Models/BundleDto.cs @@ -0,0 +1,17 @@ +using Viper.Models.CTS; + +namespace Viper.Areas.CTS.Models +{ + public class BundleDto + { + public int BundleId { get; set; } + public string Name { get; set; } = null!; + public bool Clinical { get; set; } + public bool Assessment { get; set; } + public bool Milestone { get; set; } + public int CompetencyCount { get; set; } + + //Only including levels here - other related objects can be found separately + public IEnumerable Levels { get; set; } = new List(); + } +} diff --git a/web/Areas/CTS/Models/BundleRoleDto.cs b/web/Areas/CTS/Models/BundleRoleDto.cs new file mode 100644 index 0000000..0838876 --- /dev/null +++ b/web/Areas/CTS/Models/BundleRoleDto.cs @@ -0,0 +1,9 @@ +namespace Viper.Areas.CTS.Models +{ + public class BundleRoleDto + { + public int BundleRoleId { get; set; } + public int BundleId { get; set; } + public int RoleId { get; set; } + } +} diff --git a/web/Areas/CTS/Models/CompetencyAddUpdate.cs b/web/Areas/CTS/Models/CompetencyAddUpdate.cs new file mode 100644 index 0000000..858161d --- /dev/null +++ b/web/Areas/CTS/Models/CompetencyAddUpdate.cs @@ -0,0 +1,13 @@ +namespace Viper.Areas.CTS.Models +{ + public class CompetencyAddUpdate + { + public int? CompetencyId { get; set; } + public int DomainId { get; set; } + public int? ParentId { get; set; } + public string Number { get; set; } = null!; + public string Name { get; set; } = null!; + public string? Description { get; set; } + public bool CanLinkToStudent { get; set; } + } +} diff --git a/web/Areas/CTS/Models/CompetencyDto.cs b/web/Areas/CTS/Models/CompetencyDto.cs new file mode 100644 index 0000000..9f53a47 --- /dev/null +++ b/web/Areas/CTS/Models/CompetencyDto.cs @@ -0,0 +1,37 @@ +using Viper.Models.CTS; + +namespace Viper.Areas.CTS.Models +{ + public class CompetencyDto + { + public int CompetencyId { get; set; } + public int DomainId { get; set; } + public int? ParentId { get; set; } + public string Number { get; set; } = null!; + public string Name { get; set; } = null!; + public string? Description { get; set; } + public bool CanLinkToStudent { get; set; } + public string? DomainName { get; set; } + public int? DomainOrder { get; set; } + public CompetencyDto? Parent { get; set; } + + public CompetencyDto() + { + + } + + public CompetencyDto(Competency c) + { + CompetencyId = c.CompetencyId; + DomainId = c.DomainId; + ParentId = c.ParentId; + Number = c.Number; + Name = c.Name; + Description = c.Description; + CanLinkToStudent = c.CanLinkToStudent; + DomainName = c?.Domain?.Name; + DomainOrder = c?.Domain?.Order; + Parent = c.Parent != null ? new CompetencyDto(c.Parent) : null; + } + } +} diff --git a/web/Areas/CTS/Models/DomainDto.cs b/web/Areas/CTS/Models/DomainDto.cs new file mode 100644 index 0000000..418017a --- /dev/null +++ b/web/Areas/CTS/Models/DomainDto.cs @@ -0,0 +1,21 @@ +using Viper.Models.CTS; + +namespace Viper.Areas.CTS.Models +{ + public class DomainDto + { + public int DomainId { get; set; } + public string Name { get; set; } = null!; + public int Order { get; set; } + public string? Description { get; set; } + + public DomainDto() { } + public DomainDto(Domain domain) + { + DomainId = domain.DomainId; + Name = domain.Name; + Order = domain.Order; + Description = domain.Description; + } + } +} diff --git a/web/Areas/CTS/Models/LevelDto.cs b/web/Areas/CTS/Models/LevelDto.cs new file mode 100644 index 0000000..8ee196d --- /dev/null +++ b/web/Areas/CTS/Models/LevelDto.cs @@ -0,0 +1,16 @@ +namespace Viper.Areas.CTS.Models +{ + public class LevelDto + { + public int LevelId { get; set; } + public string Level { get; set; } = null!; + public string? Description { get; set; } = null!; + public bool Active { get; set; } + public int Order { get; set; } + public bool Course { get; set; } + public bool Clinical { get; set; } + public bool Assessment { get; set; } + public bool Milestone { get; set; } + public bool Epa { get; set; } + } +} diff --git a/web/Areas/CTS/Models/MilestoneLevelDto.cs b/web/Areas/CTS/Models/MilestoneLevelDto.cs new file mode 100644 index 0000000..983029b --- /dev/null +++ b/web/Areas/CTS/Models/MilestoneLevelDto.cs @@ -0,0 +1,10 @@ +namespace Viper.Areas.CTS.Models +{ + public class MilestoneLevelDto + { + public int MilestoneLevelId { get; set; } + public int LevelId { get; set; } + public string Level { get; set; } = null!; + public string Description { get; set; } = null!; + } +} \ No newline at end of file diff --git a/web/Areas/CTS/Models/RoleDto.cs b/web/Areas/CTS/Models/RoleDto.cs new file mode 100644 index 0000000..506f120 --- /dev/null +++ b/web/Areas/CTS/Models/RoleDto.cs @@ -0,0 +1,8 @@ +namespace Viper.Areas.CTS.Models +{ + public class RoleDto + { + public int RoleId { get; set; } + public string Name { get; set; } = null!; + } +} diff --git a/web/Areas/CTS/Models/ServiceDto.cs b/web/Areas/CTS/Models/ServiceDto.cs new file mode 100644 index 0000000..0da048a --- /dev/null +++ b/web/Areas/CTS/Models/ServiceDto.cs @@ -0,0 +1,8 @@ +namespace Viper.Areas.CTS.Models +{ + public class ServiceDto + { + public int ServiceId { get; set; } + public string Name { get; set; } = null!; + } +} \ No newline at end of file diff --git a/web/Areas/CTS/Services/CtsNavMenu.cs b/web/Areas/CTS/Services/CtsNavMenu.cs index 84f9670..0f33af4 100644 --- a/web/Areas/CTS/Services/CtsNavMenu.cs +++ b/web/Areas/CTS/Services/CtsNavMenu.cs @@ -44,8 +44,10 @@ public NavMenu Nav() nav.Add(new NavMenuItem() { MenuItemText = "Admin Functions", IsHeader = true }); nav.Add(new NavMenuItem() { MenuItemText = "Manage Domains", MenuItemURL = "ManageDomains" }); nav.Add(new NavMenuItem() { MenuItemText = "Manage Competencies", MenuItemURL = "ManageCompetencies" }); + nav.Add(new NavMenuItem() { MenuItemText = "Manage Bundles", MenuItemURL = "ManageBundles" }); nav.Add(new NavMenuItem() { MenuItemText = "Manage Levels", MenuItemURL = "ManageLevels" }); nav.Add(new NavMenuItem() { MenuItemText = "Manage EPAs", MenuItemURL = "ManageEPAs" }); + nav.Add(new NavMenuItem() { MenuItemText = "Manage Roles", MenuItemURL = "ManageRoles" }); nav.Add(new NavMenuItem() { MenuItemText = "Audit Log", MenuItemURL = "Audit" }); nav.Add(new NavMenuItem() { MenuItemText = "Reports", IsHeader = true }); diff --git a/web/Classes/SQLContext/CtsContext.cs b/web/Classes/SQLContext/CtsContext.cs index f8afd14..f0c144a 100644 --- a/web/Classes/SQLContext/CtsContext.cs +++ b/web/Classes/SQLContext/CtsContext.cs @@ -54,6 +54,9 @@ partial void OnModelCreatingCTS(ModelBuilder modelBuilder) entity.ToTable("Competency", "cts"); entity.Property(e => e.Description).IsUnicode(false); + entity.Property(e => e.Number) + .HasMaxLength(20) + .IsUnicode(false); entity.Property(e => e.Name) .HasMaxLength(250) .IsUnicode(false); @@ -204,7 +207,7 @@ partial void OnModelCreatingCTS(ModelBuilder modelBuilder) .HasForeignKey(d => d.BundleId) .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("FK_BundleRole_Bundle"); - entity.HasOne(d => d.Role).WithMany() + entity.HasOne(d => d.Role).WithMany(r => r.BundleRoles) .HasForeignKey(d => d.RoleId) .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("FK_BundleRole_Role"); diff --git a/web/Models/CTS/Competency.cs b/web/Models/CTS/Competency.cs index 3b230f9..4c80bf2 100644 --- a/web/Models/CTS/Competency.cs +++ b/web/Models/CTS/Competency.cs @@ -11,6 +11,8 @@ public partial class Competency public int? ParentId { get; set; } + public string Number { get; set; } = null!; + public string Name { get; set; } = null!; public string? Description { get; set; } diff --git a/web/Models/CTS/Role.cs b/web/Models/CTS/Role.cs index 7528e4e..a9263c2 100644 --- a/web/Models/CTS/Role.cs +++ b/web/Models/CTS/Role.cs @@ -8,4 +8,6 @@ public partial class Role public int RoleId { get; set; } public string Name { get; set; } = null!; + + public virtual ICollection BundleRoles { get; set; } = new List(); } diff --git a/web/Program.cs b/web/Program.cs index 6f75abc..6156734 100644 --- a/web/Program.cs +++ b/web/Program.cs @@ -190,6 +190,9 @@ // Add Data Protection services (i.e. encryption) builder.Services.AddDataProtection(); + // Add automapper + builder.Services.AddAutoMapper(typeof(Program)); + var app = builder.Build(); // Add Content Security Policy diff --git a/web/Viper.csproj b/web/Viper.csproj index 49b4d01..6c3f27a 100644 --- a/web/Viper.csproj +++ b/web/Viper.csproj @@ -31,6 +31,7 @@ + From 18fde74bbd1213a0d4ab50e8c49a97b2b0cd9be5 Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 19 Sep 2024 13:47:47 -0700 Subject: [PATCH 07/20] Working on bundles --- .../CTS/pages/ManageBundleCompetencies.vue | 267 ++++++++++++++++++ VueApp/src/CTS/pages/ManageBundles.vue | 69 +++-- VueApp/src/CTS/pages/ManageCompetencies.vue | 2 +- VueApp/src/CTS/router/routes.ts | 6 +- VueApp/src/CTS/types/index.ts | 32 +++ .../Controllers/BundleCompetencyController.cs | 207 ++++++++++++++ .../BundleCompetencyGroupController.cs | 156 ++++++++++ web/Areas/CTS/Controllers/BundleController.cs | 114 +++++++- web/Areas/CTS/Controllers/LevelsController.cs | 8 +- web/Areas/CTS/Models/AutoMapperProfileCts.cs | 11 +- .../CTS/Models/BundleCompetencyAddUpdate.cs | 13 + web/Areas/CTS/Models/BundleCompetencyDto.cs | 12 +- .../CTS/Models/BundleCompetencyGroupDto.cs | 4 +- web/Areas/CTS/Models/BundleDto.cs | 6 +- web/Areas/CTS/Models/CompetencyDto.cs | 2 +- web/Classes/SQLContext/CtsContext.cs | 19 +- web/Models/CTS/Bundle.cs | 2 +- web/Models/CTS/BundleCompetency.cs | 4 + web/Models/CTS/BundleCompetencyLevel.cs | 16 ++ web/Models/CTS/BundleLevel.cs | 16 -- web/Models/CTS/Competency.cs | 2 + 21 files changed, 908 insertions(+), 60 deletions(-) create mode 100644 VueApp/src/CTS/pages/ManageBundleCompetencies.vue create mode 100644 web/Areas/CTS/Controllers/BundleCompetencyController.cs create mode 100644 web/Areas/CTS/Controllers/BundleCompetencyGroupController.cs create mode 100644 web/Areas/CTS/Models/BundleCompetencyAddUpdate.cs create mode 100644 web/Models/CTS/BundleCompetencyLevel.cs delete mode 100644 web/Models/CTS/BundleLevel.cs diff --git a/VueApp/src/CTS/pages/ManageBundleCompetencies.vue b/VueApp/src/CTS/pages/ManageBundleCompetencies.vue new file mode 100644 index 0000000..12fde4e --- /dev/null +++ b/VueApp/src/CTS/pages/ManageBundleCompetencies.vue @@ -0,0 +1,267 @@ + + \ No newline at end of file diff --git a/VueApp/src/CTS/pages/ManageBundles.vue b/VueApp/src/CTS/pages/ManageBundles.vue index e6d41c8..02a6ebe 100644 --- a/VueApp/src/CTS/pages/ManageBundles.vue +++ b/VueApp/src/CTS/pages/ManageBundles.vue @@ -5,16 +5,16 @@ import { useFetch } from '@/composables/ViperFetch' const { get, post, put, del } = useFetch() - import type { Bundle, Role, BundleRole } from '@/CTS/types' + import type { Bundle, Role } from '@/CTS/types' const apiUrl = inject('apiURL') //form props - const emptyBundle = { bundleId: null, assessment: false, clinical: false, milestone: false, name: "" } as Bundle - const bundle = ref(emptyBundle) as Ref + const emptyBundle = { bundleId: null, assessment: false, clinical: false, milestone: false, name: "", roles: [] } as Bundle + const bundle = ref(structuredClone(emptyBundle)) as Ref const showBundleForm = ref(false) const roles = ref([]) as Ref - const bundleRoles = ref([]) as Ref + const bundleRoles = ref([]) as Ref //bundle table props const bundles = ref([]) as Ref @@ -23,6 +23,7 @@ { name: "action", label: "", field: "id", align: "left" }, { name: "name", label: "Name", field: "name", align: "left", sortable: true }, { name: "compcount", label: "Competency Count", field: "competencyCount", align: "left", sortable: true }, + { name: "roles", label: "Roles", field: "roles", align: "left", sortable: true }, { name: "clinical", label: "Clinical", field: "clinical", align: "left", sortable: true }, { name: "assessment", label: "Assessment", field: "assessment", align: "left", sortable: true }, { name: "milestone", label: "Milestone", field: "milestone", align: "left", sortable: true }, @@ -49,15 +50,21 @@ success = r.success } + console.log(bundle.value, emptyBundle) //update roles and then reload if (success) { - await put(apiUrl + "cts/bundles/" + bundle.value.bundleId + "/roles", bundleRoles) + await put(apiUrl + "cts/bundles/" + bundle.value.bundleId + "/roles", bundleRoles.value) clearBundle() } } + function selectBundle(b: Bundle) { + bundle.value = b + bundleRoles.value = b.roles.map(r => r.roleId) + } function clearBundle() { bundle.value = emptyBundle + bundleRoles.value = [] load() } @@ -78,6 +85,14 @@
+
+ +
@@ -87,14 +102,6 @@
-
- -
@@ -112,9 +119,37 @@ :filter="filter" :loading="loading" class="q-mt-md"> - + + + + + + -
- -
\ No newline at end of file diff --git a/VueApp/src/CTS/pages/ManageCompetencies.vue b/VueApp/src/CTS/pages/ManageCompetencies.vue index ef1dbf0..3cc84b1 100644 --- a/VueApp/src/CTS/pages/ManageCompetencies.vue +++ b/VueApp/src/CTS/pages/ManageCompetencies.vue @@ -9,7 +9,7 @@ const domains = ref([]) as Ref const competencies = ref([]) as Ref const emptyComp = { name: "", number: "", description: "", canLinkToStudent: false, domainId: 0, parentId: null, competencyId: null, domain: null, children: null } as Competency - const selectedComp = ref(emptyComp) as Ref + const selectedComp = ref(structuredClone(emptyComp)) as Ref const loaded = ref(false) const showForm = ref(false) diff --git a/VueApp/src/CTS/router/routes.ts b/VueApp/src/CTS/router/routes.ts index 4adbccc..f9e6ad0 100644 --- a/VueApp/src/CTS/router/routes.ts +++ b/VueApp/src/CTS/router/routes.ts @@ -1,7 +1,6 @@ import ViperLayout from '@/layouts/ViperLayout.vue' import ViperLayoutSimple from '@/layouts/ViperLayoutSimple.vue' -const viperURL = import.meta.env.VITE_VIPER_HOME const ctsBreadcrumbs = [{ url: "Home", name: "Return to CTS 2.0" }] const routes = [ @@ -67,6 +66,11 @@ const routes = [ meta: { layout: ViperLayout }, component: () => import('@/CTS/pages/ManageBundles.vue'), }, + { + path: '/CTS/ManageBundleCompetencies', + meta: { layout: ViperLayout }, + component: () => import('@/CTS/pages/ManageBundleCompetencies.vue'), + }, { path: '/CTS/ManageRoles', meta: { layout: ViperLayout }, diff --git a/VueApp/src/CTS/types/index.ts b/VueApp/src/CTS/types/index.ts index 10b83ec..dc9e9cd 100644 --- a/VueApp/src/CTS/types/index.ts +++ b/VueApp/src/CTS/types/index.ts @@ -119,10 +119,42 @@ export type Bundle = { clinical: boolean, assessment: boolean, milestone: boolean, + roles: Role[] } export type BundleRole = { bundleRoleId: number, bundleId: number, roleId: number, +} + +export type BundleCompetency = { + bundleCompetencyId: number, + bundleId: number, + roleId: number | null, + roleName: number | null, + levels: Level[], + competencyId: number, + competencyNumber: string, + competencyName: string, + description: string | null, + canLinkToStudent: boolean, + bundleCompetencyGroupId: number | null, + order: number, +} + +export type BundleCompetencyAddUpdate = { + bundleCompetencyId: number | null, + bundleId: number, + competencyId: number | null, + order: number, + levelIds: number[], + roleId: number | null, + bundleCompetencyGroupId: number | null, +} + +export type BundleCompetencyGroup = { + bundleCompetencyGroupId: number | null, + name: string, + order: number, } \ No newline at end of file diff --git a/web/Areas/CTS/Controllers/BundleCompetencyController.cs b/web/Areas/CTS/Controllers/BundleCompetencyController.cs new file mode 100644 index 0000000..2e0c360 --- /dev/null +++ b/web/Areas/CTS/Controllers/BundleCompetencyController.cs @@ -0,0 +1,207 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Diagnostics; +using System.Reflection.Metadata; +using Viper.Areas.CTS.Models; +using Viper.Classes; +using Viper.Classes.SQLContext; +using Viper.Models.CTS; +using Web.Authorization; + +namespace Viper.Areas.CTS.Controllers +{ + [Route("/api/cts/bundles/{bundleId}/competencies")] + [Permission(Allow = "SVMSecure")] + public class BundleCompetencyController : ApiController + { + private readonly IMapper mapper; + private readonly VIPERContext context; + + public BundleCompetencyController(IMapper mapper, VIPERContext context) + { + this.mapper = mapper; + this.context = context; + } + + private bool BundleExists(int bundleId) + { + return context.Bundles.Where(b => b.BundleId == bundleId).Any(); + } + + private bool CompetencyExists(int competencyId) + { + return context.Competencies.Where(b => b.CompetencyId == competencyId).Any(); + } + + [HttpGet] + public async Task>> GetBundleCompetencies(int bundleId) + { + if (!BundleExists(bundleId)) + { + return NotFound(); + } + + var bundleComps = await context.BundleCompetencies + .Include(bc => bc.Competency) + .Include(bc => bc.BundleCompetencyGroup) + .Include(bc => bc.BundleCompetencyLevels) + .ThenInclude(bcl => bcl.Level) + .Where(bc => bc.BundleId == bundleId) + .OrderBy(bc => bc.BundleCompetencyGroup == null ? 0 : bc.BundleCompetencyGroup.Order) + .ThenBy(bc => bc.Order) + .ThenBy(bc => bc.Competency.Name) + .ToListAsync(); + + return mapper.Map>(bundleComps); + } + + [HttpPost] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> AddBundleCompetency(int bundleId, BundleCompetencyAddUpdate bundleComp) + { + if (!BundleExists(bundleId) || !CompetencyExists(bundleComp.CompetencyId)) + { + return NotFound(); + } + var compExistsAlready = await context.BundleCompetencies + .Where(bc => bc.BundleId == bundleId && bc.CompetencyId == bundleComp.CompetencyId && bc.RoleId == bundleComp.RoleId) + .AnyAsync(); + if(compExistsAlready) + { + return BadRequest("Competency is already a part of this bundle."); + } + + var bundleCompetency = new BundleCompetency() + { + BundleId = bundleId, + CompetencyId = bundleComp.CompetencyId, + BundleCompetencyGroupId = bundleComp.BundleCompetencyGroupId, + Order = bundleComp.Order, + RoleId = bundleComp.RoleId + }; + + //Create the bundle competency and add levels + using var trans = context.Database.BeginTransaction(); + context.Add(bundleCompetency); + await context.SaveChangesAsync(); + + foreach (var levelId in bundleComp.LevelIds) + { + var level = await context.Levels.FindAsync(levelId); + if (level != null) + { + context.Add(new BundleCompetencyLevel() + { + BundleCompetencyId = bundleCompetency.BundleCompetencyId, + LevelId = levelId + }); + } + } + await context.SaveChangesAsync(); + AdjustBundleCompetencyOrders(bundleCompetency); + await trans.CommitAsync(); + + return mapper.Map(bundleCompetency); + } + + [HttpPut("{bundleCompetencyId}")] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> UpdateBundleCompetency(int bundleId, int bundleCompetencyId, BundleCompetencyAddUpdate bundleCompetency) + { + var bundleComp = context.BundleCompetencies.Find(bundleCompetencyId); + if (bundleComp == null) + { + return NotFound(); + } + if (!BundleExists(bundleId) || !CompetencyExists(bundleCompetency.CompetencyId)) + { + return NotFound(); + } + + using var trans = context.Database.BeginTransaction(); + bundleComp.Order = bundleCompetency.Order; + foreach (var existingLevel in bundleComp.BundleCompetencyLevels) + { + if (!bundleCompetency.LevelIds.Any(l => l == existingLevel.LevelId)) + { + context.BundleCompetencyLevels.Remove(existingLevel); + } + } + foreach (var newLevel in bundleCompetency.LevelIds) + { + if (!bundleComp.BundleCompetencyLevels.Any(bcl => bcl.LevelId == newLevel)) + { + context.Add(new BundleCompetencyLevel() + { + BundleCompetencyId = bundleCompetencyId, + LevelId = newLevel + }); + } + } + AdjustBundleCompetencyOrders(bundleComp); + + await context.SaveChangesAsync(); + await trans.CommitAsync(); + + return mapper.Map(bundleComp); + } + + [HttpDelete("{bundleCompetencyId}")] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> DeleteBundleCompetency(int bundleId, int bundleCompetencyId) + { + var bundleComp = context.BundleCompetencies.Find(bundleCompetencyId); + if (bundleComp == null) + { + return NotFound(); + } + if (!BundleExists(bundleId)) + { + return NotFound(); + } + + try + { + context.BundleCompetencies.Remove(bundleComp); + await context.SaveChangesAsync(); + AdjustBundleCompetencyOrders(bundleComp); + } + catch (Exception) + { + return BadRequest("Cannot delete this bundle competency."); + } + + return mapper.Map(bundleComp); + } + + private void AdjustBundleCompetencyOrders(BundleCompetency bundleComp) + { + var bundleComps = context.BundleCompetencies + .Where(b => b.BundleId == bundleComp.BundleId) + .Where(b => b.BundleCompetencyGroupId == bundleComp.BundleCompetencyGroupId) + .OrderBy(b => b.Order) + .ToList(); + + //check orders are correct + int i = 1; + bool changeMade = false; + foreach (var b in bundleComps) + { + if (b.Order != i) + { + //not correct + b.Order = i; + context.Entry(b).State = EntityState.Modified; + changeMade = true; + } + i++; + } + + if (changeMade) + { + context.SaveChanges(); + } + } + } +} diff --git a/web/Areas/CTS/Controllers/BundleCompetencyGroupController.cs b/web/Areas/CTS/Controllers/BundleCompetencyGroupController.cs new file mode 100644 index 0000000..cbbb2f7 --- /dev/null +++ b/web/Areas/CTS/Controllers/BundleCompetencyGroupController.cs @@ -0,0 +1,156 @@ +using AutoMapper; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Internal; +using Viper.Areas.CTS.Models; +using Viper.Classes; +using Viper.Classes.SQLContext; +using Viper.Models.CTS; +using Web.Authorization; + +namespace Viper.Areas.CTS.Controllers +{ + [Route("/api/cts/bundles/{bundleId}/groups")] + [Permission(Allow = "SVMSecure")] + public class BundleCompetencyGroupController : ApiController + { + private readonly IMapper mapper; + private readonly VIPERContext context; + public BundleCompetencyGroupController(IMapper mapper, VIPERContext context) + { + this.mapper = mapper; + this.context = context; + } + + private bool BundleExists(int bundleId) + { + return context.Bundles.Where(b => b.BundleId == bundleId).Any(); + } + + private bool SameNameExists(int bundleId, string name, int? bundleCompetencyGroupId = null) + { + return context.BundleCompetencyGroups + .Where(g => g.BundleId == bundleId + && g.Name == name + && (bundleCompetencyGroupId == null || bundleCompetencyGroupId != g.BundleCompetencyGroupId)) + .Any(); + } + + [HttpGet] + public async Task>> GetGroups(int bundleId) + { + if (!BundleExists(bundleId)) + { + return NotFound(); + } + var groups = await context.BundleCompetencyGroups + .Where(g => g.BundleId == bundleId) + .ToListAsync(); + return mapper.Map>(groups); + } + + [HttpPost] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> AddGroup(int bundleId, BundleCompetencyGroupDto groupDto) + { + if (!BundleExists(bundleId)) + { + return NotFound(); + } + if (SameNameExists(bundleId, groupDto.Name)) + { + return BadRequest("Name must be unique"); + } + + var group = new BundleCompetencyGroup() + { + BundleId = bundleId, + Name = groupDto.Name, + Order = groupDto.Order + }; + context.BundleCompetencyGroups.Add(group); + await context.SaveChangesAsync(); + AdjustGroupOrders(group); + return mapper.Map(group); + } + + [HttpPut("{bundleCompetencyGroupId}")] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> UpdateGroup(int bundleId, int bundleCompetencyGroupId, BundleCompetencyGroupDto groupDto) + { + if (!BundleExists(bundleId)) + { + return NotFound(); + } + var group = await context.BundleCompetencyGroups.FindAsync(bundleCompetencyGroupId); + if (group == null) + { + return NotFound(); + } + if (SameNameExists(bundleId, groupDto.Name, bundleCompetencyGroupId)) + { + return BadRequest("Name must be unique"); + } + + group.Name = groupDto.Name; + group.Order = groupDto.Order; + context.Update(group); + await context.SaveChangesAsync(); + AdjustGroupOrders(group); + return mapper.Map(group); + } + + [HttpDelete("{bundleCompetencyGroupId}")] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> DeleteGroup(int bundleId, int bundleCompetencyGroupId) + { + if (!BundleExists(bundleId)) + { + return NotFound(); + } + var group = await context.BundleCompetencyGroups.FindAsync(bundleCompetencyGroupId); + if (group == null) + { + return NotFound(); + } + try + { + context.Remove(group); + await context.SaveChangesAsync(); + AdjustGroupOrders(group); + } + catch (Exception) + { + return BadRequest("Cannot delete group. Competencies must be removed from the group, and the group cannot have been used to document a student competency."); + } + return mapper.Map(group); + } + + private void AdjustGroupOrders(BundleCompetencyGroup group) + { + var groups = context.BundleCompetencyGroups + .Where(g => g.BundleId == group.BundleId) + .OrderBy(g => g.Order) + .ToList(); + + int i = 1; + bool changeMade = false; + foreach (var g in groups) + { + if (g.Order != i) + { + //not correct + g.Order = i; + context.Entry(g).State = EntityState.Modified; + changeMade = true; + } + i++; + } + + if (changeMade) + { + context.SaveChanges(); + } + } + } +} diff --git a/web/Areas/CTS/Controllers/BundleController.cs b/web/Areas/CTS/Controllers/BundleController.cs index 421c7e6..833c418 100644 --- a/web/Areas/CTS/Controllers/BundleController.cs +++ b/web/Areas/CTS/Controllers/BundleController.cs @@ -1,6 +1,7 @@ using AutoMapper; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; +using System.Data; using Viper.Areas.CTS.Models; using Viper.Classes; using Viper.Classes.SQLContext; @@ -23,10 +24,12 @@ public BundleController(IMapper mapper, VIPERContext context) } [HttpGet] - public async Task>> GetBundles(bool? clinical = null, bool? assessment = null, bool? milestone = null, + public async Task>> GetBundles(bool? clinical = null, bool? assessment = null, bool? milestone = null, int? serviceId = null, int? roleId = null) { var bundleQuery = context.Bundles + .Include(b => b.BundleRoles) + .ThenInclude(br => br.Role) .AsQueryable(); if (clinical != null) { @@ -56,9 +59,29 @@ public async Task>> GetBundles(bool? clinical = nul return mapper.Map>(bundles); } + [HttpGet("{bundleId}")] + public async Task> GetBundle(int bundleId) + { + var bundle = await context.Bundles + .Include(b => b.BundleRoles) + .ThenInclude(br => br.Role) + .Where(b => b.BundleId == bundleId) + .FirstOrDefaultAsync(); + if (bundle == null) + { + return NotFound(); + } + return mapper.Map(bundle); + } + [HttpPost] + [Permission(Allow = "SVMSecure.CTS.Manage")] public async Task> AddBundle(BundleDto bundleDto) { + if (string.IsNullOrEmpty(bundleDto.Name)) + { + return BadRequest("Bundle Name is required."); + } var nameCheck = await context.Bundles.Where(b => b.Name == bundleDto.Name).AnyAsync(); if (nameCheck) { @@ -68,8 +91,95 @@ public async Task> AddBundle(BundleDto bundleDto) Bundle b = mapper.Map(bundleDto); context.Add(b); await context.SaveChangesAsync(); - + + return mapper.Map(b); + } + + [HttpPut("{bundleId}")] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> UpdateBundle(int bundleId, BundleDto bundleDto) + { + var exists = await context.Bundles.AnyAsync(b => b.BundleId == bundleId); + if (!exists) + { + return NotFound(); + } + if (string.IsNullOrEmpty(bundleDto.Name)) + { + return BadRequest("Bundle Name is required."); + } + var nameCheck = await context.Bundles.Where(b => b.Name == bundleDto.Name && b.BundleId != bundleId).AnyAsync(); + if (nameCheck) + { + return BadRequest("Bundle Name must be unique."); + } + Bundle b = mapper.Map(bundleDto); + context.Update(b); + await context.SaveChangesAsync(); return mapper.Map(b); } + + [HttpDelete("{bundleId}")] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task> DeleteBundle(int bundleId) + { + var bundle = await context.Bundles.FindAsync(bundleId); + if (bundle == null) + { + return NotFound(); + } + + try + { + using var trans = context.Database.BeginTransaction(); + var bundleRoles = context.BundleRoles.Where(br => br.BundleId == bundleId); + foreach (var role in bundleRoles) + { + context.Remove(role); + } + context.Entry(bundle).State = EntityState.Deleted; + await context.SaveChangesAsync(); + await trans.CommitAsync(); + } + catch (Exception) + { + return BadRequest("Could not delete bundle. If this bundle has been used, it cannot be deleted."); + } + return mapper.Map(bundle); + } + + /* Bundle Roles */ + [HttpPut("{bundleId}/roles/")] + [Permission(Allow = "SVMSecure.CTS.Manage")] + public async Task>> SetBundleRoles(int bundleId, List bundleRoles) + { + using var trans = context.Database.BeginTransaction(); + var existing = await context.BundleRoles.Where(br => br.BundleId == bundleId).ToListAsync(); + foreach (var brId in bundleRoles) + { + if (!existing.Any(e => e.RoleId == brId)) + { + context.Add(new BundleRole() + { + BundleId = bundleId, + RoleId = brId + }); + } + } + + foreach (var e in existing) + { + if (!bundleRoles.Any(brId => brId == e.RoleId)) + { + context.Entry(e).State = EntityState.Deleted; + } + } + + await context.SaveChangesAsync(); + await trans.CommitAsync(); + + var roles = await context.BundleRoles.Where(br => br.BundleId == bundleId).Select(br => br.Role).ToListAsync(); + return mapper.Map>(roles); + } } } diff --git a/web/Areas/CTS/Controllers/LevelsController.cs b/web/Areas/CTS/Controllers/LevelsController.cs index 2e605aa..81e3a19 100644 --- a/web/Areas/CTS/Controllers/LevelsController.cs +++ b/web/Areas/CTS/Controllers/LevelsController.cs @@ -176,19 +176,19 @@ private void AdjustLevelOrders(Level level) } if (level.Milestone) { - levels = levels.Where(l => !l.Milestone); + levels = levels.Where(l => l.Milestone); } if (level.Course) { - levels = levels.Where(l => !l.Milestone); + levels = levels.Where(l => l.Course); } if (level.Clinical) { - levels = levels.Where(l => !l.Milestone); + levels = levels.Where(l => l.Clinical); } if (level.Dops) { - levels = levels.Where(l => !l.Milestone); + levels = levels.Where(l => l.Dops); } levels = levels.OrderBy(l => l.Order); diff --git a/web/Areas/CTS/Models/AutoMapperProfileCts.cs b/web/Areas/CTS/Models/AutoMapperProfileCts.cs index 6f250d9..385a5cf 100644 --- a/web/Areas/CTS/Models/AutoMapperProfileCts.cs +++ b/web/Areas/CTS/Models/AutoMapperProfileCts.cs @@ -6,10 +6,17 @@ namespace Viper.Areas.CTS.Models public class AutoMapperProfileCts : Profile { public AutoMapperProfileCts() { - CreateMap(); + CreateMap() + .ForMember(dest => dest.Levels, opt => opt.MapFrom(src => src.BundleCompetencyLevels.Select(bcl => bcl.Level).ToList())) + .ForMember(dest => dest.CompetencyName, opt => opt.MapFrom(src => src.Competency.Name)) + .ForMember(dest => dest.CompetencyNumber, opt => opt.MapFrom(src => src.Competency.Number)) + .ForMember(dest => dest.Description, opt => opt.MapFrom(src => src.Competency.Description)) + .ForMember(dest => dest.CanLinkToStudent, opt => opt.MapFrom(src => src.Competency.CanLinkToStudent)); CreateMap(); CreateMap() - .ForMember(dest => dest.CompetencyCount, opt => opt.MapFrom(src => src.BundleCompetencies.Count)); + .ForMember(dest => dest.CompetencyCount, opt => opt.MapFrom(src => src.BundleCompetencies.Count)) + .ForMember(dest => dest.Roles, opt => opt.MapFrom(src => src.BundleRoles.Select(br => br.Role).ToList())) + .ReverseMap(); CreateMap(); CreateMap().ReverseMap(); CreateMap(); diff --git a/web/Areas/CTS/Models/BundleCompetencyAddUpdate.cs b/web/Areas/CTS/Models/BundleCompetencyAddUpdate.cs new file mode 100644 index 0000000..237ac67 --- /dev/null +++ b/web/Areas/CTS/Models/BundleCompetencyAddUpdate.cs @@ -0,0 +1,13 @@ +namespace Viper.Areas.CTS.Models +{ + public class BundleCompetencyAddUpdate + { + public int? BundleCompetencyId { get; set; } + public int BundleId { get; set; } + public int CompetencyId { get; set; } + public int Order { get; set; } + public List LevelIds { get; set; } = new List(); + public int? RoleId { get; set; } + public int? BundleCompetencyGroupId { get; set; } + } +} diff --git a/web/Areas/CTS/Models/BundleCompetencyDto.cs b/web/Areas/CTS/Models/BundleCompetencyDto.cs index 4d5f7d8..97c377d 100644 --- a/web/Areas/CTS/Models/BundleCompetencyDto.cs +++ b/web/Areas/CTS/Models/BundleCompetencyDto.cs @@ -5,14 +5,20 @@ public class BundleCompetencyDto public int BundleCompetencyId { get; set; } public int BundleId { get; set; } - public int RoleId { get; set; } - public string RoleName { get; set; } = null!; + public int? RoleId { get; set; } + public string? RoleName { get; set; } = null!; + public IEnumerable Levels { get; set; } = new List(); + + //comp info public int CompetencyId { get; set; } + public string CompetencyNumber { get; set; } = null!; public string CompetencyName { get; set; } = null!; public string? Description { get; set; } public bool CanLinkToStudent { get; set; } - + + //group and order + public int? BundleCompetencyGroupId { get; set; } public int Order { get; set; } } } diff --git a/web/Areas/CTS/Models/BundleCompetencyGroupDto.cs b/web/Areas/CTS/Models/BundleCompetencyGroupDto.cs index 139d831..2831033 100644 --- a/web/Areas/CTS/Models/BundleCompetencyGroupDto.cs +++ b/web/Areas/CTS/Models/BundleCompetencyGroupDto.cs @@ -2,9 +2,9 @@ { public class BundleCompetencyGroupDto { - public int BundleCompetencyGroupId { get; set; } + public int? BundleCompetencyGroupId { get; set; } public string Name { get; set; } = null!; public int Order { get; set; } - public IEnumerable Competencies { get; set; } = new List(); + //public IEnumerable Competencies { get; set; } = new List(); } } diff --git a/web/Areas/CTS/Models/BundleDto.cs b/web/Areas/CTS/Models/BundleDto.cs index 9ccd1c1..81314bb 100644 --- a/web/Areas/CTS/Models/BundleDto.cs +++ b/web/Areas/CTS/Models/BundleDto.cs @@ -4,14 +4,14 @@ namespace Viper.Areas.CTS.Models { public class BundleDto { - public int BundleId { get; set; } + public int? BundleId { get; set; } public string Name { get; set; } = null!; public bool Clinical { get; set; } public bool Assessment { get; set; } public bool Milestone { get; set; } public int CompetencyCount { get; set; } - //Only including levels here - other related objects can be found separately - public IEnumerable Levels { get; set; } = new List(); + //Only including roles here - other related objects can be found separately + public IEnumerable Roles{ get; set; } = new List(); } } diff --git a/web/Areas/CTS/Models/CompetencyDto.cs b/web/Areas/CTS/Models/CompetencyDto.cs index 9f53a47..7e7e40c 100644 --- a/web/Areas/CTS/Models/CompetencyDto.cs +++ b/web/Areas/CTS/Models/CompetencyDto.cs @@ -31,7 +31,7 @@ public CompetencyDto(Competency c) CanLinkToStudent = c.CanLinkToStudent; DomainName = c?.Domain?.Name; DomainOrder = c?.Domain?.Order; - Parent = c.Parent != null ? new CompetencyDto(c.Parent) : null; + Parent = c?.Parent != null ? new CompetencyDto(c.Parent) : null; } } } diff --git a/web/Classes/SQLContext/CtsContext.cs b/web/Classes/SQLContext/CtsContext.cs index f0c144a..484af85 100644 --- a/web/Classes/SQLContext/CtsContext.cs +++ b/web/Classes/SQLContext/CtsContext.cs @@ -18,7 +18,7 @@ public partial class VIPERContext : DbContext public virtual DbSet Bundles { get; set; } public virtual DbSet BundleCompetencies { get; set; } public virtual DbSet BundleCompetencyGroups { get; set; } - public virtual DbSet BundleLevels { get; set; } + public virtual DbSet BundleCompetencyLevels { get; set; } public virtual DbSet BundleRoles { get; set; } public virtual DbSet BundleServices { get; set; } public virtual DbSet CompetencyOutcomes { get; set; } @@ -166,6 +166,11 @@ partial void OnModelCreatingCTS(ModelBuilder modelBuilder) .HasForeignKey(d => d.BundleId) .OnDelete(DeleteBehavior.ClientSetNull) .HasConstraintName("FK_BundleCompetency_Bundle"); + + entity.HasOne(d => d.Competency).WithMany(p => p.BundleCompetencies) + .HasForeignKey(d => d.CompetencyId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("FK_BundleCompetency_Competency"); }); modelBuilder.Entity(entity => @@ -184,15 +189,15 @@ partial void OnModelCreatingCTS(ModelBuilder modelBuilder) .HasConstraintName("FK_BundleCompetencyGroupId_Bundle"); }); - modelBuilder.Entity(entity => + modelBuilder.Entity(entity => { - entity.ToTable("BundleLevel", "cts"); + entity.ToTable("BundleCompetencyLevel", "cts"); + entity.HasKey(e => e.BundleCompetencyLevelId); - entity.HasOne(d => d.Bundle).WithMany(p => p.BundleLevels) - .HasForeignKey(d => d.BundleId) + entity.HasOne(d => d.BundleCompetency).WithMany(p => p.BundleCompetencyLevels) + .HasForeignKey(d => d.BundleCompetencyId) .OnDelete(DeleteBehavior.ClientSetNull) - .HasConstraintName("FK_BundleLevel_Bundle"); - + .HasConstraintName("FK_BundleLevel_BundleCompetency"); entity.HasOne(d => d.Level).WithMany() .HasForeignKey(d => d.LevelId) .OnDelete(DeleteBehavior.ClientSetNull) diff --git a/web/Models/CTS/Bundle.cs b/web/Models/CTS/Bundle.cs index 5f1599a..82fd1c4 100644 --- a/web/Models/CTS/Bundle.cs +++ b/web/Models/CTS/Bundle.cs @@ -19,7 +19,7 @@ public partial class Bundle public virtual ICollection BundleCompetencyGroups { get; set; } = new List(); - public virtual ICollection BundleLevels { get; set; } = new List(); + //public virtual ICollection BundleLevels { get; set; } = new List(); public virtual ICollection BundleRoles { get; set; } = new List(); diff --git a/web/Models/CTS/BundleCompetency.cs b/web/Models/CTS/BundleCompetency.cs index 13ca2d6..089ff70 100644 --- a/web/Models/CTS/BundleCompetency.cs +++ b/web/Models/CTS/BundleCompetency.cs @@ -19,5 +19,9 @@ public partial class BundleCompetency public virtual Bundle Bundle { get; set; } = null!; + public virtual Competency Competency { get; set; } = null!; + + public virtual ICollection BundleCompetencyLevels { get; set; } = new List(); + public virtual BundleCompetencyGroup? BundleCompetencyGroup { get; set; } } diff --git a/web/Models/CTS/BundleCompetencyLevel.cs b/web/Models/CTS/BundleCompetencyLevel.cs new file mode 100644 index 0000000..ff559ce --- /dev/null +++ b/web/Models/CTS/BundleCompetencyLevel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace Viper.Models.CTS; + +public partial class BundleCompetencyLevel +{ + public int BundleCompetencyLevelId { get; set; } + + public int BundleCompetencyId { get; set; } + + public int LevelId { get; set; } + + public virtual BundleCompetency BundleCompetency { get; set; } = null!; + public virtual Level Level { get; set; } = null!; +} diff --git a/web/Models/CTS/BundleLevel.cs b/web/Models/CTS/BundleLevel.cs deleted file mode 100644 index 81c5966..0000000 --- a/web/Models/CTS/BundleLevel.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Viper.Models.CTS; - -public partial class BundleLevel -{ - public int BundleLevelId { get; set; } - - public int BundleId { get; set; } - - public int LevelId { get; set; } - - public virtual Bundle Bundle { get; set; } = null!; - public virtual Level Level { get; set; } = null!; -} diff --git a/web/Models/CTS/Competency.cs b/web/Models/CTS/Competency.cs index 4c80bf2..3c48068 100644 --- a/web/Models/CTS/Competency.cs +++ b/web/Models/CTS/Competency.cs @@ -23,5 +23,7 @@ public partial class Competency public virtual ICollection Children { get; set; } = new List(); + public virtual ICollection BundleCompetencies { get; set; } = new List(); + public virtual Competency? Parent { get; set; } } From c8647451ce059dac1be656ff8ff264b6fc037f4a Mon Sep 17 00:00:00 2001 From: Brandon Date: Thu, 19 Sep 2024 13:52:56 -0700 Subject: [PATCH 08/20] temp fix --- VueApp/src/CTS/pages/ManageBundleCompetencies.vue | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/VueApp/src/CTS/pages/ManageBundleCompetencies.vue b/VueApp/src/CTS/pages/ManageBundleCompetencies.vue index 12fde4e..73fd18f 100644 --- a/VueApp/src/CTS/pages/ManageBundleCompetencies.vue +++ b/VueApp/src/CTS/pages/ManageBundleCompetencies.vue @@ -251,11 +251,12 @@ {{ props.row.competencyNumber }} {{ props.row.competencyName }} -