Repository: bcgov/jasper
Pull request #254: JASPER-344: Court list - Print
Author: login: ronaldo-macapobre
Labels: name: enhancement
description: New feature or request
name: javascript
description: Pull requests that update Javascript code
name: .NET
description: Pull requests that update .net code
Created: 2025-04-02T00:17:36Z | Merged: 2025-04-03T15:56:24Z

# Pull Request for JIRA Ticket: JASPER-344

## Issue ticket number and link  
https://jira.justice.gov.bc.ca/browse/JASPER-239

## Description
- Update CourtListController/Service /generate-report endpoint to use the Content-Disposition so that the report has a filename that came from PCSS
- Add Print Court List menu and dialog
- Integrate with the backend endpoint to generate report

## Type of change
- [x] New feature (non-breaking change which adds functionality)

## How Has This Been Tested?
- [x] Tested locally via `manage` script

## Checklist:
- [x] My code follows the style guidelines of this project
- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my feature works
- [x] New and existing unit tests pass locally with my changes
- [x] Any dependent changes have been merged and published in downstream modules   


Diff:
diff --git a/api/Controllers/CourtListController.cs b/api/Controllers/CourtListController.cs
index 9aa2dc1e1..52632da6e 100644
--- a/api/Controllers/CourtListController.cs
+++ b/api/Controllers/CourtListController.cs
@@ -60,7 +60,9 @@ public async Task<IActionResult> GenerateReport([FromQuery] CourtListReportReque
                 return BadRequest(validationResult.Errors.Select(e => e.ErrorMessage));
             }
 
-            var pdfStream = await _courtListService.GenerateReportAsync(request);
+            var (pdfStream, contentDisposition) = await _courtListService.GenerateReportAsync(request);
+
+            Response.Headers.ContentDisposition = contentDisposition;
 
             return new FileStreamResult(pdfStream, "application/pdf");
         }
diff --git a/api/Services/CourtListService.cs b/api/Services/CourtListService.cs
index 593e73442..5fee85585 100644
--- a/api/Services/CourtListService.cs
+++ b/api/Services/CourtListService.cs
@@ -143,7 +143,7 @@ await _filesClient.FilesCourtCalendarDetailsByDayAsync(_requestAgencyIdentifierI
             return courtList;
         }
 
-        public async virtual Task<Stream> GenerateReportAsync(CourtListReportRequest request)
+        public async virtual Task<(Stream, string)> GenerateReportAsync(CourtListReportRequest request)
         {
             return await _reportServiceClient.GetCourtListReportAsync(
                 request.CourtDivision,
diff --git a/pcss-client/Clients/ReportServicesClient.cs b/pcss-client/Clients/ReportServicesClient.cs
index e66f45735..753123dc6 100644
--- a/pcss-client/Clients/ReportServicesClient.cs
+++ b/pcss-client/Clients/ReportServicesClient.cs
@@ -47,7 +47,7 @@ private static Newtonsoft.Json.JsonSerializerSettings CreateSerializerSettings()
 
     /// <returns>OK</returns>
     /// <exception cref="ApiException">A server side error occurred.</exception>
-    public virtual System.Threading.Tasks.Task<Stream> GetCourtListReportAsync(string courtDivision, System.DateTimeOffset date, int location, string courtClass, string room, string additionsList, string reportType)
+    public virtual System.Threading.Tasks.Task<(Stream, string)> GetCourtListReportAsync(string courtDivision, System.DateTimeOffset date, int location, string courtClass, string room, string additionsList, string reportType)
     {
         return GetCourtListReportAsync(courtDivision, date, location, courtClass, room, additionsList, reportType, System.Threading.CancellationToken.None);
     }
@@ -55,7 +55,7 @@ public virtual System.Threading.Tasks.Task<Stream> GetCourtListReportAsync(strin
     /// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
     /// <returns>OK</returns>
     /// <exception cref="ApiException">A server side error occurred.</exception>
-    public virtual async System.Threading.Tasks.Task<Stream> GetCourtListReportAsync(string courtDivision, System.DateTimeOffset date, int location, string courtClass, string room, string additionsList, string reportType, System.Threading.CancellationToken cancellationToken)
+    public virtual async System.Threading.Tasks.Task<(Stream, string)> GetCourtListReportAsync(string courtDivision, System.DateTimeOffset date, int location, string courtClass, string room, string additionsList, string reportType, System.Threading.CancellationToken cancellationToken)
     {
         if (courtDivision == null)
             throw new System.ArgumentNullException("courtDivision");
@@ -129,7 +129,11 @@ public virtual async System.Threading.Tasks.Task<Stream> GetCourtListReportAsync
                         var memoryStream = new MemoryStream();
                         await response_.Content.CopyToAsync(memoryStream).ConfigureAwait(false);
                         memoryStream.Position = 0;
-                        return memoryStream;
+
+                        // Get the filename
+                        var contentDisposition = response_.Content.Headers.GetValues("Content-Disposition").FirstOrDefault() ?? "";
+
+                        return (memoryStream, contentDisposition);
                     }
                     else
                     {
diff --git a/tests/api/Controllers/CourtListControllerTests.cs b/tests/api/Controllers/CourtListControllerTests.cs
index b25da7548..5c74b863a 100644
--- a/tests/api/Controllers/CourtListControllerTests.cs
+++ b/tests/api/Controllers/CourtListControllerTests.cs
@@ -13,6 +13,7 @@
 using JCCommon.Clients.LookupCodeServices;
 using LazyCache;
 using LazyCache.Providers;
+using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Mvc;
 using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Extensions.Configuration;
@@ -198,6 +199,14 @@ public async Task GenerateReport_ShoulReturnBadRequest_WhenQueryParamsIsInvalid(
         public async Task GenerateReport_ShoulReturnFileStreamResult()
         {
             var mockValidationResult = new FluentValidation.Results.ValidationResult();
+            var fakeContentDisposition = $"inline; filename={Path.GetRandomFileName()}";
+            var mockResponse = new Mock<HttpResponse>();
+            var headers = new HeaderDictionary();
+            mockResponse.SetupGet(r => r.Headers).Returns(headers);
+
+            var mockHttpContext = new Mock<HttpContext>();
+            mockHttpContext.SetupGet(c => c.Response).Returns(mockResponse.Object);
+
             var mockCourtListService = this.SetupCourtListService();
             _mockReportValidator
                 .Setup(v => v.ValidateAsync(
@@ -206,9 +215,15 @@ public async Task GenerateReport_ShoulReturnFileStreamResult()
                 .ReturnsAsync(mockValidationResult);
             mockCourtListService
                 .Setup(c => c.GenerateReportAsync(It.IsAny<CourtListReportRequest>()))
-                .ReturnsAsync(new MemoryStream());
+                .ReturnsAsync((new MemoryStream(), fakeContentDisposition));
 
-            var controller = new CourtListController(mockCourtListService.Object, _mockReportValidator.Object);
+            var controller = new CourtListController(mockCourtListService.Object, _mockReportValidator.Object)
+            {
+                ControllerContext = new ControllerContext
+                {
+                    HttpContext = mockHttpContext.Object
+                }
+            };
             var result = await controller.GenerateReport(new CourtListReportRequest());
 
             Assert.IsType<FileStreamResult>(result);
diff --git a/tests/api/Services/CourtListServiceTests.cs b/tests/api/Services/CourtListServiceTests.cs
index 2887487f7..90e595ba6 100644
--- a/tests/api/Services/CourtListServiceTests.cs
+++ b/tests/api/Services/CourtListServiceTests.cs
@@ -81,6 +81,8 @@ Mock<ReportServicesClient> mockReportClient
     public async Task GenerateReport_ShouldReturnStream()
     {
         var (courtListService, mockReportClient) = SetupCourtListService();
+        var fakeContentDisposition = $"inline; filename={Path.GetRandomFileName()}";
+
         mockReportClient
             .Setup(r => r.GetCourtListReportAsync(
                 It.IsAny<string>(),
@@ -90,11 +92,12 @@ public async Task GenerateReport_ShouldReturnStream()
                 It.IsAny<string>(),
                 It.IsAny<string>(),
                 It.IsAny<string>()))
-            .ReturnsAsync(new MemoryStream());
+            .ReturnsAsync((new MemoryStream(), fakeContentDisposition));
 
-        var result = await courtListService.GenerateReportAsync(new CourtListReportRequest());
+        var (stream, contentDisposition) = await courtListService.GenerateReportAsync(new CourtListReportRequest());
 
-        Assert.NotNull(result);
+        Assert.NotNull(stream);
+        Assert.Equal(fakeContentDisposition, contentDisposition);
         mockReportClient
             .Verify(r => r
                 .GetCourtListReportAsync(
diff --git a/web/src/components/courtlist/CourtList.vue b/web/src/components/courtlist/CourtList.vue
index 1574ea7cf..3fc6af8fc 100644
--- a/web/src/components/courtlist/CourtList.vue
+++ b/web/src/components/courtlist/CourtList.vue
@@ -57,15 +57,22 @@
           <template v-slot:bottom />
         </v-data-table> -->
       </template>
+      <court-list-table-search-dialog
+        v-model:showDialog="showDialog"
+        v-model:types="classes"
+        :on-generate="onGenerateClick"
+      />
     </v-skeleton-loader>
   </v-container>
 </template>
 
 <script setup lang="ts">
+  import { CourtListService, LookupService } from '@/services';
   import { HttpService } from '@/services/HttpService';
+  import { DivisionEnum, LookupCode } from '@/types/common';
   import { CourtListCardInfo } from '@/types/courtlist';
   import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
-  import { computed, inject, ref } from 'vue';
+  import { computed, inject, onMounted, provide, ref } from 'vue';
   import CourtListTableSearch from './CourtListTableSearch.vue';
 
   const errorCode = ref(0);
@@ -84,6 +91,22 @@
     'To be called': (appearance: any) =>
       appearance.appearanceStatusCd === 'SCHD',
   };
+  const showDialog = ref(false);
+  const classes = ref<LookupCode[]>([]);
+  let selectedLocationId: string = '';
+  let selectedCourtRoom: string = '';
+
+  onMounted(async () => {
+    await getCourtClasses();
+  });
+
+  const getCourtClasses = async () => {
+    const data = await lookupService?.getCourtClasses();
+    classes.value =
+      data?.filter(
+        (c) => c.longDesc === DivisionEnum.R || c.longDesc === DivisionEnum.I
+      ) || [];
+  };
 
   const filterByAMPM = (pairing: any) =>
     !selectedAMPMFilter.value || pairing.card.amPM === selectedAMPMFilter.value;
@@ -112,11 +135,17 @@
   );
 
   const httpService = inject<HttpService>('httpService');
-  if (!httpService) {
-    throw new Error('Service is undefined.');
+  const lookupService = inject<LookupService>('lookupService');
+  const courtListService = inject<CourtListService>('courtListService');
+  if (!httpService || !lookupService || !courtListService) {
+    throw new Error('Service(s) is undefined.');
   }
 
-  const populateCardTablePairings = (data: any) => {
+  const populateCardTablePairings = (
+    data: any,
+    locationId: string,
+    courtRoom: string
+  ) => {
     cardTablePairings.value = [];
     if (!data?.items?.length) {
       return;
@@ -137,6 +166,9 @@
       card.amPM = adjudicatorDetails?.amPm;
       cardTablePairings.value.push({ card, table: courtList.appearances });
     });
+
+    selectedLocationId = locationId;
+    selectedCourtRoom = courtRoom;
   };
 
   const addDay = (days: number) => {
@@ -147,4 +179,34 @@
       selectedDate.value = appliedDate.value;
     }
   };
+
+  provide('menuClicked', (output) => {
+    showDialog.value = true;
+  });
+
+  const onGenerateClick = (
+    type: string,
+    reportType: string,
+    additions: string
+  ) => {
+    const selectedClass = classes.value.find((c) => c.code === type)!;
+
+    let queryParams: Record<string, any> = {
+      courtDivision: selectedClass.longDesc,
+      courtClass: selectedClass.code,
+      date: selectedDate.value,
+      locationId: selectedLocationId,
+      roomCode: selectedCourtRoom,
+    };
+
+    if (selectedClass!.longDesc === DivisionEnum.R) {
+      queryParams.reportType = reportType;
+    } else {
+      queryParams.additionsList = additions;
+    }
+
+    const url = courtListService.generateReportUrl(queryParams);
+
+    window.open(url, '_blank');
+  };
 </script>
diff --git a/web/src/components/courtlist/CourtListSearch.vue b/web/src/components/courtlist/CourtListSearch.vue
index 308b31388..a321d3e9b 100644
--- a/web/src/components/courtlist/CourtListSearch.vue
+++ b/web/src/components/courtlist/CourtListSearch.vue
@@ -113,7 +113,15 @@
   const date = defineModel<Date>('date');
   const appliedDate = defineModel<Date | null>('appliedDate');
 
-  const emit = defineEmits(['courtListSearched']);
+  const emit =
+    defineEmits<
+      (
+        e: 'courtListSearched',
+        data: any,
+        selectedLocationId: string,
+        selectedCourtRoom: string
+      ) => void
+    >();
   const GREEN = '#62d3a4';
   const commonStore = useCommonStore();
   const courtListStore = useCourtListStore();
@@ -201,7 +209,12 @@
         if (data) {
           courtListStore.courtListInformation.detailsData = data;
         }
-        emit('courtListSearched', data);
+        emit(
+          'courtListSearched',
+          data,
+          selectedCourtLocation.value!.locationId,
+          selectedCourtRoom.value
+        );
       })
       .finally(() => {
         isMounted.value = true;
diff --git a/web/src/components/courtlist/CourtListTableSearch.vue b/web/src/components/courtlist/CourtListTableSearch.vue
index 100c6e8a2..49fe263cd 100644
--- a/web/src/components/courtlist/CourtListTableSearch.vue
+++ b/web/src/components/courtlist/CourtListTableSearch.vue
@@ -19,6 +19,7 @@
         label="Files"
         placeholder="All files"
         :items="['To be called', 'Complete', 'Cancelled']"
+        hide-details
       >
       </v-select>
     </v-col>
@@ -28,19 +29,36 @@
         label="Time"
         placeholder="AM and PM"
         :items="['AM', 'PM']"
+        hide-details
       >
       </v-select>
     </v-col>
-    <v-col cols="2">
+    <v-col cols="2" class="d-flex align-center">
       <action-buttons :showSearch="false" size="large" @reset="reset" />
     </v-col>
+    <v-col class="d-flex align-center justify-end">
+      <v-menu location="start">
+        <template v-slot:activator="{ props }">
+          <v-icon v-bind="props" hover :icon="mdiMenu" size="32" />
+        </template>
+        <v-list>
+          <v-list-item
+            v-for="item in menuItems"
+            :key="item.title"
+            @click="onMenuClicked(item.value)"
+            hover
+          >
+            {{ item.title }}
+          </v-list-item>
+        </v-list>
+      </v-menu>
+    </v-col>
   </v-row>
 </template>
-
 <script setup lang="ts">
-  import { mdiMagnify } from '@mdi/js';
+  import { mdiMagnify, mdiMenu } from '@mdi/js';
+  import { inject } from 'vue';
   import ActionButtons from '../shared/Form/ActionButtons.vue';
-
   const selectedFiles = defineModel<string>('filesFilter');
   const selectedAMPM = defineModel<string>('AMPMFilter');
   const search = defineModel<string>('search');
@@ -50,4 +68,8 @@
     selectedAMPM.value = undefined;
     search.value = undefined;
   };
+
+  const onMenuClicked = inject<(value: string) => void>('menuClicked')!;
+
+  const menuItems = [{ title: 'Print Court List', value: 'print' }];
 </script>
diff --git a/web/src/components/courtlist/CourtListTableSearchDialog.vue b/web/src/components/courtlist/CourtListTableSearchDialog.vue
new file mode 100644
index 000000000..770da800e
--- /dev/null
+++ b/web/src/components/courtlist/CourtListTableSearchDialog.vue
@@ -0,0 +1,131 @@
+<template>
+  <v-dialog v-model="showDialog" @update:modelValue="closeDialog">
+    <v-card rounded="md" class="pa-3">
+      <v-card-title
+        ><div class="d-flex justify-space-between align-center w-100">
+          <span>Print Court List</span>
+          <v-btn icon @click="closeDialog">
+            <v-icon :icon="mdiClose" size="32" />
+          </v-btn>
+        </div>
+      </v-card-title>
+      <v-card-text>
+        <v-form ref="form" v-model="isFormValid">
+          <v-row no-gutters>
+            <v-col cols="12" class="mb-2">
+              <label for="type">Type</label>
+              <v-select
+                id="type"
+                :items="types"
+                item-title="shortDesc"
+                item-value="code"
+                v-model="selectedType"
+                placeholder="Select a Type"
+                :rules="[(v) => !!v || 'Type is required.']"
+              />
+            </v-col>
+            <v-col cols="12" v-if="showReportTypeComputed">
+              <label for="reportType">Report type</label>
+              <v-select
+                id="reportType"
+                :items="['Additions', 'Daily']"
+                v-model="selectedReportType"
+                placeholder="Select a Report Type"
+                :rules="[(v) => !!v || 'Report type is required.']"
+              />
+            </v-col>
+            <v-col cols="12" v-if="showAdditionsComputed">
+              <label for="additions">Additions</label>
+              <v-select
+                id="additions"
+                :items="additionsOptions"
+                item-title="text"
+                item-value="value"
+                v-model="selectedAdditions"
+                :rules="[(v) => !!v || 'Additions is required.']"
+              />
+            </v-col>
+          </v-row>
+        </v-form>
+      </v-card-text>
+      <v-card-actions class="d-flex justify-center">
+        <v-btn-tertiary type="submit" @click="generateReport"
+          >Print</v-btn-tertiary
+        >
+        <v-btn-secondary @click="closeDialog">Close</v-btn-secondary>
+      </v-card-actions>
+    </v-card>
+  </v-dialog>
+</template>
+<script setup lang="ts">
+  import { DivisionEnum, LookupCode } from '@/types/common';
+  import { mdiClose } from '@mdi/js';
+  import { computed, ref } from 'vue';
+  import { VForm } from 'vuetify/components';
+
+  const showDialog = defineModel<boolean>('showDialog');
+  const types = defineModel<LookupCode[]>('types', { default: () => [] });
+  const isFormValid = ref(false);
+  const selectedType = ref(null);
+  const selectedReportType = ref('Daily');
+  const selectedAdditions = ref(null);
+  const form = ref<VForm | null>(null);
+
+  const additionsOptions = [
+    { text: 'Yes', value: 'Y' },
+    { text: 'No', value: 'N' },
+  ];
+
+  const props = defineProps({
+    onGenerate: { type: Function, default: () => {} },
+  });
+
+  const showReportTypeComputed = computed(() => {
+    const type = types.value.find((c) => c.code === selectedType.value);
+    return (
+      selectedType.value !== null && type && type.longDesc == DivisionEnum.R
+    );
+  });
+
+  const showAdditionsComputed = computed(() => {
+    const type = types.value.find((c) => c.code === selectedType.value);
+    return (
+      selectedType.value !== null && type && type.longDesc == DivisionEnum.I
+    );
+  });
+
+  const closeDialog = () => {
+    selectedReportType.value = 'Daily';
+    selectedAdditions.value = null;
+    selectedType.value = null;
+    showDialog.value = false;
+  };
+
+  const generateReport = async () => {
+    if (!form.value) {
+      return;
+    }
+
+    const { valid: isValid } = await form.value.validate();
+
+    if (!isValid) {
+      return;
+    }
+
+    props.onGenerate(
+      selectedType.value!,
+      selectedReportType.value,
+      selectedAdditions.value!
+    );
+    closeDialog();
+  };
+</script>
+<style scoped>
+  .v-dialog {
+    max-width: 500px;
+  }
+
+  .v-dialog .v-card {
+    border-radius: 1rem;
+  }
+</style>
diff --git a/web/src/services/CourtListService.ts b/web/src/services/CourtListService.ts
new file mode 100644
index 000000000..8f925c953
--- /dev/null
+++ b/web/src/services/CourtListService.ts
@@ -0,0 +1,16 @@
+import { HttpService } from './HttpService';
+
+export class CourtListService {
+  private readonly httpService: HttpService;
+
+  constructor(httpService: HttpService) {
+    this.httpService = httpService;
+  }
+
+  generateReportUrl(params: Record<string, any> = {}): string {
+    return this.httpService.client.getUri({
+      url: 'api/courtlist/generate-report',
+      params,
+    });
+  }
+}
diff --git a/web/src/services/LookupService.ts b/web/src/services/LookupService.ts
index 61f61f0a6..c07bf5c28 100644
--- a/web/src/services/LookupService.ts
+++ b/web/src/services/LookupService.ts
@@ -2,7 +2,7 @@ import { LookupCode } from '../types/common';
 import { HttpService } from './HttpService';
 
 export class LookupService {
-  private httpService: HttpService;
+  private readonly httpService: HttpService;
 
   constructor(httpService: HttpService) {
     this.httpService = httpService;
diff --git a/web/src/services/index.ts b/web/src/services/index.ts
index 297aecc75..9252b9260 100644
--- a/web/src/services/index.ts
+++ b/web/src/services/index.ts
@@ -1,5 +1,6 @@
 import { App } from 'vue';
 import { AuthService } from './AuthService';
+import { CourtListService } from './CourtListService';
 import { DashboardService } from './DashboardService';
 import { FilesService } from './FilesService';
 import { HttpService } from './HttpService';
@@ -13,6 +14,7 @@ export function registerRouter(app: App) {
   const locationService = new LocationService(httpService);
   const filesService = new FilesService(httpService);
   const dashboardService = new DashboardService(httpService);
+  const courtListService = new CourtListService(httpService);
 
   app.provide('httpService', httpService);
   app.provide('authService', authService);
@@ -20,9 +22,11 @@ export function registerRouter(app: App) {
   app.provide('locationService', locationService);
   app.provide('filesService', filesService);
   app.provide('dashboardService', dashboardService);
+  app.provide('courtListService', courtListService);
 }
 
 export * from './AuthService';
+export * from './CourtListService';
 export * from './DashboardService';
 export * from './FilesService';
 export * from './LocationService';
diff --git a/web/src/types/common/index.ts b/web/src/types/common/index.ts
index 34198e568..34e94da2e 100644
--- a/web/src/types/common/index.ts
+++ b/web/src/types/common/index.ts
@@ -85,3 +85,8 @@ export interface KeyValueInfo {
   key: string;
   value: string;
 }
+
+export enum DivisionEnum {
+  R = 'R',
+  I = 'I',
+}
diff --git a/web/tests/components/courtList/CourtListTableSearchDialog.test.ts b/web/tests/components/courtList/CourtListTableSearchDialog.test.ts
new file mode 100644
index 000000000..b052a7cd9
--- /dev/null
+++ b/web/tests/components/courtList/CourtListTableSearchDialog.test.ts
@@ -0,0 +1,119 @@
+import CourtListTableSearchDialog from '@/components/courtlist/CourtListTableSearchDialog.vue';
+import { mount } from '@vue/test-utils';
+import { describe, expect, it, vi } from 'vitest';
+import { nextTick } from 'vue';
+import { createVuetify } from 'vuetify';
+import { VForm } from 'vuetify/lib/components/index.mjs';
+
+const vuetify = createVuetify();
+
+describe('CourtListTableSearchDialog', () => {
+  const mockOnGenerate = vi.fn();
+  const mountOptions = {
+    props: {
+      onGenerate: mockOnGenerate,
+    },
+    global: {
+      plugins: [vuetify],
+    },
+  };
+
+  it('modelValue is set to true when dialog is shown', async () => {
+    const wrapper = mount(CourtListTableSearchDialog, mountOptions);
+
+    (wrapper.vm as unknown as { showDialog: boolean }).showDialog = true;
+
+    await nextTick();
+
+    const dialog = wrapper.find('v-dialog');
+    const isShown = dialog.element.getAttribute('modelvalue') === 'true';
+
+    expect(dialog.exists()).toBe(true);
+    expect(isShown).toBe(true);
+  });
+
+  it(`modelValue is set to false when dialog not shown`, async () => {
+    const wrapper = mount(CourtListTableSearchDialog, mountOptions);
+
+    (wrapper.vm as unknown as { showDialog: boolean }).showDialog = false;
+
+    await nextTick();
+
+    const dialog = wrapper.find('v-dialog');
+    const isHidden = dialog.element.getAttribute('modelvalue') === 'false';
+    expect(isHidden).toBe(true);
+  });
+
+  it(`should show report type when selected class is of type 'criminal'`, async () => {
+    const wrapper: any = mount(CourtListTableSearchDialog, mountOptions);
+    wrapper.vm.types = [
+      {
+        shortDesc: 'Adult',
+        code: 'A',
+        codeType: '',
+        longDesc: 'R',
+      },
+      {
+        code: 'L',
+        shortDesc: 'Enforcement/Legislated Statute',
+        longDesc: 'I',
+        codeType: '',
+      },
+    ];
+    wrapper.vm.selectedType = 'A';
+
+    await nextTick();
+
+    const allSelect = wrapper.findAll('v-select');
+
+    expect(allSelect.length).toBe(2);
+    expect(allSelect[1].element.id).toBe('reportType');
+  });
+
+  it(`should show additions when selected class is of type 'civil'`, async () => {
+    const wrapper: any = mount(CourtListTableSearchDialog, mountOptions);
+
+    wrapper.vm.types = [
+      {
+        shortDesc: 'Adult',
+        code: 'A',
+        codeType: '',
+        longDesc: 'R',
+      },
+      {
+        code: 'L',
+        shortDesc: 'Enforcement/Legislated Statute',
+        longDesc: 'I',
+        codeType: '',
+      },
+    ];
+    wrapper.vm.selectedType = 'L';
+
+    await nextTick();
+
+    const allSelect = wrapper.findAll('v-select');
+
+    expect(allSelect.length).toBe(2);
+    expect(allSelect[1].element.id).toBe('additions');
+  });
+
+  it(`clicking Print should validate and emit 'update:showDialog'`, async () => {
+    const wrapper = mount(CourtListTableSearchDialog, mountOptions);
+
+    const vm = wrapper.vm as unknown as {
+      selectedType: string;
+      selectedReportType: string;
+      generateReport: () => void;
+      form: VForm;
+    };
+
+    vm.selectedType = 'A';
+    vm.selectedReportType = 'Daily';
+
+    vm.generateReport();
+
+    await nextTick();
+
+    expect(mockOnGenerate).toHaveBeenCalled();
+  });
+});


Reviews:
user: login: WadeBarnes
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-02T11:54:14Z
user: login: ronaldo-macapobre
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T14:56:01Z
user: login: JTraill
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T16:04:52Z
user: login: JTraill
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T16:07:44Z
user: login: JTraill
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T16:31:01Z
user: login: JTraill
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-02T16:34:45Z
user: login: ronaldo-macapobre
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-03T00:38:35Z

Review comments:
diff_hunk: @@ -85,3 +85,8 @@ export interface KeyValueInfo {
   key: string;
   value: string;
 }
+
+export enum DivisionEnum {
+  CRIMINAL = 'R',
+  CIVIL = 'I'
+}
path: web/src/types/common/index.ts
user: login: WadeBarnes
body: It's best practice to avoid defining these sorts of lookup codes in the frontend.
created_at: 2025-04-02T11:54:08Z
author_association: MEMBER
diff_hunk: @@ -85,3 +85,8 @@ export interface KeyValueInfo {
   key: string;
   value: string;
 }
+
+export enum DivisionEnum {
+  CRIMINAL = 'R',
+  CIVIL = 'I'
+}
path: web/src/types/common/index.ts
user: login: ronaldo-macapobre
body: Okay, `DivisionEnum` names has been renamed.
created_at: 2025-04-02T14:56:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+<template>
+  <v-dialog v-model="showDialogComputed">
+    <v-card rounded="md" class="pa-3">
+      <v-card-title
+        ><div class="d-flex justify-space-between align-center w-100">
+          <span>Print Court List</span>
+          <v-btn icon @click="closeDialog">
+            <v-icon :icon="mdiClose" size="32" />
+          </v-btn>
+        </div>
+      </v-card-title>
+      <v-card-text>
+        <v-form ref="form" v-model="isFormValid">
+          <v-row no-gutters>
+            <v-col cols="12" class="mb-2">
+              <label for="type">Type</label>
+              <v-select
+                id="type"
+                :items="types"
+                item-title="shortDesc"
+                item-value="code"
+                v-model="selectedType"
+                placeholder="Select a Type"
+                :rules="[(v) => !!v || 'Type is required.']"
+              />
+            </v-col>
+            <v-col cols="12" v-if="showReportTypeComputed">
+              <label for="reportType">Report type</label>
+              <v-select
+                id="reportType"
+                :items="['Additions', 'Daily']"
+                v-model="selectedReportType"
+                placeholder="Select a Report Type"
+                :rules="[(v) => !!v || 'Report type is required.']"
+              />
+            </v-col>
+            <v-col cols="12" v-if="showAdditionsComputed">
+              <label for="additions">Additions</label>
+              <v-select
+                id="additions"
+                :items="additionsOptions"
+                item-title="text"
+                item-value="value"
+                v-model="selectedAdditions"
+                :rules="[(v) => !!v || 'Additions is required.']"
+              />
+            </v-col>
+          </v-row>
+        </v-form>
+      </v-card-text>
+      <v-card-actions class="d-flex justify-center">
+        <v-btn-tertiary type="submit" @click="generateReport"
+          >Print</v-btn-tertiary
+        >
+        <v-btn-secondary @click="closeDialog">Close</v-btn-secondary>
+      </v-card-actions>
+    </v-card>
+  </v-dialog>
+</template>
+<style scoped>
path: web/src/components/courtlist/CourtListTableSearchDialog.vue
user: login: JTraill
body: minor: its best practice to have the `style` at the bottom
https://vuejs.org/guide/scaling-up/sfc.html
created_at: 2025-04-02T15:56:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+<template>
+  <v-dialog v-model="showDialogComputed">
+    <v-card rounded="md" class="pa-3">
+      <v-card-title
+        ><div class="d-flex justify-space-between align-center w-100">
+          <span>Print Court List</span>
+          <v-btn icon @click="closeDialog">
+            <v-icon :icon="mdiClose" size="32" />
+          </v-btn>
+        </div>
+      </v-card-title>
+      <v-card-text>
+        <v-form ref="form" v-model="isFormValid">
+          <v-row no-gutters>
+            <v-col cols="12" class="mb-2">
+              <label for="type">Type</label>
+              <v-select
+                id="type"
+                :items="types"
+                item-title="shortDesc"
+                item-value="code"
+                v-model="selectedType"
+                placeholder="Select a Type"
+                :rules="[(v) => !!v || 'Type is required.']"
+              />
+            </v-col>
+            <v-col cols="12" v-if="showReportTypeComputed">
+              <label for="reportType">Report type</label>
+              <v-select
+                id="reportType"
+                :items="['Additions', 'Daily']"
+                v-model="selectedReportType"
+                placeholder="Select a Report Type"
+                :rules="[(v) => !!v || 'Report type is required.']"
+              />
+            </v-col>
+            <v-col cols="12" v-if="showAdditionsComputed">
+              <label for="additions">Additions</label>
+              <v-select
+                id="additions"
+                :items="additionsOptions"
+                item-title="text"
+                item-value="value"
+                v-model="selectedAdditions"
+                :rules="[(v) => !!v || 'Additions is required.']"
+              />
+            </v-col>
+          </v-row>
+        </v-form>
+      </v-card-text>
+      <v-card-actions class="d-flex justify-center">
+        <v-btn-tertiary type="submit" @click="generateReport"
+          >Print</v-btn-tertiary
+        >
+        <v-btn-secondary @click="closeDialog">Close</v-btn-secondary>
+      </v-card-actions>
+    </v-card>
+  </v-dialog>
+</template>
+<style scoped>
+  .v-dialog {
+    max-width: 500px;
+  }
+
+  .v-dialog .v-card {
+    border-radius: 1rem;
+  }
+</style>
+<script setup lang="ts">
+  import { DivisionEnum, LookupCode } from '@/types/common';
+  import { mdiClose } from '@mdi/js';
+  import { computed, ref } from 'vue';
+  import { VForm } from 'vuetify/lib/components/index.mjs';
+
+  const isFormValid = ref(false);
+  const selectedType = ref(null);
+  const selectedReportType = ref('Daily');
+  const selectedAdditions = ref(null);
+  const form = ref<VForm | null>(null);
+
+  const additionsOptions = [
+    { text: 'Yes', value: 'Y' },
+    { text: 'No', value: 'N' },
+  ];
+
+  const props = defineProps<{
+    showDialog: boolean;
+    types: LookupCode[];
+    onGenerateClicked: (
+      type: string,
+      reportType: string,
+      additions: string
+    ) => void;
+  }>();
+
+  const emit = defineEmits<(e: 'update:showDialog', value: boolean) => void>();
path: web/src/components/courtlist/CourtListTableSearchDialog.vue
user: login: JTraill
body: Thanks to Vue 3.4, instead of using `emits` Id recommend utilizing the showDialog as a two way binding
https://vuejs.org/guide/components/v-model.html
created_at: 2025-04-02T16:01:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+<template>
+  <v-dialog v-model="showDialogComputed">
+    <v-card rounded="md" class="pa-3">
+      <v-card-title
+        ><div class="d-flex justify-space-between align-center w-100">
+          <span>Print Court List</span>
+          <v-btn icon @click="closeDialog">
+            <v-icon :icon="mdiClose" size="32" />
+          </v-btn>
+        </div>
+      </v-card-title>
+      <v-card-text>
+        <v-form ref="form" v-model="isFormValid">
+          <v-row no-gutters>
+            <v-col cols="12" class="mb-2">
+              <label for="type">Type</label>
+              <v-select
+                id="type"
+                :items="types"
+                item-title="shortDesc"
+                item-value="code"
+                v-model="selectedType"
+                placeholder="Select a Type"
+                :rules="[(v) => !!v || 'Type is required.']"
+              />
+            </v-col>
+            <v-col cols="12" v-if="showReportTypeComputed">
+              <label for="reportType">Report type</label>
+              <v-select
+                id="reportType"
+                :items="['Additions', 'Daily']"
+                v-model="selectedReportType"
+                placeholder="Select a Report Type"
+                :rules="[(v) => !!v || 'Report type is required.']"
+              />
+            </v-col>
+            <v-col cols="12" v-if="showAdditionsComputed">
+              <label for="additions">Additions</label>
+              <v-select
+                id="additions"
+                :items="additionsOptions"
+                item-title="text"
+                item-value="value"
+                v-model="selectedAdditions"
+                :rules="[(v) => !!v || 'Additions is required.']"
+              />
+            </v-col>
+          </v-row>
+        </v-form>
+      </v-card-text>
+      <v-card-actions class="d-flex justify-center">
+        <v-btn-tertiary type="submit" @click="generateReport"
+          >Print</v-btn-tertiary
+        >
+        <v-btn-secondary @click="closeDialog">Close</v-btn-secondary>
+      </v-card-actions>
+    </v-card>
+  </v-dialog>
+</template>
+<style scoped>
+  .v-dialog {
+    max-width: 500px;
+  }
+
+  .v-dialog .v-card {
+    border-radius: 1rem;
+  }
+</style>
+<script setup lang="ts">
+  import { DivisionEnum, LookupCode } from '@/types/common';
+  import { mdiClose } from '@mdi/js';
+  import { computed, ref } from 'vue';
+  import { VForm } from 'vuetify/lib/components/index.mjs';
+
+  const isFormValid = ref(false);
+  const selectedType = ref(null);
+  const selectedReportType = ref('Daily');
+  const selectedAdditions = ref(null);
+  const form = ref<VForm | null>(null);
+
+  const additionsOptions = [
+    { text: 'Yes', value: 'Y' },
+    { text: 'No', value: 'N' },
+  ];
+
+  const props = defineProps<{
path: web/src/components/courtlist/CourtListTableSearchDialog.vue
user: login: JTraill
body: I'm a bit confused, i see you passing v-models to this component, yet they're defined here as props

```
<court-list-table-search-dialog
        v-model:showDialog="showDialog"
        v-model:types="classes"
        v-model:onGenerateClicked="onGenerateClicked"
      />
```
created_at: 2025-04-02T16:02:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,109 @@
+import CourtListTableSearchDialog from '@/components/courtlist/CourtListTableSearchDialog.vue';
+import { mount } from '@vue/test-utils';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { createVuetify } from 'vuetify';
+import { VForm } from 'vuetify/components';
+
+const vuetify = createVuetify();
+
+describe('CourtListTableSearchDialog', () => {
+  const mockOnGenerateClicked = vi.fn();
+  const mountOptions = {
+    props: {
+      showDialog: true,
+      types: [
+        {
+          shortDesc: 'Adult',
+          code: 'A',
+          codeType: '',
+          longDesc: 'R',
+        },
+        {
+          code: 'L',
+          shortDesc: 'Enforcement/Legislated Statute',
+          longDesc: 'I',
+          codeType: '',
+        },
+      ],
+      onGenerateClicked: mockOnGenerateClicked,
+    },
+    global: {
+      plugins: [vuetify],
+    },
+  };
+
+  beforeEach(() => {
+    mountOptions.props.showDialog = true;
+  });
+
+  it('modelValue is set to true when dialog is shown', async () => {
+    const wrapper = mount(CourtListTableSearchDialog, mountOptions);
+
+    const dialog = wrapper.find('v-dialog');
+    const isShown = dialog.element.getAttribute('modelvalue') === 'true';
+    expect(dialog.exists()).toBe(true);
+    expect(isShown).toBe(true);
+  });
+
+  it(`modelValue is set to false when dialog not shown`, async () => {
+    mountOptions.props.showDialog = false;
+
+    const wrapper = mount(CourtListTableSearchDialog, mountOptions);
+
+    const dialog = wrapper.find('v-dialog');
+    const isHidden = dialog.element.getAttribute('modelvalue') === 'false';
+    expect(isHidden).toBe(true);
+  });
+
+  it(`should show report type when selected class is of type 'criminal'`, async () => {
+    const wrapper = mount(CourtListTableSearchDialog, mountOptions);
+
+    (wrapper.vm as unknown as { selectedType: string }).selectedType = 'A';
+
+    await wrapper.vm.$nextTick();
+
+    const allSelect = wrapper.findAll('v-select');
+
+    expect(allSelect.length).toBe(2);
+    expect(allSelect[1].element.id).toBe('reportType');
+  });
+
+  it(`should show additions when selected class is of type 'civil'`, async () => {
+    const wrapper = mount(CourtListTableSearchDialog, mountOptions);
+
+    (wrapper.vm as unknown as { selectedType: string }).selectedType = 'L';
+
+    await wrapper.vm.$nextTick();
path: web/tests/components/courtList/CourtListTableSearchDialog.test.ts
user: login: JTraill
body: polish: prefer to be consistent and use vue's built in `nextTick` method instead
`import { nextTick } from 'vue';`
created_at: 2025-04-02T16:04:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,109 @@
+import CourtListTableSearchDialog from '@/components/courtlist/CourtListTableSearchDialog.vue';
+import { mount } from '@vue/test-utils';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { createVuetify } from 'vuetify';
+import { VForm } from 'vuetify/components';
+
+const vuetify = createVuetify();
+
+describe('CourtListTableSearchDialog', () => {
+  const mockOnGenerateClicked = vi.fn();
+  const mountOptions = {
+    props: {
+      showDialog: true,
+      types: [
+        {
+          shortDesc: 'Adult',
+          code: 'A',
+          codeType: '',
+          longDesc: 'R',
+        },
+        {
+          code: 'L',
+          shortDesc: 'Enforcement/Legislated Statute',
+          longDesc: 'I',
+          codeType: '',
+        },
+      ],
+      onGenerateClicked: mockOnGenerateClicked,
+    },
+    global: {
+      plugins: [vuetify],
+    },
+  };
+
+  beforeEach(() => {
+    mountOptions.props.showDialog = true;
+  });
+
+  it('modelValue is set to true when dialog is shown', async () => {
+    const wrapper = mount(CourtListTableSearchDialog, mountOptions);
+
+    const dialog = wrapper.find('v-dialog');
+    const isShown = dialog.element.getAttribute('modelvalue') === 'true';
+    expect(dialog.exists()).toBe(true);
+    expect(isShown).toBe(true);
+  });
+
+  it(`modelValue is set to false when dialog not shown`, async () => {
+    mountOptions.props.showDialog = false;
+
+    const wrapper = mount(CourtListTableSearchDialog, mountOptions);
+
+    const dialog = wrapper.find('v-dialog');
+    const isHidden = dialog.element.getAttribute('modelvalue') === 'false';
+    expect(isHidden).toBe(true);
+  });
+
+  it(`should show report type when selected class is of type 'criminal'`, async () => {
+    const wrapper = mount(CourtListTableSearchDialog, mountOptions);
+
+    (wrapper.vm as unknown as { selectedType: string }).selectedType = 'A';
+
+    await wrapper.vm.$nextTick();
+
+    const allSelect = wrapper.findAll('v-select');
+
+    expect(allSelect.length).toBe(2);
+    expect(allSelect[1].element.id).toBe('reportType');
+  });
+
+  it(`should show additions when selected class is of type 'civil'`, async () => {
+    const wrapper = mount(CourtListTableSearchDialog, mountOptions);
+
+    (wrapper.vm as unknown as { selectedType: string }).selectedType = 'L';
+
+    await wrapper.vm.$nextTick();
path: web/tests/components/courtList/CourtListTableSearchDialog.test.ts
user: login: JTraill
body: https://vuejs.org/api/general#nexttick
created_at: 2025-04-02T16:04:46Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,138 @@
+<template>
+  <v-dialog v-model="showDialogComputed">
+    <v-card rounded="md" class="pa-3">
+      <v-card-title
+        ><div class="d-flex justify-space-between align-center w-100">
+          <span>Print Court List</span>
+          <v-btn icon @click="closeDialog">
+            <v-icon :icon="mdiClose" size="32" />
+          </v-btn>
+        </div>
+      </v-card-title>
+      <v-card-text>
+        <v-form ref="form" v-model="isFormValid">
+          <v-row no-gutters>
+            <v-col cols="12" class="mb-2">
+              <label for="type">Type</label>
+              <v-select
+                id="type"
+                :items="types"
+                item-title="shortDesc"
+                item-value="code"
+                v-model="selectedType"
+                placeholder="Select a Type"
+                :rules="[(v) => !!v || 'Type is required.']"
+              />
+            </v-col>
+            <v-col cols="12" v-if="showReportTypeComputed">
+              <label for="reportType">Report type</label>
+              <v-select
+                id="reportType"
+                :items="['Additions', 'Daily']"
+                v-model="selectedReportType"
+                placeholder="Select a Report Type"
+                :rules="[(v) => !!v || 'Report type is required.']"
+              />
+            </v-col>
+            <v-col cols="12" v-if="showAdditionsComputed">
+              <label for="additions">Additions</label>
+              <v-select
+                id="additions"
+                :items="additionsOptions"
+                item-title="text"
+                item-value="value"
+                v-model="selectedAdditions"
+                :rules="[(v) => !!v || 'Additions is required.']"
+              />
+            </v-col>
+          </v-row>
+        </v-form>
+      </v-card-text>
+      <v-card-actions class="d-flex justify-center">
+        <v-btn-tertiary type="submit" @click="generateReport"
+          >Print</v-btn-tertiary
+        >
+        <v-btn-secondary @click="closeDialog">Close</v-btn-secondary>
+      </v-card-actions>
+    </v-card>
+  </v-dialog>
+</template>
+<style scoped>
+  .v-dialog {
+    max-width: 500px;
+  }
+
+  .v-dialog .v-card {
+    border-radius: 1rem;
+  }
+</style>
+<script setup lang="ts">
+  import { DivisionEnum, LookupCode } from '@/types/common';
+  import { mdiClose } from '@mdi/js';
+  import { computed, ref } from 'vue';
+  import { VForm } from 'vuetify/lib/components/index.mjs';
+
+  const isFormValid = ref(false);
+  const selectedType = ref(null);
+  const selectedReportType = ref('Daily');
+  const selectedAdditions = ref(null);
+  const form = ref<VForm | null>(null);
+
+  const additionsOptions = [
+    { text: 'Yes', value: 'Y' },
+    { text: 'No', value: 'N' },
+  ];
+
+  const props = defineProps<{
+    showDialog: boolean;
+    types: LookupCode[];
+    onGenerateClicked: (
+      type: string,
+      reportType: string,
+      additions: string
+    ) => void;
+  }>();
+
+  const emit = defineEmits<(e: 'update:showDialog', value: boolean) => void>();
+
+  const showDialogComputed = computed({
+    get: () => props.showDialog,
+    set: (value) => emit('update:showDialog', value),
+  });
+
+  const showReportTypeComputed = computed(() => {
+    const c = props.types.find((c) => c.code === selectedType.value);
path: web/src/components/courtlist/CourtListTableSearchDialog.vue
user: login: JTraill
body: prefer variable names that are a bit more descriptive
created_at: 2025-04-02T16:07:44Z
author_association: CONTRIBUTOR
diff_hunk: @@ -28,19 +29,36 @@
         label="Time"
         placeholder="AM and PM"
         :items="['AM', 'PM']"
+        :hide-details="true"
path: web/src/components/courtlist/CourtListTableSearch.vue
user: login: JTraill
body: tip, you should be able to simplify to `hide-details`
created_at: 2025-04-02T16:31:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -137,6 +166,9 @@
       card.amPM = adjudicatorDetails?.amPm;
       cardTablePairings.value.push({ card, table: courtList.appearances });
     });
+
+    selectedLocationId = locationId;
+    selectedCourtRoom = courtRoom;
path: web/src/components/courtlist/CourtList.vue
user: login: JTraill
body: This reads to me that the card-table pairings that are needed to display the court-list data are getting their locations and court-rooms set, which isn't happening right?
created_at: 2025-04-02T16:34:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -137,6 +166,9 @@
       card.amPM = adjudicatorDetails?.amPm;
       cardTablePairings.value.push({ card, table: courtList.appearances });
     });
+
+    selectedLocationId = locationId;
+    selectedCourtRoom = courtRoom;
path: web/src/components/courtlist/CourtList.vue
user: login: ronaldo-macapobre
body: The initial intention of this functionality is to get the selected location id and court room. However, this will change based on the discussion in our sprint planning where the values will come from each card together with the Court Division and Class.
created_at: 2025-04-03T00:38:35Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-04-03T01:51:12Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=bcgov_jasper&pullRequest=254) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=bcgov_jasper&pullRequest=254&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=bcgov_jasper&pullRequest=254&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=bcgov_jasper&pullRequest=254&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=bcgov_jasper&pullRequest=254&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=bcgov_jasper&pullRequest=254&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=bcgov_jasper&pullRequest=254)


author_association: NONE